unsplash-logoMax Kukurudziak

If you are reading this, chances are you have worked with or likely even been writing legacy code. This is a big issue in the industry, it causes people to be unhappy, unproductive and to switch jobs. It doesn’t need to be this way. I believe we can do something about it.

But, what do I mean by saying we write legacy code? Well, what is legacy code? A quick look at legacy code on Wikipedia will tell you that it is software that is not maintained any more. In modern language, it can also refer to code passed on to a new owner. I prefer the even stricter definitions, such as:

To me, legacy code is simply code without tests.

Michael C. Feathers, Working Effectively with Legacy code

In other words, I’ve been writing legacy code. It isn’t limited to code without tests though. I say it is any code that someone three months down the line calls legacy code. Be it because of missing tests, brittleness, performance issues or something else.

Our legacy

When I joined the video hearings team there was already a product in place. It was a proof of concept created by a third party to help decide if this type of product was viable.

Being a PoC, the focus was on showcasing the main features. Meaning, it was not built to be maintainable, scalable or durable. Frankly, none of the positive traits of good code was there. Which is fair, it’s a PoC.

Working the legacy code

The first few months of my contract I was working with the team to make this PoC usable for pilots. I tried my best to apply good patterns and do code reviews to try to move the quality in the right direction.

If we’re going to throw it away anyway, what’s the point?

A colleague was frank enough to point this out when catching up before I left. His view was that I had been spending energy on trying to improve something that was not going to live on anyway.

We knew the codebase was due to retire. But we didn’t know how or when. Being completely new to the team, I didn’t feel the decision was in my hands either.

This meant it was hard to balance the level of effort to put into quality. Do we make it work or to last?

As my colleague pointed out, in retrospect, I was spending energy on the wrong thing. I was asking to build maintainable code into a broken and doomed codebase. Frustrating not only me reviewing, but the team members as well.

I’m a strong believer that entropy is inevitable in any software project. The only thing we can do is to slow it down by trying our best at writing quality code. But we need to focus our efforts in the right place as well.

In an old code base like this, trying to make it maintainable at class level was fruitless. What would help, and did, was to focus efforts on logging. That enabled us to faster dig into and fix issues, as they occurred.

Try again, rewrite

So the legacy code was not fit for purpose. What to do?

Let's rewrite all the code!
Let’s rewrite all the code!

To rewrite a product 1:1 should be faster than it was to write it the first time. Theoretically speaking that is. Features get added, the scope is bigger than presumed and we seldom do things the same way. Meaning, we are not rewriting it, we are executing the same amount of work once more in a different way. So software rewrites often fail.

Our first attempt was to “decouple” the old service so that we could reuse pieces of it. It was a painful experience. We ended up moving old broken concepts into new shiny containers.

All was not in vain though. After doing this once and realising the shortcomings, we learned.

And then we rewrote again.

Yep, you read that right. We rewrote it again. New infrastructure, new applications. And we standardised. We had one common CI/CD workflow and one common project structure. Test runs and quality control was in place everywhere.

And they coded happily ever after..

Not quite. Already two months down the line we were seeing files of over 600 LOC and frequent bug reports.

Why does legacy code happen?

You can blame skill or laziness but I don’t think that’s all. Even when I know the language and framework I can easily end up writing bad code. Sure, laziness is a part of this but not all.

I think there’s a number of things at play:

  • Our employer sees the output of our code, not how maintainable it is. So in a way, we’re not paid to write maintainable code.
  • Building bad code is like glueing parts into a machine. It is easy and looks nice when you do it. But you will curse and cry when you need to replace it.
  • Whatever you build, placing the first bits is easy. It’s only when your construction reaches a critical mass you begin to see the foundation crumbling.

So in a way, I think that much of how we develop software is counterproductive to writing maintainable software.

So how do we avoid legacy code?

Are we doomed forever to walk the plains of legacy code? I don’t think so. I think there are a number of things we can do. Below is what I think worked for us in the MoJ:

Smaller PRs

Keep batches small, do pull requests below 200 LOC. I will cover this in detail later but this helped a revolutionary shift in quality. In summary, having smaller work batches made it much easier to review and improve.

Take the time and learn to say no

Some times you can get undercooked meals at restaurants. It isn’t acceptable so we ask for a refund. But somehow, when it comes to software, our standards are different.

We as professionals need to take responsibility for the quality of our work. At the MoJ, when I heard the timeline for the next release I shook my head.

This is easy to say but even when I’m writing this something in the back of my head is thinking “it’s not impossible but if we want to do it right..”. But aren’t we always expected to do it “right”?

I raised my concern and it turns out, like so many times before, that most of the “must-haves” are actually wishes. If there isn’t time, well, we can’t do the impossible. And stakeholders are (usually) fine with that. What else can they do?

We need to say no and protect the time we need to do our job. As refactoring and testing is a part of this, well, it is non-negotiable.

What is negotiable are the pieces of work we take in. What can we cut out of the epics to do only what we have time to do “right”?

Later means never

The MoJ team was no different from any teams I have been in before. When we postponed refactoring or solving tech debt it was never addressed later.

Past few years I have raised that “later means never” and the response is always the same “yeah, but..”. Curiously, there is never a rational explanation after the “but” 😊

This comes not only from management though but from within development teams as well. I’m equally guilty.

What did work in the end was to stop negotiating time to do refactoring. As a tech lead, I asked my members to take the time for this, and for testing. Being pragmatic of course, some times we could live with the debt we created.

Sometimes refactoring or testing needed more time than what we had planned. In these cases, we treated it like any other task which scope was bigger than expected. If it is a priority, it is a priority until done. Refactoring and testing included.

Learn how to refactor

Refactoring isn’t easy. I spent hours refactoring a solution only to found that what I wanted to achieve simply wasn’t good. For example, I over-engineered a solution for navigating between pages in one application only to rip it all out and simplify it with a bit of duplication later on.

It’s fine! Refactoring isn’t obvious, easy or ever done in one perfect way. But we need to try.

Read books like Refactoring by Martin Fowler or check out https://refactoring.guru/. Talk about it. Ask your colleagues if your design makes sense and make sure to act on the feedback.

Refactoring is an excellent time to share your knowledge. You might have experience in applying a certain pattern to solve an issue. In which case, pair with them and show how to do it.

Closing words

To avoid legacy code we need to handle tech debt and refactor. We need to do it as we develop, not as an extra-curricular activity. This requires a test suite so we can’t skip that either.

Work in small batches and ensure quality as a part of each iteration. It is easier to grow habits from small steps and a habit of quality will lead to; quality!

Thank you for reading and I hope some of these tips might help you. And thanks to the team for all the effort and patience! If you read this and you have other methods to keep the tech debt at bay, please share!

This post is a part of the MoJ Lessons series. Next up I will cover the magic of the 200 LOC PR.