Introduction
My recent software engineering experience has been on building backend web APIs and upgrading legacy software systems. Working on legacy projects exposed me to different sets of experiences and lessons from code perspectives, gathering and refining requirements, to broader architectural patterns. In this article I will share my lessons and experiences on patterns, principles and practices from code and architecture perspectives while working on legacy software systems.
What makes a software project legacy?
As developer, we know software system doesn’t come out perfect on the first try, and even if it does, it's continuously evolving due to change requests and design problems. This proofs that software systems can’t be built once due to the law of increasing complexity and continuing changes stated by Manny Lehma and Les Belady years back. Even though the software might be imperfect, the code still works as expected and hence the software is not broken from a functional point of view. When design problems are not fixed earlier, the software becomes difficult to adapt to changing requirements. The software grows into a legacy system when left untouched due to the cost to fix it.
A legacy software is a valuable application that was once built to solve business problems and without continuous refactoring, it becomes obsolete due to outdated programming languages, development methods, frequent changes in development tools, etc. With legacy software, the original application that was designed and deployed suffers 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, and they still serve the purpose. Because of this
they can’t be thrown away completely hence they should be reengineered to reduce their complexity and improve their maintainability.
Design and architectural patterns for rewriting and refactoring.
The legacy software projects I have worked with are mostly backend applications written in PHP. They have common things like dependency injection, feature folders, controllers, etc. Such projects were written years ago according to the best practices then, which are currently obsolete. As the project becomes legacy certain symptoms begin to show up. Below are some common symptoms in I have encountered:
- No or missing tests
- Obsolete or no documentation
- Hidden architecture
- Code smells
These symptoms become obvious as you try to understand how the system works through reverse engineering. Reverse engineering comes with many activities such reading existing documentation and source code, interviewing domain experts, users and developers, and many more. Through these activities, I was able to refine my model of how the software should work thereby making it easier to refactor.
Adopting strangler pattern
A design pattern that allows gradually moving the system to the desired state was adopted since we couldn't afford to build everything immediately. This where the strangler pattern is the well-known for. It 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.
My rewriting happens to be on a new PHP framework. Symfony framework was the most recommended tool to be used to port the application.
With this approach, I started rewriting the front controller to use Symfony Request to handle the application’s request. You can find out more how you can achieve this here.
Handling existing features and change requests.
As I gradually port the existing features or implement new change requirements to the new infrastructure, there were key decisions to be made which include the most effective way to model the new architecture to be independent of the supporting infrastructures such as the framework, third party integrations, etc. This makes it easier for code maintainability and extensibility. This is where domain-driven design(DDD) comes into place as an approach to software development. Most of the recent engineering teams I have worked with have adopted DDD very well by practicing activities like event-storming to understand the business domain and principles to develop a system that is decoupled from the interfacing frameworks.
Better understanding requirements with event-storming
Understanding existing legacy application's features is the first stage in every project migration, refactoring or rewriting. A key activity we mostly run is event storming session to model the project’s domain and through which we are able to identify important events in the system, commands, entities and the more likely the software implementation will reflect the domain, which is the primary purpose of DDD.
Improving 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 code. Since building a decoupled system is the main goal, we adopt hexagonal architecture which helps to model the design of the software application around the domain which was explored through event-storming activity. In other words, it is a way to defend your business logic which forms your domain model against infrastructure with layers, ports and adapters. Also, the domain models are mostly known to evolve and hence write and read operations in models should be decoupled to protect them from inconsistent data. But most legacy systems have both operations in one model. To improve this, the Command and Query Responsibility Segregation(CQRS) pattern is adopted to achieve the goal of creating consistent data by decoupling write and read operations in models.
Helpful Tools
Irrespective of how nice we designed the codes, we need some tools to help make sure certain standards are met and easier to manage on deployment. Below are some of the helpful tools in PHP ecosystem that I have used:
- PHPStan
- PHPcs
- Docker
Testing
One typical symptom of legacy systems is no or incomplete tests. And to begin writing tests for these existing codes which is difficult to since such codes were written without defining seams which makes testing easier. A practical way
to 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 opener. Got to explore the existing codebase: their practices, patterns and principles and how they can be reengineered to a better system with different practices, patterns and principles and tools. The major lesson from these activities is there is a good chance those problems faced on the software projects were faced, solved, documented as pattern or approach to development, and probably created tools for them, exploring to figure these out before attempting your own solution is very essential and that's always the fun part with the team.
Top comments (1)
Thanks for sharing your experience with legacy software rewriting techniques! Your insights on the challenges and lessons learned provide valuable guidance for those navigating similar projects. Considering the complexities involved, individuals exploring such endeavors may find the assistance of specialized software modernization services beneficial in ensuring a smooth transition and optimizing the overall process.