Let the Domain Drive the Design

0
665
Gear design

Object-oriented design, when driven by the domain, decomposes huge monolith systems into a set of agile microservices. Domain-driven design has changed the narration of the object-oriented approach upside down. It has brought the domain objects onto the centrestage of design thinking, and relegated the technology to the peripheral infrastructure. In this fourth part of the ‘The Design Odyssey’ series, let us examine this approach to software design in some detail.

The Socratic method of learning involves continual probing and cooperative dialogue that is laced with argument. We go by that method to understand the concepts and patterns of domain-driven design. Let’s go!

Domain-driven design: Concepts and patterns
What is domain-driven design?

Domain-driven design, or DDD in short, is an approach to designing software systems by applying object-oriented principles in their true spirit.

It implies that DDD is not an alternative to object-oriented design; instead, it only complements it. However, the basic principles like abstraction, encapsulation, inheritance, and polymorphism along with the celebrated SOLID principles already offer a blueprint for object-oriented design. So what else is offered by DDD?

It is true that there are several principles and patterns already in vogue. However, in practice, people often flout most of these principles and patterns, unintentionally. Let’s take an example. The principle of encapsulation clearly says that the state properties of an object and the behavioural methods on those properties must be grouped together within the object itself in such a way that no outsider can directly access the state of the object. However, most of the objects we design either contain only state properties or only behavioural methods. The DDD attempts to correct it.

We often design entities and service objects with different responsibilities. An entity consists of just properties that are mapped to the fields of database records. And a service object holds the business logic to be applied to the entities. In fact, JPA and Spring Framework on the Java platform offer two popular annotations @Entity and @Service to clearly mark the classes. And this practice is working. What is wrong with it?

Neither the Spring Framework nor the annotations are wrong. It is the practice of thinking from the perspective of database design that is wrong. Indirectly, we are designing the objects depending on the database technology that we choose. We freeze the database architecture, the schema, and the queries early in the design. The problem with this approach is that the rest of the system is designed at the mercy of what is offered by the database layer. Everything is retrofitted into the frozen database layer. It makes the system too rigid for a change. And, as you know, any system that is not agile enough to absorb the change obsoletes quickly.

The buck does not stop even there. The project teams often choose a platform, besides the database, before designing the objects in the business layer. For instance, the way a Python developer designs the objects is different from the way a Java developer designs them. Another disturbing observation is that even on a single platform like Java, the objects designed for the Jersey framework look completely different from the objects designed for the Spring Framework. Objects are supposed to reflect real-world entities, isn’t it! However, in practice, it is the choice of database, platform, or frameworks that is driving the design. This is wrong because a rigid monolith system cannot respond to changes in business requirements. The DDD attempts to put the domain in the front seat, not the technology.

This looks reasonable. However, most of the people who are involved in software development are more technology experts, not domain experts. Isn’t this an obstacle to adopting DDD?

This is one of the anomalies in the industry. The teams that gather the system requirements are often ignorant of the technology, and the teams that design and build the system solutions are ignorant of the business domain. It is a common practice for a young engineer to jump from one team to another for career growth. This often results in jumping from one domain to another domain itself. It is no surprise to see a designer moving from the financial domain to the e-commerce domain. Though this gives exposure to various domains, the person will not gain sufficient expertise in any of the domains. Ultimately, such a designer gets carried away by the technology he or she knows, rather than by the nuances of the actual domain to which the system belongs.

The DDD strongly vouches for domain expertise. The overall design of the system, granular design of the objects, and their interactions should reflect the corresponding domain. How the teams gain such expertise is left to the individuals and the organisations.

Still, it is inevitable to have separate teams for product marketing and engineering. Does DDD have any solution to iron out the communication gap between the teams?

The DDD suggests that the engineering team should refrain from technical jargon. Instead, the teams are advised to develop a consistent vocabulary with a strong affinity to the domain. The vocabulary is referred to as ubiquitous language. All the stakeholders of the team are expected to use the same language. Without such a language, a specific word means many things to many people.

For instance, an e-commerce system consists of a subscriber object, a database table named customer, and a label on the user interface that reads as the user. Though these look as if they are three different entities from the outset, they may all actually refer to just one entity. Instead, all the stakeholders should stick only to one word. And that word should come from the business. If that specific e-commerce company refers to them as customers, then the word customer should be chosen, nothing else. It is even better to maintain a dictionary of the vocabulary.

In summary, DDD advises modelling the objects that correctly map to the actual business domain and using the language that the business uses. In a way, it fits into the Onion Model. Isn’t it?

