Nguyen Le PhongNguyen Le Phong

Dealing with Legacy Code

A reflective essay on working with legacy code patiently: understanding history, adding tests, making small safe changes, respecting past constraints, and improving the codebase without rushing into a rewrite.

The first thing I noticed was not the code. It was the file name. It had three words joined together, one abbreviation nobody used anymore, and a date that looked like it came from a release many people had forgotten. The function inside was long, the comments were uneven, and a small condition near the middle decided something important about billing. My first reaction was the familiar one: who wrote this?

That question is understandable, but it is rarely useful for long. Legacy code often looks strange because it is carrying history we were not present for. A rushed launch. A customer exception. A database limitation. A team that had three engineers, then one. A deadline that mattered more than elegance. A production bug fixed at midnight. The code may be messy, but mess is not always carelessness. Sometimes it is a fossil record of constraints.

Dealing with legacy code begins with humility. Not the kind that prevents us from improving anything, but the kind that slows down our judgment until we understand why the shape exists. Before deleting a branch, it helps to ask what old state might still use it. Before renaming a field, it helps to check which report, export, or integration depends on it. Before calling something bad design, it helps to learn what the team knew at the time.

This does not mean legacy code should be protected forever. Some code is genuinely costly. It slows changes, hides bugs, scares new teammates, and makes every release feel heavier. Respecting history does not mean accepting permanent pain. It means improving the system with enough care that we do not create a new problem while trying to escape the old one.

The safest improvements are often boring. Add a characterization test before changing behavior. Write down what the function currently does, even if that behavior looks odd. Extract one small pure calculation. Rename one variable after understanding it. Move one duplicated rule closer to the place where it belongs. Add logging around a risky branch. These changes do not feel like a grand refactor, but they create footholds. A codebase becomes easier to work with through many small moments of reduced uncertainty.

Tests are especially important because legacy code often lacks shared memory. The person who knew the edge case may have left. The ticket that explained it may be buried. The customer contract may live in someone's old email thread. A test is not only a technical guardrail. It is a small act of remembering. It says, this behavior matters enough that the next person should not have to rediscover it through production pain.

The temptation to rewrite is strong. Sometimes a rewrite is correct, especially when the old system blocks essential change and the team can build a migration path with real discipline. But rewrites are expensive because they ask the team to recreate both the obvious behavior and the hidden behavior. The hidden behavior is where many rewrites fail. The old code may be ugly, but it has survived contact with real users. That survival contains information.

I have learned to prefer a question before a rewrite: can we make the next change safer without replacing everything? Maybe the answer is adding an adapter around the legacy module. Maybe it is moving new behavior into a clearer boundary while old behavior remains stable. Maybe it is creating a strangler path, where the new system takes over slice by slice. The goal is not to worship incrementalism. The goal is to preserve learning while reducing risk.

Legacy code also affects people emotionally. It can make a capable engineer feel slow. It can make a new teammate feel like they are not smart enough. It can turn code review into archaeology. A healthy team names that cost without blaming the people who came before. We can say, this area is hard to understand, and also say, people probably did the best they could with the constraints they had.

There is a quiet confidence that comes from making peace with legacy code. Not passive acceptance, but patient contact. You read more than you write. You trace the path. You ask one older teammate. You add one test. You make one safe improvement. Over time, the codebase becomes less like a locked room and more like an old building with a floor plan you are slowly redrawing.

Maybe the real skill is learning to improve without contempt. Every system we write today can become legacy for someone else. If we want future engineers to treat our decisions with context, we can practice that generosity now. If you have inherited a difficult codebase, I would be interested in the small change that first made it feel a little less impossible.

이 글 어떠셨나요?