How to create better code using Domain-Driven Design

16/03/2020

Last updated: 16.10.2020 03:17

In this post, I am going to present how you can use domain-driven design tactical patterns like value object, entity, repository, domain event, factory, and domain service, to write better code. By better code I mean code that is more readable, easier to reason about and maintain. We will start with business requirements, then we will have a look at implementation that does not use domain driven design and has an anemic model, then we will analyze problems related to such approach and compare it with code build with help of domain-driven design tactical patterns.
The source code for this post is located here at Altkom Software & Consulting Github.

Business requirements

Let’s start with some business requirements. Let’s assume that we are working for a retail bank that is selling mortgage loans. Business wants to increase the efficiency of loan application processing for individual customers. To achieve this goal they want to automate application score calculation and combine information about customers from sources available online.
Scoring rules should be relatively easy to add and change. Applications that have RED scoring results should be automatically rejected, others would require a manual check of documents sent by the customer and then approval or refusal decision should be recorded.

That’s what high-level requirements look like. Let’s see how the business process looks in a more detailed manner.
Loan application processing steps:

  1. The operator submits loan application with property information, customer data, loan information and attached documents provided by the customer.
  2. The system performs basic validation: required fields, data formats.
  3. Operator commands the system to calculate the score based on rules.
  4. If the score is RED application is rejected and an explanation is provided.
  5. If the score is GREEN then the operator validates the attached documents and accepts application or rejects it due to discrepancies between provided data and documents. The system validates the operator’s competence level.

Let’s review the scoring rules for our application score calculation. Here are the rules:

The anemic enterprise cake

Onions have layers, ogres have layers, and guess what else have layers – enterprise apps have layers too. We will first have a look at something that is still a very popular style of designing and implementing applications, especially among the enterprise developers. This is a layered architecture with an anemic domain model.

Image14 - Altkom Software & Consulting

This is still a very popular option and we are going to explore the typical layered app in more detail here. There are many reasons for the popularity of this approach. First, it is very simple to understand and implement. Second, people find it in tutorials from framework and platform vendors.
In this approach, code is organized by layer, where each layer has different technical responsibilities.
You can open a solution that contains our loan application processing service implemented this way from here.

Project structure

We can see the project structure below

Image8 - Altkom Software & Consulting

Each layer is implemented in a separate project and deployment unit.
We have:

Where is the business logic?

Let’s investigate how the business requirements were implemented. In our quest to find business logic we start with the Business Logic project.

Image4 - Altkom Software & Consulting

Things look very promising here. We can identify some concepts from business requirements like LoanApplication for example. It seems to be the core concept here so let’s have a look inside.

Image26 - Altkom Software & Consulting

This class is utterly deprived of any behavior. It is not a class in classical OOP standards. It is a data structure. By looking at its source code we can tell nothing about our system. We do not know what the system can do with Loan Application. We do not know what are valid values and states Loan Application can be in.
That’s not all. All properties of this class have a public get and set. This means that any code can set these values without conforming to business rules we should obey. This totally breaks another OOP basic rule – encapsulation. Other classes (or rather data structures) representing business concepts like Customer or Property have the same issues.
In order to understand the system, we have to look elsewhere.

Our next stop is LoanApplicationService. Success!!! We found a class where the actual logic lives. We can find methods that do something related to loan application processing. We do not see a method that submits application but CreateLoanApplication seems like a possible match. We have a method that Evaluates loan application and ones that Accepts or Rejects application.
We also have methods that search for loan applications and return details of an application with a given number. This class does not conform to a single responsibility principle as it does all the things related to loan applications. We will see later how to break such class into smaller ones with the help of the CQRS pattern.

Loan application evaluation seems like a major business requirement we have to support. So let’s see how it is implemented.

Image21 - Altkom Software & Consulting

