Software Engineering: A Modern Approach
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 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:
They facilitate effective communication between developers and domain experts.
They provide names for source code elements, such as classes, methods, attributes, packages, modules, database tables, and API routes.
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:
- A
Book
can have one or moreCopy
instances. - A
Reservation
can be made for up to threeBook
instances. - There are three types of
User
:Student
,Instructor
, andExternalUser
. - The library’s
Collection
is a set ofBook
objects.
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:
- Entities
- Value Objects
- Services
- Aggregates
- Repositories
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.
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:
- Five terms from the ubiquitous language
- Three entities
- Three value objects
- One aggregate, including the root and the internal objects
- Two methods of a service
- 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);
}
To which type of DDD class does this method likely belong?
How would you classify the
User
andBook
classes?To which type of class do the methods
findUserByID
,findBookByID
, andcreateIssueRecord
most likely belong?If
IssueRecord
(a class) has a set ofIssueItem
(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.