My name is Anton Musatov. I hold a Master's degree in Computer Science and have been working as a software engineer for over 14 years. My career started as a Junior Developer at a small company and I've progressed to head a development department of 20 people across four teams, all working on a key product for a Forbes top-20 company.
During my career, I encountered a significant challenge at one company — restructuring the architecture of a legacy application with extensive business logic and a codebase over 15 years old. I successfully implemented the Domain-Driven Design (DDD) methodology into this system. While there are theoretical descriptions of this methodology in books and articles, applying such solutions to real-world applications is always challenging, especially for projects with a long history and accumulated architectural issues.
In this series of articles, I would like to share my practical experience. I will discuss the initial problems, explain why DDD was chosen, how the prototype development proceeded, what mistakes were made during the first implementation, and the technical challenges we faced and resolved. I will also describe the impact the implementation had on the development and delivery processes, the team, and the company as a whole.
These articles are not meant to replace or challenge existing books and materials on DDD but rather to highlight the practical nuances of applying the methodology to complex legacy projects. They will be useful for professionals who are already familiar with the theory or are in the process of studying it but are uncertain about the specific steps to take for implementation. In these articles, you will find solutions to complex and ambiguous situations that inevitably arise in such implementations, and you will be able to assess the impact of DDD on a real-world project, helping you determine its necessity for your applications.
At that time, I was the team lead of a small group of six developers within the company. The company was growing rapidly, with increasing task volumes, new hires, and department reorganizations. I had proven myself and was deeply involved in key areas of the company's product, which led to the offer to form and head a development department focused solely on the company’s core product. Eventually, I took charge of a department consisting of two teams and 10 people.
This new role brought more responsibility and expanded my perspective on the product and its issues. The company aimed for continued steady growth, but in development, we had reached certain limits. My first step was to identify bottlenecks and the most critical problems.
The codebase at the time was 15 years old, which wasn’t a problem in itself. The complexity lay in the fact that part of the code was written in a procedural style, some with object-oriented programming, some using a framework, and other parts with their own approaches that didn’t align with the overall architecture. This led to a lack of coding standards and, consequently, documentation, making it difficult for developers to switch between different parts of the application and slowing down the onboarding of new team members.
Another issue was the inherent complexity of the product, driven by the specific nature of the domain, with a vast amount of business logic embedded in the code. Architectural approaches that could have managed and reduced this complexity weren’t utilized. The combination of complex logic, a lack of standards, and insufficient documentation slowed down the development process and increased the cognitive load on developers, who had to juggle large amounts of business logic in their heads.
The project also suffered from high coupling between various components. The presentation layer wasn’t fully separated from the logic, and business logic often leaked into templates, while the UI was built randomly within business entities. The code for the core product was tightly intertwined with other system components, with some of the logic from external teams falling under our department’s responsibility.
The absence of architectural standards also made it impossible or impractical to write unit tests, which negatively impacted the product's stability and increased the number of bugs since most testing was done manually. Attempts to write tests faced challenges in mocking components and returning templates instead of expected values or objects.
At the same time, the company was actively diversifying its business and developing additional products. Due to the issues mentioned above, integrating new products with the core product was complex and time-consuming. This often exacerbated the coupling issues, as developers had to move business logic into the core product.
The high cognitive load, stress from missed deadlines, the number of bugs, and the lack of professional growth opportunities severely affected developer motivation. Several team members left the department, either transferring to other teams or leaving the company altogether.
At that time, we were already using one of the popular frameworks in the project. My initial idea was to allocate more time to reduce technical debt and focus on refactoring in line with the framework’s approaches. The problems described above were evident to managers and leadership as well, so they approved time for addressing technical debt, and we refactored some of the most problematic parts of the application.
This approach solved some of the issues: we reduced coupling and started writing more unit tests. The framework’s event-driven model also simplified integration with other teams' code. However, the framework didn’t significantly reduce complexity or cognitive load or address the architectural disorganization. It was a classic MVC framework, which couldn’t fully encapsulate the natural complexity of our domain. As a result, developers continued creating various classes like Helper, Accessor, and Validator, with each complex task introducing something new. Event-based integration wasn’t sufficient for other teams’ components; an internal API was needed, but the framework couldn’t provide one.
It became clear to me that a different approach was required. I organized several brainstorming sessions with the CTO and senior developers. The goal of the first session was to formulate requirements for the new approach, resulting in the following high-level list:
I then took a break to study suitable methodologies. In subsequent meetings, we discussed potential options and chose DDD, which met all the criteria.
At that point, DDD was a new and unfamiliar approach for both me and the team, so I proposed conducting a research phase before implementation. I studied books by Eric Evans and Vaughn Vernon about DDD, as well as numerous supplementary materials. The knowledge I gained allowed me to create a detailed plan for implementing the methodology in the project. I proposed starting by implementing a prototype on a single piece of business logic to assess the results. I set timelines and success metrics, presented and defined the plan to the CTO, and began the implementation.
While the theoretical information was helpful, many challenges arose even during the prototype phase, which I will detail in the following articles.