The changes in our world in recent years have led to increasing complexity in all areas. As a result, the demands on the software we use have also increased. How can the complexity of real processes be mapped in functional software systems today? Domain-driven Design has proven its worth for us at IT Sonix in large and complex software projects - but what is meant by domain-driven design?
In 2003, the term was established by Eric Evans with the publication of his book "Domain-Driven Design. Tackling Complexity in the Heart of Software", a collection of tools and techniques designed to help model complex software. In addition, approaches and principles for increasing productivity are also central to domain-driven design, which is achieved by focusing on the technicalities and the reality of the business logic to be modelled.
Developers and experts work closely together during modeling, creating a conceptual world that extends into the code. It is interesting to note that in this phase the model only uses language, and no programming takes place at this stage. Both in theory and in practice, domain-driven design can be divided into two major areas: strategic design and tactical design.
While strategic design deals with the architecture (i.e. the context delimitation and the relationship between them), tactical design takes care of the implementation of this elaborated model. However, before we dive deeper into the modeling and code thematically, we will briefly explain the most important terms of strategic design.
Strategic design provides valuable concepts for the professional mapping of business processes in the software: domain model, ubiquitous language and bounded context.
As the name suggests, the domain model deals with domains. Here, domains refer to reality, e.g. the entire company or operation, and not to the end product (the software). These domains can be divided into subdomains, such as departments, teams, or business processes. Of course, the domain model is not the actual reality, but an abstraction of it. The most suitable means of translating reality into a model is language, which is why so much attention is paid to it in domain-driven design.
The key to success lies in the ubiquitous language. Ubiquitous means omnipresent, and that is exactly what domain-driven design emphasizes: experts and developers work together to create a language that is spoken by everyone involved and that is consistent from the domain down to the code. The ubiquitous language is limited by the bounded context.
The context in which the model is applied in domain-driven design gives it meaning, and the bounded context forms the boundary of a model for a particular purpose. This boundary is especially important when a particular concept of a domain is represented by different models. For example, the word customer has a different meaning for support than for sales. Although it is the same customer in the domain sense, different bounded contexts can be determined here, each modeling a customer that fits the subtasks of the context.
Defining bounded contexts for a specific domain is one of the biggest challenges in domain-driven design. Collaborative methods such as event storming or domain storytelling are suitable for this purpose, in which domain experts and development teams jointly explore the respective domain. The resulting visualizations provide an initial basis for identifying parts of the domain that are as independent as possible and defining them as a bounded context.
The established bounded contexts are essential for the team organization as each context is managed by a team. A team can also take care of several contexts, but never several teams of one context. Context mapping can be used to describe dependencies between the contexts and thus also communication types between the teams.
Once the domain model, a ubiquitous language and the bounded context have been defined, the design and implementation of software structures and patterns begins. This is the task of tactical design with its designated building blocks: value object, entity, aggregate, repository, service, and factory. These are sufficient to model any domain.
The value object is the smallest component of a domain and is characterized by its properties – it has no identity, and its properties are immutable, it encapsulates primitive value. Accordingly, value objects that have the same properties can also be exchanged. In domain-driven design, for example, a separate object is generated for each postal code, something that can also be used for other complex data types such as amounts of money. Each calculation operation on a value object defined as a monetary amount would create a new value object from the respective result. No matter how small the counterpart may be, in domain-driven design there is a model for everything.
One of the advantages of this method is that an object can be created for anything, and it is not just a variable name, so it cannot be confused with others (strings). The value of the object is immutable. A value object can also consist of several value objects, such as an address, which is composed of the different value objects street, postal code and country.
The next important part for modeling is entities. Unlike value objects, these have a unique identity. They are also not dependent on the properties assigned to them – these are variable and interchangeable and have no influence on their identity. These entities are at the center of the entire domain-driven design and the software.
If we stick with our example of streets and postal codes, an individual could be an entity. The individual remains the same person, even in case of address changes, the person can be distinguished by his or her identity from another individual with the same name. Entities can also represent business functions. If, for example, it is necessary for a business process to fulfill a certain knowledge function, a separate entity is often created for this purpose.
In domain-driven design, value objects and entities are organized hierarchically. This requires aggregates. Instead of dividing related logics in the code into different services, which can lead to inconsistencies, aggregates encapsulate all business logic for a specific concept (bounded context) – they define invariants. As a result, only interfaces that create a consistent state are exposed.
An aggregate is always used for business functions that result in changes to multiple entities. For this, a change is made to the aggregate root, which overrides all entities arranged within the aggregate, and not to the individual entities themselves. This is the only way to ensure that these functions are executed on all necessary entities. Operations on aggregate roots behave in a similar way to database transaction.
Repositories are needed to represent a set of aggregated roots. They are completely independent of the persistence technology within the domain and usually represent only an abstraction of it. They are implemented only by the infrastructure layer.
For functionalities that cannot be represented by an entity or value object, there is the option of defining them as a service. Even if this is a rarity, since almost everything can be defined with the above-mentioned components, services can still be helpful for modeling certain functionalities that, for example, do not work hierarchically or do not even exist as an entity in the first place.
The factory makes it possible to move the responsibility for creating complex aggregates to a separate object.
In addition to these purely domain-specific components, it is also necessary to implement technical aspects. These should be organized in such a way that there is a clear separation of purpose and loose coupling between them. For this purpose, domain-driven design offers a layered architecture model, which consists of infrastructure, interfaces, application and domain.
The infrastructure supports all other layers and often consists of implementations of domain abstractions: database repositories, message backend, REST clients, and external libraries are examples.
The next layer is interfaces, which represent the gateways to the outside world, such as REST endpoints or message handlers. They form the top layer and are therefore allowed to access all the layers below them. The key point of domain-driven design is that the domain does not develop any dependency on the infrastructure. The rule is that access is only allowed from top to bottom and not vice versa. However, shortcuts are possible. The interface layer can access the domain directly without taking a detour via the application layer. This is often the case fur purely read operations, e.g. via a REST interface. The interface layer takes care of serialization and validation. Primitive data such as a simple ID is converted into a value object here, which is also the validation of the input parameters. Use cases or domain logic do not fall within its scope of responsibility.
The application layer is responsible for the use cases. Complete use cases are specified and coordinated here, but without domain logic. Database transactions and security mechanisms such as user rights usually take place on this layer. Especially in the case of user rights, difficulties can sometimes arise when it comes to assigning the correct layer. How much are they part of the domain or just a technical tool? If it is something purely technical, then the Application Layer is intended for this in domain-driven design. It is important that this layer is independent of the calling interface - i.e. the layer above it. The application layer should only use objects from the domain for input or output since it only uses them and does not convert them. It is recommended that each method of this layer only interacts with at most one aggregate root to keep complexity low. If a use case requires interaction with multiple entities, this may indicate the need for another aggregate root that encapsulates them and ensures their consistency and invariance.
The last layer - the heart of the software - is the domain itself. It contains the entire domain logic - albeit only as an abstraction - and is consistently aligned with the ubiquitous language defined in the strategic design.
Since the domain model is not dependent on any other layers, domain-driven design can also be realized with a hexagon or onion layer model, since access is always from the outside in, and each layer has its own authorization.
How do we work?
For the implementation of complex and large projects, the layered architecture model is particularly useful for us. The clearly separated layers help us to think about the business logic completely independently of the technologies used. If, for example, something changes in the business rules, it must be adjusted in the domain layer, without any significant impact on other layers. In addition, domain-driven design makes it possible to understand the business logic without much effort using the code in the domain layer, since this does not require technologies such as databases, REST interfaces, etc. At the same time, new technologies can be implemented without damaging the business logic.
Since the application of domain-driven design is associated with great effort, this approach is primarily recommended for very large and particularly complex projects. This method can score points almost across the board since the isolated domain layer and the language used consistently in all layers (ubiquitous language) – right down to the code – make it possible to easily retrieve all of the business technicalities from the domain.