We found out that there is another class responsible for evaluation called ScoringService. As we can see, after calling the EvaluateApplication method, we have some logic that changes application status to Rejected for applications with a red score. This means that not only LoanApplicationService changes things inside the LoanApplication object but also ScoringService. With an anemic model, this is one of the problems. You can never be sure where the state of your business object changes as any piece of code can set its properties to any value. The next issue with such implementation is that we have functions on a stateless class that has side effects. Without analyzing the code we do not know if the given function changes something inside objects passed as parameters or not.

Now let’s see how scoring rules are implemented in the ScoringService.

Image24 - Altkom Software & Consulting

Rules are implemented as a set of if statements. Someone could refactor it to set of functions but that will change things only when the number of rules will increase.
But let’s analyze some of the rules. Does the code resemble language used to describe the requirements?

Image17 - Altkom Software & Consulting

It takes a while to realize that this code is equivalent to “The customer age at the day of the last loan installment must not exceed 65 years”. This is, of course, a very simple expression but I’ve seen in my career expressions that span up to 5 or 6 lines of code and were extremely hard to “decode”. We will see later how Domain Driven Design tactical patterns can be used to avoid it.

Image7 - Altkom Software & Consulting

This is another example of the same issue, plus it presents a completely wrong approach to the calculation of a monthly installment. It seems that the developer did not ask the right questions. Compare this code with “The loan monthly installment must not exceed 15% of the customer monthly income.”.

For the sake of completeness, we will also see how LoanApplication objects are constructed to be in a valid state. It happens in the CreateLoanApplication method of LoanApplicationService. You can see that data is copied from DTO into a business object and then another service performs validation. In our case, it is ValidationService that uses FluentValidation library to validate instances of our business objects. With this approach nothing blocks from the creation of LoanApplication with an initial state that is not compliant with the business requirements. For example, we can create LoanApplication without a customer or with a negative income.

One last thing I’d like to point out is the usage of generic repositories. If you look inside the Data Access project, you will find GenericRepository<T> class, which implements GenericRepository interface.

Image1 - Altkom Software & Consulting

This is all really nice and cool, as with very little code you get all data access fixed. You can now not only create, update, delete and load any entity but also you can do any kind of query using IQueryable. I am not a fan of this approach. First of all, not all classes should support all CRUD operations. Also, the ability to construct any kind of query causes that data access responsibility leaks out of the data access layer. This may make writing unit tests harder. Also, we lose the ability to hide querying details inside dedicated classes that will correspond to concepts in the business domain.

Issues with the anemic domain model

Let’s sum up our findings.

Anemic model

Classes that represent concepts from the business domain are an example of an anemic domain model anti-pattern. Based on their code we can tell nothing about the way that the system works. They also break many OOP rules like encapsulation. This way any piece of code can change the state of our business object to a one that violates business requirements. Even at construction time, no rules are validated, allowing a developer to create the object in a completely invalid state. Such an approach results in code that is significantly harder to reason about and understand as we have to search the whole codebase to find places where certain properties are set.

Primitive obsession

Another issue observed in the codebase is a primitive obsession. We created only 3 or 4 classes to represent concepts in our business domain. We mostly used primitive types like string, decimal or date time to construct our classes. By doing this we lost many concepts expressed in the requirements and also we made validation and encapsulation harder to achieve.

The gap between the language of business and language of code

We also observed that there is a gap between language in which the requirements are expressed and the language used in code. This is a result of a lock of modeling and collaboration with business experts that would allow developers to have a deeper understanding of the underlying domain. As a consequence of this, with time the code will drift further and further away from the language of business and also will be “polluted” with technical jargon. It will make maintenance and further development of it much harder, as each time a new developer will have to “map” concepts from the business world into concepts in the code. This will also make conversations with business experts harder as both parties speak completely different languages.

Readability

Although code in our example solution is pretty “clean” and easy to understand, as requirements grow and change it will become harder and harder to keep the code clean and readable.
We have seen that even in simple cases like our scoring rules, it may take some time to understand. What is worse, we have to search our codebase in order to find all places where something related to a given concept – like application status – happens.

