In the past months, I have asked myself a lot why we always strive to write perfect code. Picking up coding again for an internal project made me realise our team (and probably a large part of the rest of the software development world) spend a lot of time on writing perfectly formatted, ordered, patterned and tested code. But is this really necessary?

As a software development company, we are constantly trying to balance budgets, time and features with our clients. As a result, features are changed or may not even get built because it would take too long and become too expensive. On the other hand, engineers have the feeling they need to rush things and can’t achieve the perfection they dream of. I guess this is recognisable for most agencies.

Some of our Software Engineers in action on our yearly company retreat.
Some of our Software Engineers in action on our yearly company retreat.

Last month I had a conversation with the CEO of one of our clients. Their CTO and Head of Engineering asked us to help rework a part of their codebase. It had become impossible to add new functionality without breaking anything else and no one really knew how everything worked. While running stable and fast, this highly successful startup’s code is a big mess from a technical point of view. The CEO asked me why we need to make this effort since, from his perspective, there was no real issue, development just needed to be faster at delivering new features.

In these cases, I think there is truth in both points of view. The engineers want to write perfect code using the latest techniques, make sure that the code is well documented so they can fully understand how everything works and that it has tested so they can easily update things later. Product owners on the other hand just want things to be done, fast and cheap, so they can ship new features or convince new clients.

How can you make these conflicting views work together?

Ignore the future, code for now

Most product companies go through a few phases. Each of these phases requires a different view of what “perfect” means. We could discuss long and hard about which phases exist, but for the sake of this article, I will just make the distinction between proof-of-concept code, MVP code and long-term code. Some examples of each to clarify.

When fleshing out a new idea for a product, it doesn’t make sense to spend any time on writing code that is open for extension, fully tested and conforming to the latest coding standards. The goal is to make a proof of concept, for example by connecting a few APIs or trying out a new interface idea. It is very unlikely anyone will have to dive into this code again when the goal is achieved.

When building a minimal viable product most people overestimate the need for good code. Every startup’s most important thing is to be out there with a nice looking, functional product. How it works under the hood doesn’t really matter. Until your MVP really gets traction you can run on shitty code or even do things manually to prove you have a product/market fit. Only once you nail it and the customers start flowing in, you should start caring about code, but up until then, you’re almost writing one-off code too.

As soon as those hard-earned customers start flowing in, you are most likely generating some revenue or have attracted outside money. Now is the right time to start thinking about clean, long-term code. This is the situation our client from the example in the introduction was in. Since your audience is most likely to grow a lot, you need to start considering performance, stability and availability a lot more. Your engineering team is also going to scale up. This will force you to implement coding standards, documentation standards and a bunch of other procedures and practices. You start to need perfect code.

You can see in each of these examples a difference in the goal of the code and a difference in what “perfect” means in those situations.

Perfect code does not exist

Given these different phases, a product can be in, a general definition of perfect code does not exist.

We work for a wide variety of clients with an even wider variety of codebases. Some of those we have started, others originated from the client or another development agency. In some cases, it is even a mix of our start, handed over to a client’s own development team for some time, but ending up with us again later on.

This experience shows that each project is different, uses different technologies, has different coding styles or programming patterns, but also that most of these solutions may have been perfect at that time. Still, with these kinds of handovers, engineers often complain about the work the other team did, it’s not perfect.

In reality, there is no such thing as the perfect way to do something. It might sound strange, but programming is not an exact science. There are multiple ways to do things, which might all be valid.

Dealing with non-perfect code

There is however a very big difference between not perfect and bad. Think about the Pareto principle and Sufficient Design.

Every programmer that is forced to work on a project with legacy code, an MVP or even an existing long-term product, will want to rewrite it. It puts them back in control and gives a feeling of security, working on something they understand instead of dealing with what they will most likely consider a big spaghetti with meatballs. Big rewrites from the ground up are however always a bad idea. You will lose a lot of business logic and knowledge while doing so. This is not necessary, things can be left untouched, and considered not perfect, but not bad either if they match the following criteria (taken from this article):

  • Does the code do what it is supposed to do?
  • Is it correct, usable and efficient?
  • Can it handle errors and bad data without crashing — or at least fail safely?
  • Is it easy to debug? Is it easy and safe to change?

The last one is probably the hardest one and the least likely. In those cases, a developer can isolate parts of the code and make them abstract, then write a test to make sure it works like expected. If any changes are needed from then on, the tests allow rewriting that specific part only, making it easier to debug and change code.

When starting from scratch, extra care is needed. Of course, any new project (or refactor of an existing part of a product) should be written properly: clean and readable code that follows some coding standard. The danger here is premature optimisation. Think about the current goal, not things like caching or overly complex database structures, avoid expensive technology or caring too much about performance. The less complex the code, the easier it is for new developers to get started. This is important in early-stage startups, but also when working for clients; someone might need to take over the code one day.

Andreas, explaining the principles of Atomic Design to our team
This is me, explaining the principles of Atomic Design to our team at our last company retreat.

Bringing this to practice

Every week I talk with people with great ideas, but a tiny budget to execute them. When they ask me what it would cost to build their idea, I answer between 10k and a couple of billion, basically bouncing the question back and asking what they want to spend on it. Depending on their answer, I try to explain to them clearly what they can expect: a proof-of-concept, an MVP or a product with long-term code.

As programmers, we should try to be less perfectionistic and keep that goal in mind. Delivering value is more important than the cleanliness of our code. Only when you go for the long-term it makes sense to go for perfection.

As a CEO you should ask yourself whether your budget is right for the stage of your product and keep in mind the limitations and opportunities that give. Sometimes refactoring is needed.

I believe that whenever we start a new project, internally or for a client, we need to question how perfect the code should be. So we can deliver according to the short and long-term expectations.

People make fun of my usage of the word just. Way too often I say “just do it like this”. People then go on explaining to me that it is not that simple, that I forgot about a thousand edge cases. Maybe I do this on purpose, just thinking about the happy path, ignoring anything that can go wrong. In the context of this article, I decided to highlight the just because it is highly related to the issues discussed in the article. Sometimes you have to code only for the happy path.

Reading tip: I’m starting a greenfield project and I’m terrified

More guides on coding principles and good practices