Why Domain-Driven Design & Event Sourcing and its Benefits
Monolith application, there are several problems that technical and non-technical teams are trying to deal with because of Monolith application and Layered architecture:
- When the number of requirements increases and different domains and new features are added at different times, it becomes inevitable that the code structure becomes more complex, and adding new features can be quite difficult for developers and also product owners, and business analysts.
- Monolith applications use the layered architecture which is MVC. This put the Model between the Controller and the database, and it almost acts like a database abstraction layer. It generally uses an anemic domain model where the domain objects contain little or no business logic so this can not provide enough rules about declaring boundaries. Because there are no certain boundaries new requirements make apps more complex.
- Too much priority is given to the database so developers are started to think of database-oriented.
- There are complex, slow, and crashing database queries that prevent customers to have a fast app experience.
- There are strongly dependent services where an operation is performed on one service and that service calls directly to another service to cause a balancing operation.
- Communication of team members between business experts and technical and non-technical team members to successfully design and implement a system that solves a business problem is the main goal of teams but because there is no common language between team members and they are not talking in the same language, it may leave its original goals.
Because of all these problems, Domain-Driven Design (DDD) is a really good solution to solve these challenges by bringing together the technical and non-technical teams that working on a software project and this is providing a set of practices and architectural patterns that makes it easy to build a successful, maintainable and stable system.
This article is going to give detailed information about What are Domain-Driven Design & Event Sourcing and its benefits?
What is Domain-Driven Design?
DDD is a set of tools that help you design and implement high-value software, both strategically and tactically.
Strategic Design
This is the first step of DDD and It is used for what is strategically important to your business and how can we divide our business by its importance level and how to integrate it as needed.
Firstly, we need to define bounded contexts by segregating our domain models. Then we need to develop a common language for an agreement between domain experts and technical team members for each bounded context which is called Ubiquitous Language. The language that we develop together will be used in the written, verbal communication, and software model of the team. Then, we can use Subdomains to help us deal with the complexity of legacy systems. Also, we can integrate multiple Bounded Contexts using a technique called Context Mapping.
What are Bounded Contexts and the Ubiquitous Language
Bounded context is a boundary and each component of the software model has a specific responsibility. The components that are inside of bounded context are context-specific. When determining the bounded concepts, we can think of them as our problem space. However, because we combine the components that are close to each other, we started to define our core domain. After this time, creating bounded context will be the solution space for us. All bounded contexts have their language that all team members use for communication, this is called Ubiquitous Language.
All bounded contexts create their language because the same words can have different meanings for each bounded context. For example, for fulfillment team, they are more responsible for addressing information of the shipment model but for payment team, they are mostly responsible for the total amount of price and payment method. Therefore, as we see the same terminology has a different meaning for different bounded contexts.
From a source code perspective, the best practice is that one team should work on one a bounded context, and the source codes, repositories, and database schema should be separated by bounded contexts. It is also possible that one team can work on different bounded contexts. However, many teams should not work in the same bounded context.
Let's continue with an example;
Assume that we are going to develop an e-commerce system and with our product team which includes developers, domain experts, and product owners, we started to analyze our domain.
- We have a product catalog terminology that has name, quantity, price, pictures, variants, descriptions, etc.
- Our customers can buy a product and we should have the state of order like created, shipped, delivered, canceled, order date, description, history of the states changes, order quantity, order price, shipment address, etc.
- Our system should generate a cargo tracking number for the shipment and the user can follow the shipment status, user can change the shipping address. And our system should query the status of different cargo companies.
- Customers have account information. They can login to the system. We should manage the authentication and authorization process.
- Our category managers can prepare campaigns dynamically.
- When our customers or sellers need help with any topic, our system should provide a chatbot and live chat support to solve their problems.
- We should support all payment methods and have integrations with banks. When we receive the order, we should also complete the payment process. Also, when an order is canceled or a customer creates a claim for an order, we should return money to the customer's credit card or bank account.
As we see that there are so many requirements after our analysis as a team and thinking all these requirements together are starting to be a Big Ball of Mud. Therefore, we should first define Core domains and subdomains and define bounded contexts.
As you can see, we addressed the strategic business challenges related to the Domain and identified some Sub-Domains. We have set a Bounded Context for each and classified it accordingly.
What are Subdomains
A Subdomain is a subdivision of the general business domain. One bounded context may have multiple Subdomains. But, the best option is one Subdomain per Bounded Context, and one Bounded Context per Subdomain.
There are three types of subdomains which are Core Domain, supporting subdomain, and generic subdomain.
Core Domain: It is the most important subdomain because it is essential for our business model. Without a core domain, our business will fail. Because of these reasons we have to start with implementing the core domain. For my example below, I prefer to choose Catalog Subdomain as a Core Domain. Because it is the main part that customers will interact.
Supporting Subdomain: It is also specific to our domain and we have to develop it. Therefore, this is also an important Subdomain for us because our Core Domain can not be successful without it. We can count Order and Campaign Subdomains as a Supporting Subdomain from my example because my core domain can not be successful without implementing Order and Campaign subdomains. These are also quite essential for us.
Generic Subdomain: It has less value for our business model. This is not specific to our business so it is possible to buy or outsource this requirement from out of our organization. Therefore, we can count the Payment Subdomain as a General Subdomain from my example because it is not specific to my business and it is quite general enough to allow buying it from out of my organization.
What is Context Mapping
As we learned that in projects, there are multiple Bounded Contexts and our Core Domain needs to integrate with our other Bounded Contexts. In DDD this integration between bounded contexts is called Context Mapping. As we see that all bounded context have their Ubiquitous Language and because of this reason there is a trade of integrating multiple bounded contexts because they know different languages.
There are some kinds of Mappings that we can use to overcome this problem;
Partnership: It is a relationship between two teams. Two teams will Success or Fail together because they have to synchronize schedules and dependent work.
Shared Kernel: It is about the intersection of two Bounded Contexts. Two or more teams will share a common model. The codebase can be maintained and developed by one team. It must require open communication between teams and agreement about what shared model will be included.
Customer-Supplier: There are two sides to this pattern which are Supplier and Customer. Suppliers are trying to provide a solution for Customer’s needs by the suppliers determining what they get and when they get it.
Conformist: When we have the upstream and downstream teams, we can mention about conformist relation between them. Generally, the downstream teams need to adapt upstream team’s requirements. For example, when the sellers want to integrate with the Trendyol Product Team, they should become conformists.
Anticorruption Layer: This is about the translation layer between upstream and downstream teams who have different Ubiquitous Languages. They try to provide a model to isolate their domain complexities while integrating from the outside.
Open Host Service: The protocol is open so that anyone who needs your service can integrate it into the system. Web services and microservices are good examples of this integration model. It doesn’t care about the relationship between bounded contexts and the developer team. You may need to combine the open host service model with any of the other patterns.
Published Language: it is about using a documented language for the input and output of the system.
There are some ways to integrate with bounded contexts like Remote Procedure Calls with SOAP or REST, messaging queues, sharing databases, or file systems.
Sharing a Database or File system should be the method that we need to avoid but if we have to choose it, we need to isolate our consuming model by Anticorruption Layer.
SOAP is about using services from another system. Request and Response travel over the network. It provides a strong coupling between two bounded contexts. The main problem of SOAP is that when the problem occurs on a network or system that is using soap API, our system starts to respond only to errors. However, when everything is working, it can be a good way for integration while we provide a well-designed API that provides an Open Host Service with a Published Language.
REST is integrating the bounded contexts by using four common HTTP methods GET, POST, PUT, and DELETE. A service of bounded context which uses REST interface should provide an Open Host Service and a Published Language with well-designed Rest URLs. The previous failover scenarios that we mentioned about in SOAP are also valid here. In addition to this, we should not force our clients to the Conformist relationships by reflecting Aggregate in domain models. So we should give importance to what clients need while designing REST interfaces.
Messaging Queue is one of the best solution for integration because it allows us to avoid interruptions during communication, unlike Soap and Rest. In this way, an Aggregate that is in one Bounded Context publishes a domain event, then other bounded contexts which require this domain event to subscribe it, and generally, they can create a new aggregate or update the existing one by their business rules.
In this model, client bounded context does not get any response because this process proceeds asynchronously. But, the client bounded context sends a Command Message to the service bounded context and it can still subscribe to the published domain event by service bounded context.
Because it is about asynchronous communication, we should expect some amount of latency and we must consider this delay in our solutions. In addition to this, the messaging mechanism that we choose has to support At-Least-Once Delivery by periodically redelivering a given message in cases of message loss, slow-reacting, or when receivers are down, and also, receivers should be designed to deal with receiving a message more than one time.
Tactical Design
The goal of tactical design is to develop working code by using the domain model. Our bounded contexts have aggregates, value objects, and entities.
What is Value Object
A Value Object is about the value and it should be immutable. It also has a business logic. Therefore, we can try to design our domain concepts as Value objects by keeping them small and reusable.
Let's think about our single value properties that we generally use in our project and convert them into a value object such as phone number, email address, identity number, and tax number.
We can be sure that a value object that contains a phone number is always a valid one with our PhoneNumber.java value object.
PhoneNumber.java example;
public class PhoneNumber implements Serializable, Comparable<PhoneNumber> {
private final String phoneNumber;
private final CountryCode countryCode;
public PhoneNumber(String phoneNumber, CountryCode countryCode){
}
public boolean equals(Object o) {
// Check that the fields are equal
}
public int hashCode() {
// Calculate hash code based on all fields
}
public int compareTo(PhoneNumber other) {
// Compare however you want
}
public static class Builder {
private String phoneNumber;
private CountryCode countryCode;
public Builder() { // For creating new PhoneNumber
}
public Builder(PhoneNumber original) { // For "modifying" existing PhoneNumber
phoneNumber = original.phoneNumber;
countryCode = original.countryCode;
}
public Builder withPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
// The rest of the 'with...' methods omitted
public PhoneNumber build() {
if (this.countryCode == null) {
throw new IllegalArgumentException("County Code can not be empty");
}
if (this.phoneNumber == null) {
throw new IllegalArgumentException("Phone number can not be empty");
}
if (this.countryCode.equals(CountryCode.TR) && this.phoneNumber.length() < 12) {
throw new IllegalArgumentException("Phone number format is incorrect");
}
return new PhoneNumber(phoneNumber, countryCode);
}
}
}
What is Entity
It is an individual one and it has a unique identity. Entities can be mutable and their state can be changed. For state changes, we can specify state-altering methods rather than using setters methods. Because setter methods only modify the properties but it does not know why.
For example, we have an Order entity, and it has quantity, state, and delivered date properties. Instead of setting them one by one, we can write a method called orderDelivered that keeps the business logic and changes state according to the business requirements.
public void orderDelivered(Long quantity, String reason) {
if (this.quantity - quantity - this.deliveredQuantity == 0) {
setSate(OrderState.DELIVERED);
} else {
setSate(OrderState.PARTIAL_DELIVERED);
}
setDeliveredQuantity(quantity);
setDeliveredDate(DateUtis.getNow());
this.changeLog.register(new OrderDeliveredEvent(quantity), reason);
}
What is Aggregate
It is a group of entities and value objects which have certain characteristics. It has a consistent stage and an Entity own the aggregate this is called the Aggregate Root and the ID of the Entity is used for the Aggregates identifier.
The only aggregate root is accessible from the outside. Therefore, when we design an entity, we should decide whether this entity will be an aggregate root or just used as a local entity in aggregate. For example, if there is a parent-child relationship between entities, the parent one is automatically called an aggregate root, and the child one is called a local entity.
What should we pay attention to when designing an aggregate?
- We should design our aggregates small and simple by avoiding a large number of one-to-many relations and reading and writing small data.
- We should refer to another Aggregate by using its’ ID instead of referring to it directly. It is possible to create a Value Object that has the ID of another Aggregate Root.
public class Order extends AggregateRoot<OrderId> {
private CustomerId customerId;
// Other fields and Methods
public void copyCustomerInformationToOrder(CustomerRepository repository) {
Customer customer = repository.findById(customerId);
setCustomerName(customer.getName());
setCustomerSurname(customer.getSurname());
}
}
- All Aggregates would have their transaction and after any state change occurred, all aggregate committed their transaction because they have a separate transaction. Therefore, when we need to handle transactions for multiple Aggregates, using domain events and eventual consistency is a good choice.
- To prevent data loss while saving or updating Aggregates, we can use optimistic locking which is easy to distribute and scale
Domain Events
It is an event that occurs in the business process. As we know that a domain model has a static state at a time. By the domain events, the state of the model is changing. Domain events have a specific order, one operation can not start unless another event happens. It means that an Aggregate can not modify or create unless another Aggregate operation is completed.
Domain events are published by Aggregate Root or Services. They should not be updated and they should have creation time and ID to make them unique.
It is possible to consume events by more than one listener and the publishers who create an event are not care about the event is consumed successfully or not because all listeners have their transactions. Thanks to this, adding a new feature to our system is quite easy because the codes that we developed before are not affected by these new features. We just add new listeners and grow our business requirements.
Event Sourcing
All domain events are caused by a Command. Aggregates take command and make the necessary business validation and update or create the aggregate.
Then publish the domain event to the event store. Thanks to this we never lose our business data by applying all the domain events one by one to create the final state of the aggregate. Therefore, all aggregate should have their transaction, and modifying the aggregate state and saving the event to the event store should be in a single transaction.
For example, assume that we are using Trendyol Wallet effectively and we make 10 transactions per week. after 2 years our wallet aggregate started to deal with 960 events and the number of events will be increased day by day. Because of this reason, the performance of our system can be diminished. To prevent this performance loss, we can use caching and snapshots. Caching is about storing the latest state somewhere. And snapshotting strategies are about snapshotting every N number of events or snapshot events when specific types of events occurred or. etc.
In this article, I tried to give some information about Why we need DDD and Event Sourcing. Next article, I am going to mention about CQRS & Event Sourcing with Axon Framework.
Thank you for sharing your time and reading my Article!!.