paint-brush
Transforming Legacy with Domain-Driven Design, VI: Resultsby@ayacaste

Transforming Legacy with Domain-Driven Design, VI: Results

by Anton MusatovNovember 13th, 2024
Read on Terminal Reader
tldt arrow

Too Long; Didn't Read

In this article, I will share the outcomes of adopting Domain-Driven Design in a large legacy project.
featured image - Transforming Legacy with Domain-Driven Design, VI: Results
Anton Musatov HackerNoon profile picture


In my previous articles, I explained why I initiated the adoption of Domain-Driven Design (DDD) in the project and discussed the less obvious challenges I had to resolve independently during implementation. In this article, I will share the outcomes of this effort.

Timeline

It took about a month to hold initial meetings with the team and the CTO to discuss and select the best methodology. Following this, I spent around two months diving into the theory and preparing a detailed work plan for developing the prototype. The prototype development itself took roughly four months. At this stage, the concept was fully developed and demonstrated the effectiveness of DDD methodology in our project, both theoretically and practically. However, much work remained— the entire system had not yet transitioned to the new architecture, and considerable efforts lay ahead.


During subsequent meetings with the business side, we agreed on the following approach: all new developments and departmental projects would follow the new architecture, while some of the most in-demand legacy functions generating 80% of requests would also migrate to the new platform. Less frequently used elements of the old system would remain on the previous architecture. This compromise allowed us to keep up with business project schedules while gradually forming a more maintainable and scalable architecture. Although this extended the overall implementation timeline, it was essential for a smooth transition.


In my case, it took around two years post-prototype implementation to fully adapt the department to the new architecture. For a large legacy project, this is an excellent timeframe, considering that business projects continued without interruption, and the new approach was introduced gradually.

Ubiquitous Language

A core principle of DDD is the creation of a shared language between domain experts and developers. In the early stages of prototype development, my focus was primarily on the technical aspects. With over five years of experience at the company, I was well-acquainted with the domain, allowing me to concentrate on development without extensive business input. However, it became evident in later stages that successful DDD implementation required a unified language and domain model developed collaboratively with the business team.


Our development department closely collaborated with a small team of business analysts, who translated requests from internal and external clients into actionable development tasks. I met with them to discuss the benefits of the new methodology, emphasizing that DDD’s full potential could be realized only with active involvement from analysts as domain experts. We agreed that I would hold a series of lectures to explain how the domain model was structured and which terms were used. These sessions were successful: analysts actively participated, sharing insights and perspectives on the domain. This enriched our model, helped address implementation errors, and refined the terminology.


Despite having a strong grasp of the domain, the analysts lacked a unified model for describing it and specialized tools for creating and maintaining it. They mainly used text and table-based descriptions of functionality. After the lectures, we discussed how the models were implemented in the code, and I suggested providing the analysts with read-only access to our repository. This allowed them to view models and entities in a more structured format and follow changes more efficiently. In some cases, they could independently assess the effort required for various implementations. Additionally, we used Markdown documentation and PlantUML diagrams, which made it much easier for analysts to work with the source code. Such an approach does require analysts with a higher technical skill level, which may not be feasible for every project.


The interaction between developers and analysts improved significantly. We created a shared terminology field that we successfully used and maintained. However, analysts did not become the primary drivers of domain model changes and could not independently create domain models. This was influenced by the complexity of the models and analysts' heavy workload with client requests. Developers continued to model the domain independently, proposing appropriate structures and entities. By this point, developers understood the importance of accurate models and actively collaborated with analysts to gather details for precise model formation.


This approach is sometimes called “DDD light.” While full DDD remains the ideal, in the real world, it’s often unattainable due to many factors beyond developers' control. In such cases, a “light” approach can be an effective alternative for ensuring quality interactions and model implementation.

Other Teams

My department was responsible for developing core functionality for trading procedures within a large electronic trading platform system. Our core domain was tightly integrated with other parts of the application, demanding high standards of interaction with other teams. When we started implementing DDD, there was a need not only to apply new approaches within the department but also to train other teams on how to integrate with our code correctly and understand the principles of DDD. To achieve this, I organized a series of tech talks for all company developers.


