Tuesday, April 14, 2020

Reactive Architecture: Domain Driven Design

Domain Driven Design(DDD) is an architectural approach to design large software systems. It has its roots in a book named 'Domain Driven Design: Tackling Complexity in the Heart of Software' by  Eric Evans. Interestingly, rules and guidelines laid down by DDD are highly compatible with those prescribed in reactive architecture. DDD basically emphasizes on breaking a large domain into as small domains as possible. Large domains are difficult to model and breaking them into smaller domains helps in simplifying the overall problem. This decomposition of a large problem into smaller domains result into domain boundaries between those smaller pieces of the overall system.

On the other hand, if we look at microservices architecture, goal is very similar to have self sufficient individual services which cater only a specific functionality of the system with a very well defined API for each microservice and have clear cut functional boundaries in between those individual services. And if we add reactive principle of asynchronous non-blocking interaction between these microservices, what we get is a system consisting of reactive microservices.

Combining DDD with reactive microservices helps us to easily and correctly determine domain/functional boundaries between our microservices, which otherwise is not an easy task. There are reactive platforms available, like Lagom, which are built on ideas and concepts of DDD, and makes it easy for developers to imbibe these ideas into their own application.

Of course, both DDD and reactive architecture can be used separately without other. But compatibility between underlying principles of both is very high and hence both are used together very commonly.

Domain and Ubiquitous language

A domain can be defined as an area of knowledge. For ex ample, in health domain we have doctors, nurses, chemists, hospitals, clinics, medical instruments, disease, pathologists etc. We can express this domain knowledge n various ways. Like, we can use diagrams or documentation. Software is also a way to express this knowledge. In this way, a software becomes an implementation of a domain. People who are involved in such a domain are called domain experts.
DDD is an approach in which we develop our software in a way that it implements a domain model and domain experts can easily relate to it. When we do it, it becomes very easy for domain experts and developers to communicate. The communication language is called Ubiquitous language in DDD terminology and it comes from domain experts. Software developers talk to domain experts and get used to the vocabulary those experts use instead of forcing domain experts to get used to software terms, and over course of time their own domain knowledge improves to expert level. There may be exceptions when software developers have to add some software abstractions to Ubiquitous language due to lack of corresponding word in domain. Basic idea is to have discussions about software without using software jargon.

Breaking the Domain

Domains are quite large and complex in themselves and it's really difficult to model them as a whole. One of the DDD goal is to decompose original domain into smaller sub-domains, so that they can be understood easily. When we do so, each sub-domain has its won ubiquitous language and a model. Combination of model and language for a domain is called its Bounded Context. This way we define bounded contexts for all of our sub-domains. It may happen that there is some duplication of ideas and concepts across sub-domains, but temptation to abstract those concepts must be avoided as with time they evolve and become different.
Bounded Context is one of the most important outcome of domain decomposition and they are very good starting points for reactive microservices. We may need to further break a bounded context into several microservices, but they give us an idea that what parts(bounded contexts) must not be part of same microservice.
We may make use of objects to identify bounded contexts in domain. But a more recent approach is to make use of activities and events to identify bounded contexts. This is called Event First DDD. We may simplify the process by using notation like subject-verb-object. This helps us to have a consistent way of phrasing activities and events. Like, doctor examines patient. Doctor, the subject, performs an action, examines, on an object, patient. Once we phrase all our activities and events in our domain this way, it becomes easy to group together related events and activities, thus giving us a better idea of bounded contexts in the domain.

Domain Activities

Within a bounded context, activities can be divided into several categories.

Command: A command represents a request to perform an action. As it is a request, action asked for hasn't happened yet and it is meant for future. Also, as it is a request, it can be rejected. Important thing about command is that it is meant for a specific recipient, like a microservice, and when that recipient accepts it and executes it, state of the domain undergoes a change. Some examples of command can be like, 'Put an order', or 'Make a reservation'. Recipient of the command can reject the command for any reason it deem fit.