DDD to the rescue

Time to make our code better. Before we can use our lovely domain driven design patterns we have to take care of something more important. We have to spend our time discussing the business domain with business experts. There are many questions we should ask like: When calculating someone’s age should I consider just a year or the whole birthday? How do I calculate a monthly installment for a loan? Does the currency matter or loan amount is always in the same currency as the customer’s income? When calculating the last installment date, which date should I take as a starting point?
There are many tools and techniques you can use for efficient communication including event storming, domain storytelling or example mapping. But we won’t discuss it here as our focus is implementation. For now let’s assume that we have done our lessons well, discussed with the domain experts and spend some time modeling and creating our project ubiquitous language.

Let’s move on to the code. The solution seems much simpler as it contains one main project, a test project and a mock for the national debtor system.

Image2 - Altkom Software & Consulting

Let’s see what’s inside of the main project.

Image20 - Altkom Software & Consulting

We will start with the domain model folder

Image10 - Altkom Software & Consulting

What a change! We have three times more classes than in anemic model solution and we can spot concepts familiar from business requirements. We see a language much richer than in the previous approach. Most of these names should be familiar to a domain expert – we have LoanApplication, Customer, Property, ScoringRule, ApplicationScore. In fact, during the modeling phase we collaborated with a domain expert to discover many concepts important for the domain that we missed previously. In our code, we made many concepts that were implicit – explicit. With the help of domain driven design, we created a solution that should be easier to understand and one that closely models the business problem we are trying to solve. Let’s see in more detail how each of the tactical patterns was used.

Value Object

Value objects are the basic building blocks of our domain model. We should have many of them. If you are doing domain modeling and don’t have many value objects it is, in my opinion, a design smell. It means you are missing some important concepts and your entities are overloaded with responsibilities. You should consider another round of modeling with domain experts and extraction of these responsibilities into value object classes.
Value objects represent concepts in the problem domain. They have no identity and equality is based on attributes values (hence the name Value Objects). They are immutable and replaceable, and in contrast to entities have no lifecycle.
We use them to group a set of related attributes and behavior related to those attributes. Value objects just like entities should protect themselves from being in an “invalid state”. We should validate incoming data in a constructor or in a factory method and do not allow the creation of value objects that do not conform to business rules. Value objects can, of course, be composed of other value objects.

In our example, we have many value objects. Some of them are very simple containers for related data like Address, Name or ScoringResult. Some represent measurements and quantities encapsulating calculation rules like MonetaryAmount
Some represent specific concepts of our problem domain and are more complex like Loan or Customer.

Let’s have a look at Loan class. This class represents loan parameters and is responsible for monthly installment calculation and last installment date calculation.

Image13 - Altkom Software & Consulting

All our value objects are derived from a base class that takes care of comparison and equality implementation. Equality is based on values of attributes – list of these values is returned from GetAttributesToIncludeInEqualityCheck (there are many other ways to implement a base class for value object – for example, you can use reflection to iterate over all attributes of a given class)

Image19 - Altkom Software & Consulting

And finally, some business logic implemented on value objects that represent concepts in the domain and are used in the business requirements.

Image3 - Altkom Software & Consulting

With such methods, we can much easier and clearer express something like: “The loan monthly installment must not exceed 15% of the customer monthly income.”

Image5 - Altkom Software & Consulting

or “The customer age at the day of the last loan installment must not exceed 65 years”.

Image22 - Altkom Software & Consulting

As you can see, now the business rules in code can be read almost like a regular sentence. Isn’t that great?

Entity