The Onion Model consists of a domain layer, service layer, application layer, and infrastructure layer. The domain layer comprises technology-agnostic objects. The design of this layer is purely driven by the domain, nothing else. A domain object consists of both state and behaviour. However, there are places where more than one domain object needs to be involved in a business workflow. The service layer consists of objects that deal with more than one such domain object. The application layer acts as the gateway to the rest of the world. This is the layer where requests and responses are handled. The infrastructure layer is largely specific to the technology. It consists of databases, messaging systems, test environment, user interface, etc. It is natural that the DDD approach and the Onion Model go hand-in-hand.

However, DDD is not just limited to it. The DDD strongly advises decomposing a monolith domain into smaller sub-domains, and to deal with each of the sub-domains independently. In other words, the Onion Model is applied for each of the sub-domains.

If the sub-domains are handled independently, how are the domain objects that belong to more than one sub-domain handled? Are the objects duplicated or is the effort duplicated?

There are different strategies to handle such scenarios. However, it all boils down to the way the domain is decomposed into sub-domains. Often, the sub-domains are aligned to the lines of business. For instance, the catalogue service, the order service, and the fulfilment service are some of the probable sub-domains of an e-commerce domain. There are also secondary sub-domains that are not directly connected to the business, but are important, such as accounts. And we have sub-domains that are technology-specific, like messaging systems. A careful demarcation of the sub-domain boundaries is vital to get some benefit out of DDD.

Who does the domain decomposition? It looks as if it is beyond the scope of a software designer.

Yes. The domain decomposition is the responsibility of the management team. The senior solution architects are usually part of such a management team. Once the decomposition is finalised and sub-domains are identified, the engineering teams take over the responsibility to model and implement them. Speaking technically, instead of delivering one single monolith enterprise system, the engineering department delivers a set of smaller services which are now popularly known as microservices.

Does this mean that there will be no overlap between two sub-domains?

The degree of overlap between sub-domains is directly related to the nature of the business and the nature of decomposition. At one extreme, there is no overlap, which is desired. At another extreme, there is a lot of overlap that defeats the very purpose of decomposition. Both are very rare. Often, we see sub-domains with a small degree of overlap. Still, teams are advised to deal with their sub-domains independent of others, even if it results in duplicate effort. In the long run, a set of loosely coupled services are more beneficial.

Other than high-level concepts and best practices, does DDD offer any handy patterns that are easy to follow and replicate?

Of course, yes! Entities, value objects (VOs), data transfer objects (DTOs), aggregates, and repositories are very popular though a good number of people may not really be aware of the fact that they are DDD patterns. An entity is a domain object that can be uniquely identified, whereas a VO is a domain object that does not have a unique identity. A DTO is an object that is transferred across the domain layer. An aggregate is a cluster of domain objects with a root, and a repository is an abstraction of the aggregate store.

Is the entity of DDD the same as the entity of ORM, E-R models, etc?

They are not exactly the same. An entity in DDD is not necessarily associated with any persistence. They may also exist only during runtime. The point is that any domain object that is uniquely identified is called an entity. In other words, the state of an entity can change over a period of time, but it can still be identified uniquely. For example, an order in an e-commerce system is identified by something like order-id, though the value of the order and status of the order keeps changing.

An employee with an employee-id, a customer with a customer-id, an account with account-number, and a book with ISBN are all entities, then! They may or may not be persisted. When persisted, they may be mapped to a record in the RDBMS, or a document in NoSQL, etc. Entities that have relations with other entities form aggregates. Can the entities also have behaviour?

Of course, they can! Entities are first-class domain objects. They can have behaviour besides the state. For example, an order object may have operations like applyDiscount(), cancel(), deliver(), etc. For many, this looks a bit odd since somehow they are under the impression that an entity is just a database record without any operations. And that is a misplaced understanding.

Well, a domain object with a unique identity is an entity, and a domain object without a unique identity is a value object. Is that right?

That’s not the way to look at it. A value object is identified by looking at the whole of the object, not just one property. For example, two event objects in a UI system are indistinguishable if the type, time, and coordinates of the event are exactly the same. The value objects must be immutable, since any change in the value of the object makes it unrecognisable. Usually, it is encouraged to model properties of primitive types like phone numbers, email, addresses, etc, as value objects. Like the entities, the VOs also may or may not be persisted.

How are the entities and value objects persisted? Do they use data access objects?