In the initial sessions, held right after the prototype’s implementation, I explained DDD’s theoretical foundations and new architectural approaches, focusing on the advantages of this method. I described in detail how other teams could now interact with our domain. We provided them with both an internal structured API at the Application Layer for command execution and integration events, allowing them to easily subscribe to and monitor changes.


After the DDD methodology was fully implemented in our department, I reviewed the results, highlighting specific issues resolved through the new approach. This allowed the teams to understand the real benefits of DDD, inspiring them to try it out themselves.

Several teams responsible for complex domain code also became interested in DDD and gradually started adopting it. At least two teams successfully transitioned to this methodology within a year following our DDD implementation. Since my department had already addressed many of the complex challenges of DDD, I provided consulting and information support, enabling other teams to adapt to the new approach much faster.


As a result, not only did our code become more structured and organized, but the code of adjacent teams improved as well. This significantly enhanced the stability and reliability of the entire system, improving interdepartmental collaboration and simplifying future project support and development.

Reducing Coupling

Implementing layers significantly reduced code coupling within our area of responsibility. Business rules were isolated at the domain layer, and the application layer offered a convenient internal API. Framework code was primarily concentrated in the client code layer, while technical support was moved to the infrastructure layer. Each code component now had a clear place and specific interaction directions.


Additionally, by designing domain models more thoughtfully, we eliminated unnecessary coupling within the domain code. For instance, in the legacy code, parts of the business rules were often reused or even inherited across different types of trading procedures. This seemed logical at first glance, but subsequent changes unique to specific procedures couldn’t be encapsulated, necessitating exceptions in the general business rule code. In the new implementation, I accounted for this shortcoming, enabling future changes in each procedure type to be made independently without affecting others.


The internal API separated the UI from logic, and later we used this same API to create an external REST API and SPA application. This same internal API and integration events facilitated the separation of other contexts’ code from ours, greatly simplifying future modifications. We could now focus on changes in specific modules without needing global system adjustments, enhancing code maintainability and flexibility.

Increasing Stability

We separated business logic from other code, simplifying unit testing for business components. We implemented in-memory repositories and developed several tools to easily mock entities. This allowed us to write fast, fully in-memory tests for any command at the application layer. These tests are slightly more verbose than classic unit tests focused on individual functions or methods, but they are closer to actual business scenarios. Each command at the application layer reflects a real business action, making it easier to write tests in terms of real user stories rather than abstract data.


As a result, the process of writing tests became clear and convenient, and I introduced a rule that all business code must be accompanied by unit tests. Developers, tired of the legacy code’s instability, responded positively to this rule, eliminating the need to explain the importance of testing. Soon we had a substantial set of tests for the domain area. This noticeably improved code stability. First, developers wrote tests for the code right from the start, enabling them to identify and fix inaccuracies during development. Second, when the QA team or clients identified issues during testing or in production, we added tests to cover these scenarios, preventing similar errors from recurring.


Unit tests, with all external services mocked, were inexpensive in terms of resources and runtime, allowing us to maintain them in large quantities. Of course, potential issues remained at other levels of the system, so we also maintained functional and integration tests for critical scenarios covering all architectural layers. However, such tests were more time-consuming and resource-intensive, so their number was limited.


Consequently, we reduced the number of bugs both during QA testing and in production with clients. This also boosted confidence in the code for both developers and management.

Development Speed

Our system, once a chaotic mix of tightly coupled, untestable code, transformed into a structure of well-isolated, small components, each with a clearly defined area of responsibility. Working with these components significantly reduced the cognitive load on developers. Instead of spending hours deciphering complex, obscure legacy component dependencies, developers could quickly locate and adjust the needed object within the appropriate layer.


Standardizing each layer further reduced the load. Each layer had its own object types, sufficient for handling or modeling almost any scenario within the domain. This allowed the team to focus solely on business logic without being distracted by reinventing architectural solutions for every new task.


Due to this standardization and component isolation, we could deliver tasks to production more quickly, greatly improving the team’s overall efficiency.

Onboarding Speed

Standardizing our architecture through DDD significantly reduced the onboarding time for new team members. Before implementing DDD, new developers typically took about six months to acclimate to our project. Much of this time was spent learning the domain itself and, more challengingly, navigating legacy code. Additionally, the team lead had to spend substantial time explaining how individual components functioned, as it was nearly impossible to document the many divergent approaches used in the code.


