Book cover

Web Version | 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 Eric Evans introduced in 2003 in his book of the same name. These principles aim to facilitate the implementation of software systems with a design closely aligned with concepts from the 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. The domain of this system is the task of managing a library.

DDD emphasizes that developers should have a deep understanding of the domain they’re working on. This understanding is gained 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 essence, the design is driven by the domain, not by frameworks, architectures, or programming languages.

DDD advocates for expressing the separation between domain and technology in the system’s architecture. To achieve this, patterns such as Layered Architecture (studied in Chapter 7), Clean Architecture (covered in another article), or Hexagonal Architecture (also discussed in an article) can be used.

Before proceeding, it’s important to note that DDD is most beneficial for software with complex domains where the business rules are challenging to understand and implement.

1.2 Ubiquitous Language

DDD advocates that for a software project to succeed, domain experts and developers must share a common language, which becomes the ubiquitous language of the system. This concept is illustrated in the following figure:

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

The figure illustrates that some terms are known exclusively by domain experts, while others, which are technical in nature, are understood solely by developers. The intersection of these knowledge sets forms the ubiquitous language of the system, comprising terms that both groups understand and use.

The terms of the ubiquitous language serve two purposes:

In addition to defining the ubiquitous language terms, it is important to define the relationships and associations among them.

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

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

Some terms are known exclusively by developers, such as proxy, observers, cache, layers, and routes, among others. Conversely, there are terms known only by librarians, such as alternative ISBN types.

To complete the ubiquitous language, we must define the relationships and associations between these terms, as illustrated below:

To document these relationships, we can use UML Class Diagrams, as discussed in Chapter 4. However, in the context of DDD, these diagrams should be kept simple and concise. Specifically, they should focus on key concepts and relationships rather than include all attributes and methods of each class.

1.3 Domain Objects

DDD was developed for designing object-oriented systems. When implementing these systems, the following types of objects are commonly used:

These domain objects serve as the conceptual tools that developers use to successfully design a given system. They are also referred to as the building blocks of DDD. In the following sections, we will explore 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 example, each User in our library system is an entity, uniquely identifiable by its registration number.

In contrast, value objects lack a unique identifier and are defined by 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 equivalent.

Additional examples of value objects include Currency, Date, Phone, Email, Hour, Color, and similar conceptual elements.

Why distinguish between entities and value objects? Entities are central 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 borrowed Book.

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 discard the old object and create a new one with the updated Address. The advantages of immutable objects have been previously discussed in Chapter 9.

Some programming languages offer syntactic support for implementing value objects. For instance, in Java, they can be implemented using records.

Services

Many domain operations don’t fit neatly 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. In other architectural styles, they may also be called managers or controllers.

The methods of services usually operate on entities and value objects. However, service objects should be stateless, meaning they don’t have attributes and only implement methods.

Services are typically implemented as singletons, ensuring a single instance exists 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 BorrowingService {
  void borrowBook(User user, Book book) {...} 
  void returnBook(User user, Book book) {...}
  ...
}  

In the first operation, a User borrows a Book. In the second operation, the User returns a borrowed Book. Since both operations involve the interaction between User and Book but are not intrinsic to either entity, DDD suggests implementing a separate service object to handle them.

Aggregates

Aggregates are collections of entities and value objects. In some cases, it is more appropriate to consider groups of objects together to maintain a consistent view of the domain, rather than reasoning about objects individually.

An aggregate has a root object, which must be an entity. Externally, the aggregate is accessed through 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, as only the root can reference them.

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

To create aggregates, it’s often useful to employ methods known as factories. As we previously discussed in Chapter 6, these methods are implementations of the design pattern of the same name.

Example: In our library system, an IssueRecord is an entity that stores information about the books issued to a given User, including a list of IssueItem objects. Each IssueItem contains, for example, the dueDate and the returnDate of the issued Book.

Thus, IssueRecord and IssueItem form an aggregate, as shown in the following figure. They constitute a single logical structure. IssueRecord is the root of the aggregate, and IssueItem represents the class of an internal object, which cannot be manipulated without accessing the root first.

Aggregate example

It’s important to note that IssueItem references Book, but the 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 issued or not. The same applies to User.

Repositories

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

For instance, consider a service that lists the books issued to a given User. To implement this service, we cannot assume that all IssueRecord aggregates are available in main memory. Instead, in any real system, they are stored in a database.

However, in DDD, we want to keep developers focused on the domain, rather than having their attention diverted by concerns related to 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. Repositories allow developers to manipulate domain objects as if they were collections stored in main memory. The implementation of a repository takes care of reading and saving these objects in a database, typically by using SQL.

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

class BookRepository {
  List<Book> findBookByISBN(String isbn) {...}
  List<Book> findBookByTitle(String title) {...}
  List<Book> findBookByAuthor(String author) {...}
  List<Book> findBooksAcquiredInDateRange(Date start, Date end) {...}
  ... 
}

In addition, this repository can implement methods to insert, update, and remove Book objects:

class BookRepository {

  // methods above
  
  void insertBook(Book book) {...}
  void updateBook(Book book) {...}
  void removeBook(Book book) {...}
}

All methods in BookRepository serve to shield developers from having to know the underlying database structures that store data about the library’s book collection.

1.4 Bounded Contexts

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

Rather, it’s natural for these organizations to have systems serving users with diverse profiles and requirements. In particular, 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 necessitate a separate domain with its own language. For instance, within this finance domain, the User class might be renamed to Client and might require additional attributes to accommodate finance-specific requirements.

1.5 Conclusion

In a reference document 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 be reflected in the source code of the system, including 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 have decided to use DDD. Describe:

  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. Consider 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 form an aggregate? Which class is likely outside of the aggregate and why?

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

void borrowBook(int userId, int bookId) {
  User user = findUserByID(userId);
  Book book = findBookByID(bookId);
  user_ok = checkIfUserIsUpToDate(user);
  book_available =  checkIfBookHasAvailableCopies(book);
  if (user_ok && book_available)
     createIssueRecord(user, book);
}
  1. To which type of DDD class does this method likely belong?

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

  3. To which type of class do the methods findUserByID, findBookByID, and createIssueRecord most likely belong?

  4. If IssueRecord (a class) has a set of IssueItem (another class), what type of structure would these two classes typically form?

4. After learning DDD, a developer decided to structure a complex system as follows. 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 aligned with DDD principles? 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

Check out the other articles on our site.