We’ve all been there at some point. We joined a new company or a new engineering group, and none of this “legacy code” makes any sense. “Why is all this designed that way?” “What did the people that architected this thought to themselves?” “I’m pretty sure it doesn’t actually do what they think it does…”
When you are a junior engineer you usually associate these thoughts and feelings with your personal inadequacies. You just assume there’s more to learn than you’ve yet to realize, and therefore you humbly accept that these assumptions you are making are due to new things you need to learn, new coding styles, design practices, ideas.
However interestingly enough, the more experienced we get, either as engineers, tech leaders or managers, we gradually lose this unassuming point of view and replace it with something different. We replace it with an increasing sense of judgment, and the desire to re-birth things in our image. We want to just re-write it all.
Who can blame us? Joining a new company or a group as a senior engineering figure, especially as a manager, is an overwhelming experience at times. We have so much on our plate, so much history and ground to cover, our predecessors left us enormous amounts of debt we need to deal with. All that while we are expected to bring our own agenda to the table. We need to prove our ability to make big changes, do great things. That’s whats expected of a us after all, no?
Well, yes. But also… No.

A dough rounder from Joel On Software
In a blog post from 2005, Joel Spolsky the co-founder of Stack Overflow and Trello, tells the story of his first job back in 1983, working in bread factory in Israel. In his story he writes how the first thing that grabbed him as he walked in on his first day, was how messy and dirty everything looked.
“The first time I walked into the bakery I couldn’t believe what a mess it was. The sides of the ovens were yellowing, machines were rusting, there was grease everywhere.”
However to his surprise, when he approached the factory manager to ask why things are the way they are, the answer he got was that they just finished cleaning. In fact, he said, it was the cleanest the factory has been in weeks.
It took him months of trying to clean the unclean-able. Removing grease where grease is actually important for the machines to function. He had to relearn what clean meant in a bread factory.
“To an outsider, it was impossible to walk in and judge whether the place was clean or not. An outsider would never think of looking at the inside surfaces of the dough rounder to see if they had been scraped clean. An outsider would obsess over the fact that the old oven had discolored panels, because those panels were huge. But a baker couldn’t care less whether the paint on the outside of their oven was starting to turn a little yellow. The bread still tasted just as good.”
This is all an elaborate allegory to code of course. In his post, Joel continues to talk about the idea of separating coding styles from functionality when judging code, and how to write new code in ways that makes “bad code” stand out. But this lesson is also true on the higher level. The design of code, the architecture of a system, the functionality of a product.
See, we are all biased towards identifying unknown code as bad code, it’s just how our brains are wired. We create in our head complex models for the things that we build. That’s what helps us navigate intricate designs, troubleshoot and problem-solve. The more we are involved in the creation of something, a class, a service, a system, the better is that model in our head.
When we meet new software that we’ve never seen before, especially one that is complex and has a lot of history, our instinct is to try and map it to one of the existing models that already reside in our head. But that doesn’t always work. Usually new things that we encounter will land completely detached from anything we’ve already experienced. Even worse, sometimes they will match one of our models only partially, hitting the horrible “uncanny valley”, making us actively hate any discrepancy we find from the parts we expect to be there.
The more experienced we are, our instincts will drive us harder to “fix” these gaps and align new software that we meet closer to a model that is already in our head. We will instinctively resist building a new model for this code we didn’t write and might pursue creating a model by rewriting things completely.
More often than not, we will rationalize this to ourselves by calling everything that came before us “legacy”. We will criticise the current state and all those that participated in it’s creation, and assume we can do much better. Even though that might not always be the case.
But worst of all, sometimes we actually will be right.

Years ago I joined as a team lead to a young engineering group that was just recently acquired by a large cybersecurity company. As with many small start-ups post acquisition, it was chaos. The team was small and was in the early stages of it’s engineering maturity journey. The architecture was a patchwork of various random technologies. The design and code were incredibly complex in comparison to the purpose I assumed they were built for. Nothing was really being tested and we barely had more than a couple of customers actually using the product.
But it was fine, I guessed. This was the first time I’ve worked on this type of product. Many of the technologies were new to me. I was only starting to learn Scala, so I assumed what I saw were idiosyncrasies unique to the language, or design patterns I’ve yet to learn.
Then came our first big customer, and everything started breaking down.
The next months I spent my days interviewing new hires, teaching our engineers git methodologies, testing guidelines and CI/CD practices. While my nights, if not handling another critical production incident, were devoted to studying our codebase and patching broken functionality.
I would read and re-read code, write tests and debug. I added comments, logs, metrics instrumentation, anything that helped me understand what this code is doing and why.
I was building the model in my head, piece by piece. I shared what I discovered with others, both those that were the original creators of it and newcomers as well. I was analyzing the “bread factory” and finding out what was grease and what were rotting pieces of dough that were spoiling the loaves.
It was close to the one year mark, when I dared to bring up the idea of re-architecture. But things were much different at that point. I’ve hired new people with the skills that I needed. I helped them all build this very flawed model in their heads and understand why it was the way it was, even if at times it didn’t make much sense. We patched what we could, so we could focus on our strategy and not only on firefighting. Most importantly, we’ve set up a gradual path for refactoring and prioritized what needs to be dealt with first.
It’s been several years since that decision have been made, and it resulted with success for the most part. The team grew, more and more customers arrived, and the product stood the test of scale and time.
I personally no longer work there, but I know that the re-achitecture still goes on, and there is not a doubt in my mind that “new code” that we wrote have since already been refactored as well.

So which is it? Should we re-write everything or not?
I’m afraid it’s not that simple of a question, and reality is more nuanced than that.
There’s a truth we all tend to forget. New or old, code doesn’t age like fine wine, code ages like milk. However we can never afford to replace something that we have yet to understand by ourselves, no matter how experienced or senior we are.
The question of whether we should rewrite things or not is missing the point. It’s never a question of “if” but a question of “when”, “why” and “how”.
Code written today might become less relevant tomorrow, but that doesn’t mean we need to rewrite it every Monday. The decision to make changes is always relevant, but it can only be done when you understand the ecosystem well enough and why things were built they way they did. It can only be done when you have the right skills at your disposal, when the timing is right, and when you truly know how to plan and execute it.
As senior engineering figures we are all expected to make great impact in our roles, and those expectations grow the more experienced we get. But it’s important for us to remember that impact is not measured by change. Impact is measured by value created. If you are not bringing value, you are not actually making an impact.
“Legacy code” might hold many flaws or assumptions that are no longer relevant, but it also holds value that was built over time. Until we have a good understanding of the value it brings, we have little chance to actually succeed with changing it without losing that value it was built for in the first place. Only after we’ve built that model in our head, that “bread factory”, we will be able to analyze it, plan our next steps, and make changes that drive impact and create true value.
First published on Medium, 7 February 2022.