In the article Demystifying the DDD I explained the motivations involved in applying DDD in a project and the favorable scenarios for that. In this article, I want to summarize what Eric Evans encourages as practices for understanding, distilling and implementing the domain, using a predetermined case as an example.
DDD is a development philosophy focused on complex business domains. It involves a set of practices for understanding the business, communication, collaboration, and developing an emerging solution that reflects mastery in the code itself.
What practices are these? How to apply them?
Model Driven Design
A software project usually arises out of a need for an individual or company. The practice recommended by Eric Evans, the creator of DDD, to understand this need to achieve the solution is Model Driven Design, or Model Driven Design, which is the modeling of the domain based on the constant interaction between the technical team and the business specialists, through ubiquitous language.
Meaning of the term “ubiquitous”:
- Which is or can be everywhere at the same time; omnipresent.
- That has spread a lot; universal.
In these interactions, it is common to identify the most relevant words (nouns), and to think about classes and their relationships. Model Driven Design goes further, identifies which are the most relevant actions (verbs), showing how these relevant terms behave within the domain. It is not just about what information will be stored and how it relates. It is what the system solves, what actions it takes. Hence the importance of a heterogeneous team being involved in the analysis, and not just an analyst, or DBA, or developer.
To explain a little more about the application of DDD, we will use a fictitious case: Imagine that we are part of a consultancy, and one of our customers requested a software that manages the production of equipment on demand from a Sales Order.
Below are the specifications sent by the customer:
- The Sales Order may contain several Products, composed of several Components;
- When a Sales Order is issued, it is necessary to check the availability of the Product Components. The available Components are booked, and Purchase Orders are issued for those that are unavailable;
- There is a legacy system that manages the entry and exit of Components in the Inventory, which can be integrated to notify the arrival of the requested Components;
- When all Components are available, the Service Order is issued. After completion of production, other processes can be initiated, such as issuing an invoice.
In a first step, we can highlight the nouns that seem relevant to the business:
- O Sales order may contain several Products, composed of several Components;
- When one Sales order is issued, it is necessary to check the availability of Components of Product. Reservation of Components available, and issuing Purchase requests for the unavailable;
- There is a legacy system that manages the entry and exit of Components at the Stock, which can be integrated to notify the arrival of Components requested;
- When all Components are available, the Order of Service. After completion of production, other processes can be initiated, such as issuing Invoice.
We could easily get a MER (Relationship Entity Model) from there. But we know that the system is not just a data repository, so let's look for the relevant actions:
- The Sales Order may contain several Products, composed of several Components;
- When one Sales Order is issued, it is necessary check availability of Product Components. Air is madereserve of available Components, and Issuance of Purchase Orders for the unavailable;
- There is a legacy system that manages the entry and exit of Components in Inventory, which can be integrated to notify the arrival of Components requested;
- When all Components are available, it is Service Order issued. After completion of production, other processes can be started, such as issuance of invoice.
Distilling the domain
With interactions it is possible to identify the core of the business, that is, the core business. In this way, we can start the domain distillation process, that is, identify which, or which, are the main, auxiliary and generic domains, and understand how they relate:
- Primary domain is what brings value to the business. This is where the main logic is;
- Auxiliary domain is what supports the main domain. It can bring necessary information or activities, but they do not represent the main business;
- Generic domain it is a domain that does not have a specific rule for the business in question. It can be a CMS, an email marketing area, an SMS service. To help visualize, one might ask: "would an external solution solve this problem?" If so, it is probably a generic domain.
Distilling the domain not only helps to categorize contexts into main, auxiliary and generic, but it also shows the relationship between them.
- Shared Core: it is a project that is consumed by more than one subdomain. It promotes efficiency, as it prevents a particular stretch from being replicated in two domains, for example. However, be careful: sometimes the information you need in one domain is different in others. Sometimes, the same entity can behave differently in each domain, and sharing it can cause unnecessary or even inappropriate processing. When it is pertinent to use the Shared Core, it is important to be concerned with continuous integration, since more than one subdomain will be consuming the same project. Changing the Core to meet a subdomain requirement can affect the functioning of others, and so on;
- Customer supplier: It is usually the scenario of two different teams, from the same company or not, developing systems that communicate. It is common to have a degree of hierarchy, that is, one team has a certain freedom in changing the communication structure (customer) and the other needs to constantly adapt to these changes (supplier). It may be that at some point, this relationship becomes a Conformist, as shown below;
- Conformist: It is the case of a system in which there is no adaptation on one side. It can be a legacy project, or an external project that is no longer supported, for example. For communication to occur, it is necessary that the local project (or in progress) translates the inputs and outputs for this system, as long as it makes sense to use it;
- Posted Language: It is usually a generic domain, with a specification on how the communication should occur. It can be an API, Service, or Driver;
- Anti-corruption layer: It is a layer that provides communication between systems, "translating" the inputs and outputs. It can be an effective way to live with legacy projects - with it, the main domain will not be polluted with technical debts associated with the legacy project.
Let's go back to our case. We start with a superficial view of the domain:
Based on the submitted specification, we identified the subdomains involved:
After some interactions and dialogues that went well beyond the initial specification, we identified that the core of this company's business is production intelligence and stock control. These two subdomains contain the peculiarities that will guarantee efficiency and quality in the services that the company offers. The registration area is essential, because it will provide the information that will be used in other domains, but it does not have many rules. It is an auxiliary domain. In the case of Sales, Issuer NF-e and Purchases, there is no particular rule other than the basics - issue a list of items to any recipient. They are generic domains.
The identification of subdomains and their roles can be a decisive factor for the success of the project. The team's energy should be directed to the main domain, which will be implemented with the ubiquitous language, involving architectural and design decisions that help to understand the domain by the code. An auxiliary domain does not need the same effort. He can just be… good enough. Simpler architectural strategies may well solve the problem, even with a team of less experienced developers. And a generic domain can be acquired, either by an open source solution, or by a paid product, or even have the development outsourced.
After several interactions, survey of the current scenario, which projects already exist, how they integrate and what is really missing, we identified that:
- The inventory control system is a legacy system that works. There are some concerns about quality and performance, but it does not affect the business;
- The production flow is a rigorous process, today done manually through forms and auditing;
- The purchasing system is a closed market solution that meets expectations;
- The sales system is an outsourced solution, which already has the integrated NF-e Issuance functionality.
In this case, when we classify the domains together with the business specialists, the PO (Product Owner) decided that the Registration and Production area will be developed internally. The Inventory Control project will be maintained. The purchasing system will continue to be used, and the sales system should be adapted to integrate with the Production domain.
For us, what was the impact of that?
We allocate developers with more advanced technical skills to work in the Production domain, and other more novice developers to work in the Registrations domain. Both systems will share the same Authentication and Permissions control.
We have developed an anti-corruption layer to communicate with the legacy project, so that the systems can communicate without “polluting” the main domain.
The architecture of these three systems developed internally (Registrations, Production and Inventory Control) can be seen below:
- For the Registration system, a clean and functional code is enough. It is not a problem to have a transactional and structured approach. Investing in a sophisticated architecture to solve such a simple problem can mean wasted time and money.
- The architecture of the Inventory Control system is quite confusing. It is a project that has undergone team and management changes, had some standardization in the layers and abstractions, but due to a number of factors, it ended up becoming a confused tangle. Note: a messy tangle that works. If it was negatively impacting the business, it might be interesting to hire another team to refactor it. But right now, it is a project that meets the requirements. Refactoring for the simple pleasure of making the code more “beautiful”, without solving a specific business problem, can mean loss of time and money;
- For the Production system: then, you can start with a slightly more robust architecture, in which you can identify the domain logic in isolation from infrastructure implementations, if possible. I'm not saying that the team will spend a month implementing Patterns and services that may never be used. The purpose of the system is to serve the domain. Investing in unnecessary abstractions and patterns can, again, mean wasted time and money.
Continuing with our solution:
- We request a communication interface for the team responsible for the Sales system, and we carry out the integration;
- We developed a conformist translation interface for communication with the Purchasing system;
The result can be seen below, in the Context Map:
If there is a document that Evans recommends keeping, it is the context map. It must be accessible to all teams involved internally. Thus, each developer will be able to measure the impacts and risks of their changes.
Note that the Product Entity is replicated in three domains. This is because, in this scenario, each domain uses a certain behavior of the Entity, which makes no sense to be generalized.
Ok, we have information, we have identified the main domain where we will apply DDD at the code level, and we will start the development. Evans highlighted a series of good development practices already known that can be used to reflect the domain, the ubiquitous language, in the code itself. He called them Building blocks.
I shall superficially explain the less intuitive terms:
- THE entity it is an object that has an identity, behavior, and a defined life cycle;
- a aggregate it is a set of related objects, having an entity as the root;
- O valuable object it is an immutable object, usually with a simpler structure;
- An aggregate often has specific rules for creation (new object) or retrieval (existing object). In this case, you can create factories to facilitate the “assembly” of objects and ensure integrity;
- Domain event is some action with relevance to the business. It may be an event that needs to be registered, or that triggers other internal processes (within the system itself), or external processes (other systems), for example.
None of this is exactly new. They are concepts of Object Orientation, SOLID, and Design Patterns. But these concepts alone, although applied correctly, do not necessarily reveal dominance. Evans showed, example after example, that these features, combined with DDD thinking, can achieve this goal.
Among the examples focused on our case, we can mention:
- Entities: Product, Component;
- Aggregates: Purchase Order, Sales Order;
- Valuables: Purchase Order Item, Sales Order Item;
- Services: Sales Order Issuance, Purchase Order Issuance;
- Domain events: Arrival of components in stock.
Emerging solution and refactoring
In the beginning, the arrival of components in the stock was treated directly with a job that read an output from the Inventory Control system and called a service in the Production system. Over time, the Production system grew, and began to integrate with other areas of the company. The solution was refactored for a microservice architecture, and used the concept of messaging to communicate external events.
I separated some principles used in the implementation of the Production system:
- Starting simple: You do not need to apply all the technical knowledge that you have acquired in life at once. Sticking to what you need can be challenging for enthusiasts. Even so, it is worth the effort to avoid causing accidental technical complexity. A system will rarely start with a microservice architecture, for example;
- Do not get attached to Patterns and features until they are needed: It is important to be careful with “estimation” languages and resources - I believe that at each project start, it is important to identify together with the team which resources will best serve the domain, and not show an exaggerated attachment to certain resources / frameworks before understanding whether they really represent a gain for the project;
- Seize opportunities for improvement: It may be that, after several interactions, the team has matured enough to see that the implementation is limiting the growth of the project. Well, it may be time to refactor some, or even all, of the code. Failing to take advantage of these opportunities can pollute the domain, encourage “patches” and make maintenance more difficult and costly. The fear of refactoring when the need arises can have very negative long-term impacts. The initial effort in applying DDD loses the result, and the project becomes more of a burden, more a candidate to be exchanged in the future with a much higher cost than if it had been adapted as needed.
As you can see, DDD goes far beyond coding. It can represent the strategic direction of the project. It allows a more concrete basis for decision making, such as allocating people according to their skills techniques and business understanding in the most appropriate domains, identify what can be parallelized, outsourced or acquired in the market, understand what can be maintained and what should be refactored.
It is knowing where to invest in a more robust architecture and where to keep the simplest. It is knowing how to live with a legacy project or a third party system properly. It is knowing how to direct the best of your team. And yes, it's also about code.
One of my favorite quotes from Evans is when he refutes the concept that the code is for users. For him, "code is also for developers". The code is the living documentation of the project, and its quality, simplicity and clarity will assist in its life cycle, so that the next teams will be able to understand it, maintain it, and when or if necessary, refactor it.