- Published on
The complex art of writing simple code for a business
Writing code as a job is more complex than just writing the code. The moment you get paid, two opposite forces start pulling at you:
-
On one side, you need to build something that works, and it needs to be delivered for yesterday. Wait, what do you mean you haven’t started yet? Get on to work!
-
On the other side, the code needs to be clean and flexible enough to evolve when a new task requires it—probably also for yesterday.
To be a good software engineer you need to strike a balance. But I know you. You somehow reached this random blog—supposedly about formal languages, grammars and related topics. And because of that I know this idea makes you shiver. Your nature makes you want to implement the perfect solution. To solve every corner case and future possibility. To write abstract and clever code.
I also used to think like that. I loved refactoring my code a million times in my personal projects. And this behaviour was heavilly encouraged in my first job. But I could feel something wasn’t right. Code took too much time to reach production. The solutions were clean but felt slightly useless and not even that robust. By the time I finished them I wasn’t sure if any user was actually going to care.
Everything changed when I switched jobs and was “forced” to write dumb simple code for a while. The culture was different, the focus was on having impact. Clean enough, but without wasting time. Only then I started to understand that writing simple code is actually complex, and why putting effort on keeping it simple is worth it.
Why we write code — The cost of forgetting impact
I will always remember my first job. Not only because it was the first, but also for it's unique engineering focus. I doubt I'll ever find something similar.
I don't want to enter into the details of what we did, but the business revolved around having a faster product than the rest. And hence we used a lot of self-built libraries. Everything had to be optimised. Everything was “over-engineered”. I put it in quotes because this is precisely what gave us a competitive advantage. That architecture allowed us to be faster and cheaper than the competition.
But there were two problems:
- Over-engineering was so rooted into the company it impacted every decision, even minor ones that didn't require it. This meant adding or changing features was difficult. Everything had to fit in an existing overcomplicated architecture and cover every possible edge case. Everything took an eternity to develop. This wouldn’t be a problem if it was made for a reason. There were times quality was required and that quality takes time. But if you combine it with the fact that….
- It felt useless. You could spend months developing and deploying your feature, and it wouldn't have an impact. This was a mixture of multiple chaotic factors:
- We had multiple clients with different requirements pulling in different directions. Product Managers didn’t prioritize correctly. Clients were short-tempered and complained easily, making PMs extra wary of any change.
- We didn’t know which features the users used and which not. We kept mantaining broken code and were afraid of cleaning it up. Too much technical debt piled up over 10 years.
With this post I want to talk about how my vision of coding and software engineering has changed based on my experiences on my previous and current company. I acknowledge it is not fair to compare them in other aspects like complexity.
Even if our users actually used the features we implemented, there was something certain: We spent too many hours taking care of edge cases that no one would ever care about. Just because of either engineering pride, or PM stubbornness. Perfection clouded our judgement and we forgot that the goal is to bring value, not to tick all items of a to do list.
We should cover that edge case that only happens one in a gazillion times, it's only visual and has an extremely easy workaround. And we should do it in a way that is generalized as to treat it as any of the other cases, even if it means making the code that handles normal (and impactful edge cases) a million times more complex.
-- Exageration of what someone could have said at my previous company at some point–or maybe even past-me. I wouldn't be surprised.
To my junior self it made complete sense: We were a company full of engineers—not just software engineers, but also mathematicians and physicists—people trained to think in terms of formal proofs, validations, and rigorous verifications. If you're writing a parser for Java, you don't suddenly decide you don't want to write the rules for parsing interfaces. It's all or nothing, you can't leave anything out.
But in a business that is not true. There are things client must have, things they want, things that would be nice, and things they don't care—that sounded too much like a productivity guru. What am I becoming?
All this led to an inevitable conclusion: Ever-growing technical debt, employee burnout and slow growth for the company.
Programming, coding and pressing keys
If I had to sum up the important points mentioned till know is that in my previous company:
- The code was complex (a significant amount of times, unnecessarily)
- We didn't feel we could have much impact and we didn't have a choice in the impact we could have.
So, in my position all the complexity was solely on coding. Every day I was faced with an existing jigsaw puzzle, and I had to add or improve a piece. The only problem is that the product was designed following String Theory so the puzzle had 26 dimensions and you needed to ensure the piece didn't break in any of them.
I was a coder. My job was to make the code clean, clever and witty. You could say I was more in charge of the lines of code than the final product. Which is why I was stunned when I changed company and was faced with a completely different situation.
The complexity outside of code
In my second company our team was responsible of building a new system. That by itself made things much more simple: No need to mantain legacy code, not inherited culture–to a certain degree, since we followed the stack that other teams did–and the opportunity to make our own decisions. For once, programming didin't feel like a puzzle. And that was great, but... potentially boring? Where was the challenge?
During my first months I was afraid that once I got used to the new ways of working, every task would feel the same. I would become a cog in the machine, doing empty task after empty task. After all, our system started as a vitaminated CRUD with some (luckily) complex data validation and processing.
Little did I know that I would end up discovering a whole world of software engineering. A world centered not only on coding, but bringing value, aligning with stakeholders, and building a long-term, simple and mantainable product. In this post I'm going to stay away from the topic of communication and design/architecture to keep the focus on the complexity of code. Because as it turns out, keeping things simple takes more effort than expected.
The fight for simplicity
During my onboarding in the team, I saw there was a huge enphasis on simplicity. But-unlike I initially thought-that doesn't make things less interesing, quite the opposite.
Keeping complexity down requires concious effort. Complexity is like entropy, it increases naturally as systems grow. And it compounds easily when systems interconnect, after all, little drops of water make the ocean. If the small parts of the code that actually can be simple aren't, then the system as a whole will explode in complexity. To make it mantainable it needs to be ASAP (As Simple As Possible), ACAP (As Consistent as Possible) and AETRAP (As Easy to Read As Possible)—Yes I know, I have stretched the "joke" too much.
All this requires a deeper understanding of the problem. Analysing different potential solutions and their advantages and drawbacks. Looking at how each affect the code complexity. Making trade-offs between doing things the right way and being able to deliver on time.
As someone that was used to the rigorous and exhaustive ways of working of my first company, I was surprised to learn that sometimes this means choosing NOT to cover an edge case if it's not going to happen often enough. That would have been unthinkable. But it makes sense when you remember that your job is to bring value, not to be perfect.
In this company, the complexity had moved. It wasn't in the code, but in keeping code complexity down, making trade offs and defending them, understanding how our product fits in the business and prioritizing correctly.
And it's challenging
This "simpler" way of programming is a challenge by itself. Just a different type of challenge than what we are used to. It's making code that everyone can understand without effort. That won't raise too many comments in the PR, and most importantly, that you'll be able to re-learn quickly when you—inevitably—forget about the code 6 months later.
On one hand it requires the usual clean-code/architecture guidelines: Define the flow, split it in components with single responsabilities, etc. But it also requires a more humane aspect: To get out your own mind and see how others will see your code or designs. It can be as simple things as:
- Having to think who is the target of a document you are writing to adjust the details and language you use.
- Not focusing only in the end solution, but how the team will arrive there. What should be done first, in which order. What can be left out.
- I was surprised at how almost every big iniciative in our team is always split into multiple phases. That has been really helpful across time.
- Prioritising consistency over style/naming preferences. This really helps because it makes the code more predictable, making it easier to read and traverse.
- Being able to defend a solution and communicate in a clear way, focusing on what the main concerns are without digressing—and of course, accepting defeat if your solution is not chosen.
Of course there are ugly parts, mainly in form of endless meetings and nitpicks. I will never take back the 20 minutes of my life I spent listening to my team discuss over the color of a button.
But the truth is, even after a year and a half working here, the system is quite consistent and things get shipped surpisingly fast in production. The system has become a huge monster of code and we still need to improve the way we handle technical debt, but it's still in a much better state and much more understandable that I was expecting it to be.
Final Thoughts
If I had to sum up this extremely long post with ChatGPT, I'm pretty sure it would say: "WTF are you making me do and when did you start writing essays?"
After a long discussion where I convince it to go through the pain of reading this post, it would say that "Complexity lies in many areas of software engineering: code, business logic, architecture, communication within the team, teams and stakeholders, etc. And sometimes the best way of keeping it down is to decide not to do something, if it is not going to have an impact. Even if that goes against your engineer nature."
(Clarification for nitpicky people: that small "summary" was actually not written with ChatGPT, I was just joking. Although that is probably something that ChatGPT would say.....)
This was a really odd post compared to everything I have written so far. I'm not even sure if someone will bother to read half of it. But I have been reflecting on this topic for a while. In fact I wanted to go deeper on writing clean code but the post was getting too big. Maybe I'll do a follow up in the future.
Also I hope I get some time to start a new season of this blog, maybe focused on building state machines 👀. But no promises.