The Over-Engineering Trap
Note: this post is part of a series of blog posts to grow your engineering career.
I used to care a lot about making the best piece of software ever. In my 20s, I carefully looked at any line of code. I wanted to have the perfect program that could satisfy all features. I was sometimes re-deploying programs every day, just for “this new feature” or “this performance improvements that break the look earlier than before”.
I was proud of my code: it was so complex that almost no one understood how it worked. My code was doing plenty of stuff. And it was doing them fast. I thought I was so smart that nobody could match my skills.
In reality, I was just ignorant and incompetent. I did not know how to write software nor ship a product.
Today, we will see two traps young engineers fall into: the software complexity trap and the platform complexity trap.
Over-Engineered Code
Programmers love to depict themselves as the heroes that make the most optimized code or the best architecture. The hard reality is:
the compiler will do better than anybody at optimizing code ; often, optimizing offers marginal gains and is not necessary.
cost of compute is close to nothing today and optimizing code is not a problem worth solving (more on this later)
Instead of focusing on performance optimizing, engineers should focus on two key aspects:
architectural choices: the software architecture must be simple and not complex (in the big-O notation sense). For example, instead of choosing two arrays to find an element, use a hash map. Compilers can very rarely optimize. Such architectural tricks are good enough to scale the majority of systems.
maintainability: the code is easy to understand, anybody in your team can understand it so that their onboarding time is minimal
If optimizing your code requires increasing its complexity, the potential gains in CPU or memory gains are erased by the labor costs to maintain the software. As the cost of compute goes towards zero, any performance improvements offer limited gains.
By trying to optimize your code, you are making it more expensive to operate. The real cost of software is maintenance, not execution and for this reason, you should make your software straightforward to understand and maintain.
And if your software is very complex to support many features: ask yourself if you need all these features. A great product is done by mastering a few features set, not by having as many features as possible. Having too many features is an antipattern: it confuses the user and hides the key element of your product.
Take-away
Favor simplicity and maintainability over performance and complexity
Use simple and popular technologies and frameworks well understood
Over-Engineered Platform
Another trap junior engineers fall into is the platform complexity trap. It’s common to hear someone telling you that you absolutely need microservices, kafka queues and elasticsearch clusters for your new application. But for the majority of applications, it is a source of unnecessary complexity.
Microservices are great for solving massive scaling problems that only a few large-scale companies really face. Very often, CPU in microservices are idling, waiting for requests to process. This is very lucrative for the cloud vendors (who operate like airlines and overbook their capacity) but this is a complexity for the engineers, which comes at a cost.
The following video is very famous in Silicon Valley for one reason: it’s real.
Making your platform more complex makes is difficult for other engineers to ramp up and understand how to improve and maintain the system. It also comes at operational costs: engineers need to learn technologies they are not familiar with. Engineers are more likely to panic and take more time to resolve an incident with a technology they did not use before.
In 90% of the cases, all you need is an API running in an EC2 instance, a database that has indexes on its tables and maybe one offline job to do some compute (cost: $70 per month in AWS). ExpressJs (the most popular JS backend framework) can handle more than 10,000 requests per second (benchmark), which is enough for many applications, especially in the startup world. There is no reason to take more complicated setups unless you have very good reasons.
Codiga (the startup I founded) was using a few EC2 instances for the backend, a MySQL database and a few python runners to analyze customer code. The AWS bill was a few hundreds a month. And the system kept operating for more than ten of thousands of users. A similar base is used by larger companies like Front, which have been built on a JavaScript backend from the start and still operate with it.
Always start with a basic, monolith architecture. It keeps all the code in one place, it’s easy to deploy and allows you to iterate quickly. Only when you know you (will) have scaling problems, consider adding more complexity. Make this decision based on real-world metrics (e.g., number of Monthly Active Users - MAU, number of requests per second), not because someone pushed you to do it.
Take-away
Do not use complex platform services (microservices, kafka queues) unless you really have to
Use simple technologies anybody may know
Final Words
As a software engineer, your job is to deliver a product to a customer.
The code and the execution platform must be easy to understand and maintain. If you keep these principles in mind, it facilitates maintenance (fixing a bug or adding a new feature) and shortens the time for a new hire to be familiar and operational on your system.