Entities represent concepts in the domain problem that have a unique identity and have a lifecycle. Identity does not change over time. For example, in the insurance domain, we may have a Policy entity that has a number that uniquely identifies given policy issued by our insurance company. In the banking system, we may have an account entity identified by an account number. Sometimes we do not have natural business keys and have to use surrogate keys generated by our system.
Entities are composed of value objects and relation to other entities. As in the case of value objects, they are responsible for hiding and protecting its internal state from being “invalid”. This is often called protecting invariants in wise DDD books. Basically it means that methods in our entity class should make sure that the execution of the given operation is allowed in its current state and that result of such execution is in accordance with business rules.
Entity should push as much behavior as possible to value objects and other entities it is composed of. It should “orchestrate” them and update its state based on the results. Sometimes entities must use domain services to perform some business logic. In such cases, we usually pass domain service reference to an entity method that needs it. We will see an example of such an approach with LoanApplication class that needs scoring rules service in order to evaluate itself and then update its score based on the results.
There are some useful patterns that can help you implement entities. For example, the specification pattern helps you move implementation details of validation against business rules into separate classes used by an entity class. Also if you have many possible “states” of a given entity, wherein different state different operations are allowed, then consider implementing each state as a separate class – use state pattern cautiously.

The main entity in our example is LoanApplication class. Let’s have a look at how it is implemented.

Image11 - Altkom Software & Consulting

It is derived from a common base class. In our example, the only thing that the base class has is a property that represents id. To represent an id of each entity we created a value object class for each id type. This is very useful if you follow Vaughn Vernon’s advice on designing effective aggregates. This way your ids are type-safe and the compiler will detect any places in code where we try, by mistake, to pass the id of something else than expected, which might happen if we use Longs or GUIDs for ids.
Our entity is composed of value objects, which is good. You can also see that access to properties is private or read-only. This way we achieve encapsulation and protect our state from being changed without checking the business rules.
In some cases public getters are needed for the tests. We could get rid of them with the help of the memento pattern, but in my experience, this is usually not worth the effort.

Image23 - Altkom Software & Consulting

Image16 - Altkom Software & Consulting

As we can see our entity class protects itself from being created in an invalid state. But due to the use of value objects, most of the work is delegated to value objects’ constructors.
The only thing left is just checking for null values.

Image28 - Altkom Software & Consulting

Now we have our business methods. The first one is Evaluate. Here we can see an example of collaboration between entity and domain service. Our entity does not care what the rules are and how the score is calculated. This is delegated entirely to the domain service ScoringRules class. Our entity only cares about the evaluation results and updates its state based on it.

Image12 - Altkom Software & Consulting

We also have methods responsible for acceptance or rejection of an application. As you can see these methods take care of making sure that the application is in the right state and that the user has appropriate permissions. Here we can also see how we “link” two entities that are parts of different aggregates, an instance of Operator class and an instance of LoanApplication class. We do not create direct references between them. Instead, we only reference parts of other aggregates by id.

Aggregates

Aggregate is the most powerful tactical pattern but also the hardest to get it right. As bounded context divides our solution space into smaller areas focused on certain business capability, aggregates divided bounded context into smaller groups of classes, where each group represents concepts closely related to each other, that work together.
With this pattern we should decompose large models into smaller clusters of domain objects.
A group of connected value objects and entities form aggregate. One class that is a gateway to functionality offered by the whole group is called an aggregate root.
Aggregate forms a transactional consistency boundary. This means that changes to aggregates should be persisted in one database transaction or the whole state change should be rolled back.
As this is a complex and important topic it requires a separate post and we are not going into the details of how to design aggregates here. If you are interested in this topic there is a dedicated section in Vaughn Vernon’s book “Implementing Domain-Driven Design” or you can check out a series of his articles on the subject.

In our simple project we have two aggregates LoanApplication and Operator.

Domain Service

