Book cover
All rights reserved. Version for personal use only.
This web version is subjected to minor edits. To report errors or typos, use this form.

Home | Dark Mode | Cite

Software Engineering: A Modern Approach

Marco Tulio Valente

1 Domain-Driven Design (DDD): A Summary

The heart of software is its ability to solve domain-related problems for its user. – Eric Evans

1.1 Introduction

Domain-Driven Design (DDD) is a set of software design principles that were proposed in 2003 by Eric Evans in a book with the same name. The principles aim to facilitate the implementation of software systems with a design based on concepts closely aligned with a business domain.

The domain of a system refers to the business problem it aims to solve. In this article, we’ll illustrate DDD using a library management system as an example. Therefore, the problem of managing a library constitutes the domain of our motivating system.

DDD advocates that developers should have a deep understanding of the domain they’re working on. This understanding should be acquired through frequent conversations and discussions with domain experts. Consequently, the software’s design should reflect its domain rather than being dictated by a specific technology. In other words, the design is driven by the domain, not by frameworks, architectures, or programming languages.

DDD also advocates that this separation between domain and technology should be expressed in the software architecture. For this purpose, patterns such as Layered Architecture, Clean Architecture, or Hexagonal Architecture can be used.

Before proceeding, it’s important to mention that the usage of DDD makes more sense in software for complex domains where the business rules are challenging to understand and to implement.

1.2 Ubiquitous Language

Ubiquitous Language is one of the most important concepts of DDD. It refers to the terms that should be fully understood by both domain experts and developers.

For a software project to succeed, DDD argues that these two roles—domain experts and developers—should speak the same language, which constitutes the ubiquitous language of the system. This idea is illustrated in the following figure:

The ubiquitous language represents the shared knowledge between domain experts and developers.

The figure makes clear that there are terms that only domain experts know. On the other hand, other terms, which have a technical nature, are only known by developers. Finally, there is a set of terms that both professionals should know, which constitutes the ubiquitous language of the system.

The terms of the ubiquitous language are used with two purposes:

  • To enable fluid communication between developers and domain experts.

  • To name source code elements, such as classes, methods, attributes, packages, modules, database tables, API routes, etc.

Besides defining the ubiquitous language terms, it is important to define the relationships and associations that exist among them.

Example: In our library system, the ubiquitous language includes terms such as the following:

Book, Copy, ISBN, Librarian, User, Collection, Reservation, Loan, Catalog

Other terms are only known by developers, such as proxy, observers, cache, layers, routes, among others. There are also terms that are known only by librarians, such as alternative ISBN types.

Finally, we should define the relationships and associations between these terms, as exemplified next:

  • A Book can have one or more Copy.
  • A Reservation can be made for up to three Book.
  • There are three types of User: Student, Instructor, and ExternalUser.
  • The library’s Collection is a set of Book.

To document these relationships, UML Class Diagrams can be used, as we studied in Chapter 4. However, these diagrams should be simple and light. For example, they do not need to include all attributes and methods of each class.

1.3 Domain Objects

DDD was proposed for designing object-oriented systems. Therefore, when designing these systems, certain types of objects are commonly used. Among them, DDD defines the following:

  • Entities
  • Value Objects
  • Services
  • Aggregates
  • Repositories

These domain objects should be viewed as the conceptual tools that developers should use to successfully design a given system. Therefore, they are also called the building blocks of DDD. Next, we will comment on each of them.

Entities and Value Objects

An entity is an object that has a unique identity, distinguishing it from other objects of the same class. For instance, each User in our library is an entity, identifiable by their registration number.

In contrast, value objects lack a unique identifier and are instead defined by their state, i.e., the values of their attributes. For example, a User’s Address is a value object. If two Address objects share identical values for attributes such as street, number, city, and zip code, they are considered identical.

Additional examples of value objects include Currency, Date, Phone, Email, Hour, Color, and more.

Why distinguish between entities and value objects? Entities are more important objects, demanding careful planning for saving and retrieving them from a database. Moreover, developers should pay attention to the life cycle of entities and carefully understand the rules governing their creation and removal. For instance, in our library system, a User cannot be removed if they have a pending Loan.

On the other hand, value objects are simpler by definition. They should ideally be immutable, meaning once created, their internal values cannot be changed. To modify a User’s Address, for example, we must abandon the old object and create a new one with the updated Address. The advantages of immutable objects have been previously discussed in Chapter 9.

It’s worth noting that some programming languages offer syntactic support for implementing value objects. For instance, in Java, they can be implemented using records.

Services

There are numerous domain operations that don’t fit into entities or value objects. Consequently, it’s advisable to create dedicated objects to implement these operations. In DDD terminology, these objects are referred to as services. But in some systems, they may also be called managers or controllers.

The signature of services usually refers to entities and value objects. However, service objects should be stateless, meaning they lack attributes and only implement methods.

Services are typically implemented as singletons, ensuring a single instance throughout the system’s execution. For further details on this design pattern, please refer to Chapter 6.

Example: In our library system, we might have a service providing the following operations:

class LoanService {
  void borrowBook(User user, Book book) {...} 
  void returnBook(User user, Book book) {...}
  ...
}  

In the first operation, a User borrows a Book. In the second one, the User returns a borrowed Book. Since both operations are not specific to either User or Book, DDD suggests implementing a separate service object to handle them.

Aggregates