With the new architecture, the onboarding process became much simpler. We began asking newcomers to read Evans' book and go through a few Markdown files in the repository, which gave them a solid understanding of the system. A single, detailed meeting with the team lead then provided any additional clarification. After that, new developers could gradually learn domain details through task execution. This approach reduced onboarding time several-fold, easing the process for both the new hires and their mentors.

Team Motivation

The heavy cognitive load from legacy code, frequent bugs due to a lack of tests, and outdated architecture put considerable pressure on the development team. Task deadlines were often extended, and developers lacked clear strategies to address accumulating issues, which led to demotivation. In retrospectives and one-on-one meetings, motivation consistently emerged as a primary concern. Some team members genuinely wanted to resolve these issues but felt lost on how to proceed. Others sought professional growth but didn’t see opportunities for it within the department.


At one point, I faced an employee turnover challenge: some team members transferred to other teams, while others left the company despite initially being loyal. This impacted both employee retention and recruitment; we struggled to attract new hires with anything other than routine work and a paycheck. Candidates often opted for other companies where they saw more appealing development opportunities.


Introducing DDD positively impacted the situation. Developers felt heard, and they saw tangible change beginning to happen. The new architecture was not just an abstract concept—I explained at the outset precisely which issues it would address and how. We were able to agree with management on a gradual adoption process for the new architecture, and the team saw leadership’s support for these changes, making problem resolution a shared mission.


For those seeking professional growth, new opportunities opened up: we needed to implement numerous features within the new architecture, allowing room for development. Additionally, interest in DDD emerged in other teams, and our team members became experts, helping colleagues. This approach became popular in the market, and by implementing it, I was able to attract new developers who found DDD work genuinely interesting.


In the end, DDD not only helped solve technical issues but also restored and increased team motivation, enabling people to see real prospects for their growth.

Company Impact

One of the first major achievements following DDD (Domain-Driven Design) adoption was the launch of a new electronic trading platform in a neighboring country. Before DDD, similar projects took the team up to six months, but with the new approach, we accomplished it in just a month. This allowed us to outpace competitors and become the first platform in the country to offer this functionality, enabling the company to enter the international market and increase revenue.


After migrating the main platform to the new architecture, we significantly improved our responsiveness to market changes and demands. We actively developed platform features within the new architecture, offering unique services and high-quality customer experiences. As a result, for three consecutive years after adopting the new architecture, the company ranked first in the national industry rating of electronic trading platforms based on corporate customers' satisfaction with service quality.


The new architecture also greatly simplified integration with additional systems and services. Following our team’s lead, several other development teams within the company adopted the new methodology. This enabled us to quickly develop a new product that automated not only the bidding process itself but also ensured seamless automation of all stages—from submitting a request by the internal client to paying the supplier under a completed contract. Eventually, this product ranked among the country's top 10 SRM systems.

Summary

Implementing DDD (Domain-Driven Design) significantly impacted me personally, my team, our product, and the company as a whole. This step became a new growth milestone for the entire organization, as this methodology allowed us to not only resolve numerous accumulated problems but also scale the business successfully. DDD is a powerful tool, though it comes with compromises and limitations.


Firstly, DDD requires the team to invest significant time in studying a broad theoretical foundation. As I mentioned in previous articles, you will inevitably face unique implementation challenges that require independent solutions. Additionally, factors outside your control may prevent a full DDD implementation. This methodology requires close collaboration and active interaction across different company departments, which is not always feasible.


DDD simplifies certain system components, such as contexts, layers, entities, and classes. However, overall system complexity inevitably increases. Many simple components combine into a more extensive, complex, and structured system. Despite its complexity, it remains organized and manageable, and architectural control allows us to keep complexity under control.

However, implementing DDD may bring some technical overhead, and minor tasks may take slightly longer due to increased efforts required for code structuring. The entry threshold for new developers might also be higher, though onboarding organization and process control become easier.


Therefore, DDD should be used for truly complex domains with extensive business logic and projects planned for long-term support. In such cases, DDD is not only highly effective but often essential for the project's successful development and scalability.