There are situations when it is hard to assign given responsibility to just one of the entities or value objects in our model. In these situations, we should try to use domain services. Domain Services encapsulate domain logic and represent business concepts from our domain. They are part of the ubiquitous language we created together with the domain experts. Usually, domain services represent some kind of business policy. Domain services, unlike entities and value objects, have no state and no identity.
In order to perform their tasks, domain services orchestrate entities and value objects. There are two places where we typically use it. In application services, we can instantiate domain service from a dependency injection container (although I prefer to wire dependencies manually and not register domain classes in a DI container). In the domain layer, we can pass instances of domain services to entities (double dispatch pattern), use Service Locator pattern or decouple with the help of domain events (domain service can subscribe to the interesting domain events and perform its tasks when given event happens).
Another usage of domain service is to represent a contract for external service for which we want to hide the implementation details, protecting our domain model from the outside influences.

Let’s see two examples of using domain services in our project.

The first example is already mentioned in ScoringRules class.

Image9 - Altkom Software & Consulting

Scoring rules and evaluation implementation do not naturally belong to any entity or value object we have. They are not part of the LoanApplication, nighter Loan, Customer or Property, therefore we created a separate class to represent the concept of rules and score calculation. This way we took the burden of managing rules and hid internal implementation of the scoring algorithm from other elements of our model. This means that any changes here, like adding new rules, won’t require changes in the LoanApplication class. It is also an example of applying the single responsibility principle to our codebase.

The second example is the use of domain service to wrap external service and provide an anti-corruption layer.

Image18 - Altkom Software & Consulting

Here we just define an interface in our domain layer, that will allow our domain classes to interact with the external debtor registry system using concepts our domain understands, like Customer. This interface will be implemented in the infrastructure layer and will translate between our domain model and model used by the external system.

Domain Event

Domain events are very useful constructs if you want to reduce coupling between components of your system and between your system and the external world. However, like anything in system architecture, they shouldn’t be used without consideration.
Domain events represent important business events in our domain and are part of the ubiquitous language. Technically we use them in a publish-subscribe manner, where some elements of our domain model rise events and others subscribe to it and react accordingly.
We can divide domain events into two categories internal and external.

Internal

Internal domain events are used inside our bounded context. This kind of event can reference domain objects (entities, value objects). Internal domain events have two common use cases. First, is to synchronize the state of different aggregates, where one aggregate updates its state and raises an event and the second aggregate subscribes to such an event and reacts to the changes. Second, is to subscribe to domain events in the application services and perform some infrastructure-related tasks such as sending e-mail or SMS to a customer when a loan application is accepted.

External

External domain events are used to synchronize between bounded context, which in case you are building microservices, might mean synchronization between two physically separate systems. Such events cannot reference domain objects and should only use simple types so that other systems could understand it and to avoid coupling our domain concepts with other systems. External events are part of the published language. This means that changing it would require careful analysis, as we do not want to break external systems, over which we do not have any control. Such events usually contain just identifiers of all aggregates which changed state as part of a given event. Sometimes more data is added in order to avoid unnecessary communication between bounded contexts.

Let’s have a look at domain events in our sample project. Below is LoanApplicationAccepted class which represents an event of loan application being accepted.

Image1 - Altkom Software & Consulting

As you can see it uses simple types not domain types. It has only id of an aggregate that has changed as part of the event. It is also designed to be serializable to Json format.
Below you can find an example of how to raise a domain event.

Image25 - Altkom Software & Consulting

Here in the application service we publish the event. Actual implementation of a mechanism that published events is part of the infrastructure layer. In our example you can find an implementation that uses EasyNetQ library to send messages to RabbitMQ message broker. You can find it in RabbitMqEventPublisher class.

Factory

In our example the creation login of our entities is pretty simple. But there are cases when the process of building our domain objects is complex and may involve some domain services or repositories. For simple cases a constructor or better a static factory method on entity (or value object) class is sufficient. For more complex scenarios we should move that complex logic from entity into separate class – a factory class.
Factory class will encapsulate complex business logic required to create object in valid state and will ensure that all required business rules are fulfilled.

Repository