Event: An event represents an action that has already happened, and hence notion of rejection has no meaning to it. Unlike command, event is broadcast to multiple destinations, like multiple microservices. Another conceptual difference with command is that while command causes a change in state of the domain, an event records the change in state. And hence, an event is mostly consequence of a command.

Query: A query represents a request for state of the domain, and request sender expects a response, unlike command or event which don't necessarily warrant a response. A query is generally targeted to a specific recipient. Importantly, a query should never cause a change in state of the domain. If we repeat a query, we always get same response back given there has been no change in the state.

In a reactive system, asynchronous non-blocking messaging is the means of communication. Commands, events, and queries are the types of messages we use in message driven system. These messages form the API of a bounded context(microservice, for example).
If we send a command, 'Make a reservation', with all details needed for it, we don't get a response containing status of the reservation(completed/rejected). What we may get is some sort of acknowledgement of receival of the request. This way of communication is asynchronous in nature. To actually know the status of reservation request, we may need to monitor some event, like 'Reservation completed' or 'Reservation rejected'. This way of communication where we don't wait for response makes system asynchronous and message driven.

Suppose we are working on an application which tries to implement a restaurant system. With help of DDD, we have identified bounded contexts like, Orders, Reservations, Payments, Customers, Menus etc.
When we finally try to implement a bounded context in code, we need to map activities defined in bounded context to software entities. When we do so, we use terms used in ubiquitous language. For example, for command 'Open an Order', we have corresponding class or object named as OpenOrder.

Domain Objects

Finding bounded contexts by defining activities also helps us in defining different types of domain objects. There are several categories of domain objects:

Value Object: A value object is basically meant to represent a piece of information. Two value objects containing same information are functionally same. Value object is immutable, because if we could modify it's information it would functionally become another object. Example for a value object can be an object containing address data. Though main purpose of value object is to contain immutable data, they can also contain some basic business logic, like logic to extract some extra information from its attributes. In a reactive system, value objects are perfect for creating messages which are then passed between different components.

Entity: An entity differs from value object in the sense that it is mutable, but contains an attribute which is immutable and acts as identifier for it. For example, a person can be represented as an entity. Person can have attributes which undergo change, like weight, age etc but it has an identifier attribute(like SSN) which uniquely identifies it. Two persons having same identifier represent same person in the system. Entity contains considerable amount of business logic too, like all rules which control modifications of the attributes. In an actor model, entities are perfect for creating actors as actors have a unique identifier in form of their address, and they have a state which is mutable.

Aggregate: An aggregate is a specific type of object which represents a group of domain objects. At the root of this group is always an entity called as aggregate root, which then contains other domain objects. For example, a Person can have address, phone number and name. This group represents an aggregate with Person as aggregate root and other domain objects like address, phone number and name completing rest of the aggregate. We can't access domain objects contained inside an aggregate directly and we need to go through aggregate root for it. Another important aspect of aggregate is that a transaction should not span across multiple aggregate roots. Doing so means aggregates are not defined correctly or transaction has problems. In reactive systems, aggregates make good candidates for distribution. A very important aspect is figuring out aggregate roots. In many cases we have a single aggregate root per bounded context but that's not always the case.

Domain Abstractions

Apart from activities and domain objects, a bounded context also consists of certain abstractions. A very common example of domain abstraction is service. A service, unlike domain objects, is stateless and is basically meant to implement a specific business logic. This logic is not suitable to be part of a value object or an entity. An accepted design practice is to define an abstraction for a service and then provide concrete implementations for it. Domain just know about the abstraction, and doesn't interact with concrete implementation. This design approach has advantage of replacing one implementation with another when needed.
Having services is a good idea, but too many services in the domain also means that services are doing much of the work and this leads to what we call anemic domain. Before we create a service, we should first try to have the job done by entity or a value object.
Some good candidates for service implementation can be email sender service, hashing service, repository service, factory implementation.


No comments:

Post a Comment