One of the hallmarks of the DDD is that the domain model is technology-agnostic. In other words, the objects in the domain layer do not really know about the outside world. Even the storing and retrieving of the domain objects is done through an abstraction called repository. A repository gives an impression as if it is in-memory storage of the domain objects. Some of the usual methods of a repository include save(), find(), update(), delete(), etc.

Unlike DTOs, which are more inclined towards databases, repositories may be backed by anything such as databases, file systems, REST endpoints, etc. A notable point is that the repository interface belongs to the domain layer whereas its implementation belongs to infrastructure. Anything that is related to the infrastructure layer is expected to be pluggable.

And finally, the data transfer objects, as the name suggests, are special kinds of objects that are meant for request and response payloads. They do not belong to the domain layer; instead, they are useful in the application layer. But, what is the need for DTOs? Why can’t entities and VOs be used for transferring the data?

The reason is self-explanatory. The entities and value objects are domain objects. A domain object is useful and relevant within its own domain or sub-domain. Exposing the domain model outside of the domain creates a needless coupling between the domain and the outside world. For instance, the name of a customer might be modelled as a value object within the domain model with FirstName and LastName as the fields. However, for the outsiders it is immaterial. They may see the name as just a character sequence. A DTO does the job of converting a name object into a string and vice versa.

Illustration
Let us look at some examples to understand the domain-driven design patterns. These examples have nothing to do with any specific programming language. Consider them as a pseudo-language to convey the idea.

Following are the classes Order and OrderManagement:

class Order {
  private double amount;
  private Date date;
  private String status;
  ....
  getAmount();
  getDate();
  getStatus();
  setAmount(double);
  setDate(Date date);
  setStatus(String status);
}
class OrderManagement {
  public void cancel(Order order) {
    apply business rules;
    order.setStatus(“cancelled”);
  }
  ....
}

The problem with the above design is that the status property of Order is being manipulated by an outsider OrderManagement. Where is the encapsulation? This is more like global variables and global functions with a bit of sugar-coating in the form of setters and getters. The status of the Order must be handled by the Order itself since it is its state property. The following is a better version of the design:

class Order {
  private double amount;
  private Date date;
  private String status;
  ....
  getAmount();
  getDate();
  getStatus();

  setAmount(double);
  setDate(Date date);

  public void cancel() {
    apply business rules
    status = “cancelled”;
  }
}

The above Order class does not expose any direct setters. However, it should not be understood as if setters are bad. The object is still mutable, but through more meaningful methods like cancel().

The next enhancement to the above class is to add an identity. Remember that it is not a technically-driven design decision. In the e-commerce domain, every order is given a unique identity. We are merely mapping to the real world.

class Order {
  private int id;
  private double amount;
  private Date date;
  private String status;
  ....
  getId();
  getAmount();
  getDate();
  getStatus();

  setId(int id);
  setAmount(double);
  setDate(Date date);

  public void cancel() {
    apply business rules
    status = “cancelled”;
  }
}

A blunder in the above design is that the property ID is made mutable. Identities are never mutable. The setId() method is meaningless. The Order, being an entity, must be associated with any identity at the time of creation and must never be changed after that.

class Order {
  private int id;
  private double amount;
  private Date date;
  private String status;
  ....
  getId();
  getAmount();
  getDate();
  getStatus();

  setAmount(double);
  setDate(Date date);

  constructor() {
    generate and assign id;
  }

  constructor(int id) {
    assign id;
  }
  public void cancel() {
    apply business rules
    status = “cancelled”;
  }
}

So far, so good. The next betterment is to replace the primitive types with value objects wherever they are appropriate. For instance, the above Order object accepts any double as the order amount. How is that acceptable? Isn’t it reasonable to say that the order amount must be above zero? Let’s refactor.

class Money {
  private double value;
  getValue();
  setValue(double);
}

class Order {
   private int id;
   private Money amount;
   ...
}

This looks a bit more reasonable. However, we again committed a blunder. Isn’t Money a value object? Can a value object be modelled as mutable? No, right? Here is the correct version:

class Money {
  private double value;
  getValue();
  constructor(value) {
   validate the value;
   assign value;
  }
}

And, finally, a repository for Order:

interface OrderRepository {
  save(order)
  load(id);
  delete(id);
  loadAll();
  ...
}

There can be several implementations like JpaOrderRepostory, MongoOrderRepository, RedisOrderRepository, and so on.

UMS as microservices
The user management system that we have been exploring in the last three parts of this series of articles is a single monolith system that offers multiple features. By applying the principles of domain-driven design, it can be decomposed into a set of microservices. And that is what we are going to see in the next part.

LEAVE A REPLY

Please enter your comment!
Please enter your name here