This pattern is simple but tricky at the same time. The problem with it, is similarity to repository treated as data access pattern. In domain driven design a repository is not just a data access object, which implements all CRUD and database queries needed for given entity type. In domain driven design repository should be part of the ubiquitous language and should reflect business concepts. This means that methods on repository should follow our domain language not the database concepts.
We often see generic repositories with methods for CRUD and methods allowing execution of ad-hoc queries. This is not a good way if we want to follow DDD principles.
Instead we should only expose operations required and create methods for queries related to business concepts. For example if we are building task management system, and we want to query for overdue task, which in terms of business requirements means tasks with due date two days behind schedule, we should have a method like GetOverdueTask instead of GetTasksByDueDateAfter(DateTime theDate).
We should try to follow a collection-like interface as it should allow us to work with entities stored in a database as if we were working with regular collection.
In the domain layer we only define an interface which is implemented in the infrastructure layer. This way repository interface is a boundary between domain model and persistence model.
We should create repositories only for entity types that are aggregates roots.

Let’s have a look at how repositories are defined in our example code base.

Image15 - Altkom Software & Consulting

You can see that this repository, responsible for LoanApplication entity, is designed to have a collection-like interface. It exposes only a minimal set of operations required to satisfy business requirements. There are only methods to add a new loan application and to find one by loan application number.
Note that it does not take part in the read side of application. There are no methods here related to searching for loan applications. You can find these outside of the domain model in the read model part (according to CQRS principles) in classes such as LoanApplicationFinder.

Application Service

Application services are not part of the domain model. They live on the edge of the domain model and are in fact client of domain model. They expose domain model capabilities to the external world. Typical responsibilities of an application service are setting up dependencies, transaction management, authentication and authorization (though in many cases security concerns are and should be part of the domain model), logging and errors handling. Application services are also the place where we should perform “technical” tasks like e-mail sending.
Application services should not contain business logic. It should delegate the actual processing to domain classes. In a typical scenario application service uses repository to retrieve an instance of an entity that is an aggregate root, then calls a method on this aggregate to fulfill business use case.

Let’s have a look at one of the application services in our example application.

Image27 - Altkom Software & Consulting

As you can see, this class is responsible for loan application evaluation. It has no direct implementation of business logic. All the processing is delegated to entity class. Application service just sets up all required dependencies, uses repository to load an aggregate, tells it to perform business logic and finally takes care of commiting the transaction.

CQRS

Our sample application is divided into separate classes and models for read operations and for write operations in accordance with CQRS principles. CQRS is a very simple, yet powerful pattern that also enables us to write cleaner and less coupled code, as now we can use different tools for reading and different for writing, and our classes are smaller, and more cohesive. You can read about CQRS in one of our previous posts.

Summary

I hope this post showed that applying domain-driven design tactical patterns helps us write better code. We have smaller, more cohesive classes that conform to single responsibility principle. Also, in our code we operate with the same language we speak to the domain expert. This is very important because adding new functionalities or changing existing ones should be much easier when we can discuss things directly with experts, without need to translate concepts from business domain into completely different code constructs.
From my personal experience it is much easier to find where and why certain things “happen” in my code if I follow DDD principles.
Please note that tactical patterns can effectively be applied only if you perform iterative modeling activities together with the domain experts. Tactical patterns work best together with strategic patterns like bounded context and bounded context mapping.

Code for this article can be found at https://github.com/asc-lab/better-code-with-ddd.

Useful links

  1. https://github.com/asc-lab/better-code-with-ddd
  2. Patterns, Principles, and Practices of Domain-Driven Design by Scott Millet with Nick Tune
  3. Hands-On Domain-Driven Design with .NET Core: Tackling complexity in the heart of software by putting DDD principles into practice by Alexy Zimarev
  4. Implementing Domain-Driven Design by Vaughn Vernon
  5. Presentation from ASC Meetup on Writing Better Code with DDD

Autor: Wojciech Suwała, Head Architect in Altkom Software & Consulting