Article

Working on Legacy Software: Rewriting techniques, experience and lessons

November 1st, 2023

Introduction

Here at Future500 B.V we are mostly building APIs and upgrading legacy software systems. I had a lot of lessons learned from code perspectives, gathering requirements and refining them and of the broader architectural patterns after joining the team. In this article I will share these lessons and experiences on patterns, principles and practices from both code and architecture perspectives.

What makes a software project legacy?

As software systems can’t be built in one go due to the law of increasing complexity and continuing changes stated by Manny Lehma and Les Belady years back, software will experience many typical design problems. Because it is a design problem, the code still works as expected and hence the software is not broken from a functional point of view.
When a design problem is not fixed earlier, software becomes difficult to adapt to changing requirements. That's when software begins to grow into a legacy system. However, legacy software is not inherently bad.

A legacy software is a valuable application built to solve organizations' problems and without continuous refactoring, it becomes obsolete due to outdated programming languages, development methods, frequent changes in development tools et cetera. A typical issue with legacy software is the original application that was designed and deployed has changed requirements which makes it suffer from design drift - a phenomenon that occurs when a development team continuously fails to recognize and adapt to change, causing the concepts in the software’s domain and the concepts in code to start to slowly “drift” apart from one another, creating dissonance which ultimately leads to technical debt.

Because legacy software is valuable to the businesses we work with, it still serves the purpose. Because of this it can’t be thrown away completely but should be reengineered to reduce its complexity and improve maintainability.

Rewriting/Refactoring

The legacy software projects I have encountered are mostly backend applications written in PHP which have things like dependency injection, feature folders, controllers and sometimes conventional HTML with HTML tags. And because they were written years ago according to practices common then (which have become obsolete), the following symptoms show up in such projects:

  • No or missing tests
  • Obsolete or no documentation
  • Hidden architecture
  • "Code smells"

These symptoms would be known to you when you understand how to use the software. Getting to know this is through reverse engineering. There are so many activities that go into this: reading existing documentation and source code, interviewing domain experts, users and developers and many more. Through these activities you will be able to refine your model of how the software should work which makes rewriting easier.

To rewrite software that solves those issues you need to adopt some technique to gradually transform the entire application since you can’t afford to build everything immediately. This is where design patterns come in at the technical level.

Adopting the strangler pattern

The strangler pattern is the well-known pattern that helps to incrementally migrate a legacy system by gradually replacing specific pieces of functionality with new applications and services. With this, all the old system’s features will be replaced. This pattern was proposed by Martin Fowler. Rewriting usually happens on a new framework and Symfony is the most used PHP framework here at Future500, so let’s port the application to the Symfony framework instead of just ‘raw’ PHP. With this, I start rewriting the front controller to use the Symfony Request class to handle the application’s request. You can find out more how you can achieve this here.

Handling feature requests and features

As I gradually port features or implement future change requirements to the new framework, infrastructure, et cetera, there are key decisions to be made which include the most effective way to model our architecture to be independent of supporting platforms, infrastructure or frameworks. This makes it easier for code maintainability and extensibility. This is where domain-driven design (DDD) has a place as an approach to software development. At Future500, DDD is well adopted by using its activities like event-storming to understand the business domain and principles to develop a system that is well decoupled from the interfacing frameworks.

Better understanding requirements with event-storming

Understanding legacy applications that need to be upgraded is the first stage in every project migration, refactoring or rewriting. At Future500, we run event storming sessions to model the project’s domain through which we are able to identify important events, commands and entities in the system. This makes it more likely that the software implementation will reflect the domain, which is the primary purpose of DDD.

Low level architecture (code design) with Hexagonal architecture

At the low level all these domain events, commands, entities and aggregates are objects that need to be designed in our code. To achieve a system that is truly decoupled from external factors - which is the primary purpose of our rewriting because of maintainability and extensibility reasons - we adopt hexagonal architecture which helps to model the design of the software application around the domain which was explored through event-storming. In other words, it is a way to connect the domain model to infrastructure. With domain models that usually evolve, techniques like Command and Query Responsibility Segregation (CQRS) help to decouple write and read operations in models which are mostly combined in one model in legacy projects.

Helpful Tooling

Irrespective of how nice we designed our code, we need some tools to help make sure certain standards are met and easier to manage on deployment. When rewriting projects in PHP we are mostly working with these tools:

  • PHPStan
  • PHPcs
  • Docker

Testing

One typical symptom of legacy systems is no or incomplete tests. Writing tests for an existing codebase is difficult since most code was written without defining seams which makes testing easier. A practical way we make sure we don’t break things during refactoring is to write integration tests against the production system. Then add unit tests while refactoring.

Lessons

Working on legacy projects was an eye-opener. I got to explore the existing codebase and learned how to adopt certain practices, patterns and principles while working with great tooling. And one other thing I have learned is with any projects you are working on there is a good chance that problems you're facing were solved years ago and have been documented as a pattern or an approach to development. And somebody probably also created tooling for that.

Conclusion

While working on a legacy system, adopt practices that help you understand the business domain. For us at Future500 event storming helps a lot. Maybe you can look into this if you are not already familiar with it. Leveraging design patterns makes your work easier because you are able to communicate ideas and plans for effective rewriting or refactoring. In addition to having great code design and architecture, rely heavily on helpful tooling which could boost the refactoring process.

Oliver Mensah

Software Developer

Do you have similar issues? Contact us at Future500

We can tackle your technical issues while you run your business.
Check out what we do or write us at info@future500.nl