Aggregates are collections of entities and value objects. That is, sometimes it does not make sense to reason about entities and value objects individually. Instead, we have to group such objects to have a consistent view of the domain we are modeling.

An aggregate has a root object, which must be an entity. Externally, the aggregate is accessed from this root. The root, in turn, references the internal objects of the aggregate. Thus, these internal objects are not visible to the rest of the system, that is, only the root can reference them.

Since they are a coherent unit, aggregates are persisted in conjunction in databases. The deletion of an aggregate, from the main memory or from a database, implies the deletion of the root and all internal objects.

It is also interesting to have methods to create aggregates, which are called factories. As we also studied in Chapter 6, such methods are implementations of the design pattern of the same name.

Example: In the library system, a Loan has a User (which is an entity) and a list of LoanItem. Each LoanItem contains information about a certain Book that was borrowed.

Therefore, Loan and LoanItem form an aggregate, as shown in the next figure. That is, they are a single logical structure. Loan is the root of the aggregate, and LoanItem is the class of an internal object, which cannot be manipulated without accessing the root first.

Aggregate example

Note that LoanItem references Book, but that latter class is not part of the aggregate, as its objects have a life of their own, i.e., they exist independently of whether they are borrowed or not. The same applies to User.

Repositories

To implement certain services, we first need to get references to entities or aggregates.

For instance, consider a service that lists the Loan made by a given User. To implement this service, we cannot assume that all Loan aggregates are available in the main memory. Actually, in any real system, they are stored in a database.

Nevertheless, in DDD, we want to keep the developers focused on the domain, rather than having their attention diverted, at certain times, to a data storage technology.

Thus, a repository is an object that shields developers from concerns related to database access. In other words, a repository offers an abstraction for the database used by the system. They allow manipulating domain objects as if they were lists (or collections) stored in main memory. The implementation of the repository takes care of reading and saving these lists in a database, for example using SQL.

Example: In the library system, there is a repository with methods for retrieving Loan data from a database:

class LoanRepository {
  List<Loan> findUserLoans(User user) {...}
  List<Loan> findLoansByDate(Date start, Date end) {...}
  List<Loan> findOverdueLoans() {...}
  ... 
}

In addition to find* methods, a repository can implement methods to save, update, and remove objects:

class LoanRepository {

  // find* methods (see above)
  
  void save(Loan loan) {...}
  void update(Loan loan) {...}
  void remove(Loan loan) {...} 
}

1.4 Bounded Contexts

Over time, software inevitably grows in complexity and scope. Thus, it is unrealistic to expect that large and complex organizations will have a single domain model.

Instead, it’s natural for these organizations to have systems serving users with diverse profiles and requirements. Particularly, this diversity complicates the definition of a single ubiquitous language. Then, the solution lies in decomposing these complex domains into smaller and more manageable ones, which DDD refers to as Bounded Contexts.

Example: Let’s consider a scenario where our library expands to include a financial department. This department has specific needs that demand a separate domain with its own language. For instance, within this finance domain, the User class might be renamed to Client and could demand additional attributes to accommodate financial-specific requirements.

1.5 Conclusion

In a reference material he wrote in 2014, Eric Evans defined DDD as follows:

Domain-Driven Design is an approach to the development of complex software in which we: (1) Focus on the core domain; (2) Explore models in a creative collaboration of domain practitioners and software practitioners; (3) Speak a ubiquitous language within an explicitly bounded context.

The ubiquitous language should also be reflected in the source code of the system, encompassing the naming of variables, parameters, methods, classes, packages, and so on. Moreover, in DDD projects, it’s recommended to have the following types of objects: entities, value objects, services, aggregates, and repositories.

Exercises

1. Suppose you work at a company that has an online food delivery app. You are in charge of designing the domain layer of this app. To accomplish this, you decided to use DDD. Describe then:

  1. Five terms from the ubiquitous language
  2. Three entities
  3. Three value objects
  4. One aggregate, including the root and the internal objects
  5. Two methods of a service
  6. Two methods of a repository

2. Assume an e-commerce system, with the following classes: Order, OrderItem, and Product. Draw a class diagram that represents the relationships between these classes. Which classes constitute an aggregate? Which class is outside of the aggregate and why?

3. Suppose the following method of the library system discussed in the article:

void lendBook(int userId, int bookId) {
  User user = retrieveDataFromUser(userId);
  Book book = retrieveDataFromBook(bookId);
  user_ok = checkIfUserIsUpToDateWithTheLibrary(user);
  book_available =  checkIfBookHasAvailableCopies(book);
  if (user_ok && book_available)
     createBookLoanForUser(user, book);
}
  1. To which type of DDD class does the lendBook() method belong?

  2. How do you classify the User and Book classes?

  3. The methods retrieveDataFromUser, retrieveDataFromBook, and createBookLoanForUser belong to which type of class?

  4. Suppose that Loan (a class) has a set of LoanItem (another class). What type of structure do these two classes form?

4. After learning DDD, a developer decided to structure a complex system as follows. Basically, the files that implement each of the domain objects advocated by DDD are located in the same packages (or, if you prefer, modules or folders). Is this decision recommended or not? In other words, is it consistent with the principles of DDD? Justify your answer.

   Root
      |__ Entities
      |   | files implementing entities
      |
      |__ Aggregates
      |   | files implementing aggregates
      |
      |__ ValueObjects
      |   | files implementing value objects
      |
      |__ Services
      |   | files implementing services
      |
      |__ Repositories
      |  | files implementing repositories