An introduction to Domain-Driven Design

Laurent Grima
Dec 6, 2018 · 7 min read
Photo by Pineapple Supply Co. on Unsplash

Domain-Driven Design is an approach to software development based on making your software deeply reflect a real-world system or process.

“Domain” in Domain-Driven Design officially refers to a “sphere of knowledge and activity around which the application logic revolves”. In other words, the “Domain” is what is commonly referred to as “business logic” in the software world.

In Domain-Driven Design, business logic is considered to be the heart of the software.

You’ll find in this article an introduction to Domain-Driven Design that mostly follows what is explained in Eric Evans’ book. Other implementations and vocabulary also exist, as well as similar architectures sharing the same principles, such as clean architecture and hexagonal architecture.

Is Domain-Driven Design for me?

Going for a Domain-Driven approach also means higher costs at first. Developers will first face a steep learning curve and managing the architecture will make things longer to build. For these reasons, Domain-Driven Design is not recommended for simple projects and unexperimented teams.

At Inato, Domain-Driven Design was a good match because we were beginning to find our code hard to test, hard to read from a functional perspective, and hard to extend when new use cases were coming up. The complexity of the product was going up quickly and we needed to make our code base architecture scale to handle it and ship value faster.

First, an example

A simple example of an application use-case implemented following the principles of Domain-driven design.

You can already see here that:

  • The code is very expressive, because the code can be read almost like plain English.
  • The code is easy to test (See how we would write a test here).
  • Concerns are separated, because this business code is independent from the way data is stored.

The basics of Domain-Driven Design

  • Separating the concerns into layers
  • Modeling the Domain
  • Managing the life-cycle of Domain objects

🍰 I. Isolating the domain: the layered architecture

Indeed, if the domain-related code is mixed with other code, it becomes rapidly very difficult to reason about.

The recommended architecture is made of 4 layers.

Dependencies between the 4 layers

User Interface (or Presentation Layer)

Application Layer

This layer is kept thin. It does not contain business rules or knowledge, but only coordinates tasks and delegates work to collaborations of domain objects in the next layer down.

It does not have state reflecting the business situation, but it can have state that reflects the progress of a task for the user or the program.

Domain Layer (or Model Layer)

State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure.

This layer is the heart of business software.

Infrastructure Layer

🌐 II. Domain modeling

The basic constraint is that the model must both help the implementation of features and represent real-life knowledge.

To enforce the representation of the domain inside the code, Domain-Driven Design also encourages the use of an “Ubiquitous Language”, which is shared between developers and business people.

👩‍🏫 1. Best practices to enforce healthy domain modeling

  • Commit the team to exercising that language relentlessly in all communication within the team and in the code.
  • Use the same language in diagrams, writing, and especially speech.
  • Resolve confusion over terms in conversation, in just the way we come to agree on the meaning of ordinary words.
  • When changes are made to the model, refactor the code (renaming classes, methods, modules, …) to conform to the new model.
  • Recognize that a change in the Ubiquitous Language is a also change to the model.
  • Conversely, developers need to realize that changing the code also means changing the model.
  • Domain experts (product people) should object to terms or structures that are awkward or inadequate to convey domain understanding; developers should watch for ambiguity or inconsistency that will trip up design.

🏗 2. Expressing the model: Building Blocks

  • Value Objects
  • Entities
  • Services
A picture I took from the book Domain-driven design by Eric J. Evans

Value objects

Value Objects are often passed as parameters in messages between objects. They are frequently temporary created for an operation and then discarded.

Best practices:

  • Treat the Value Object as immutable.
  • Don’t give it any identity and avoid the design complexities necessary to maintain Entities.
  • Ensure that the attributes that make up a Value Object form a conceptual whole.
An example of a Value Object

Entities

An example of an Entity

Services

An example of a domain Service

Modules

There should be low coupling between Modules and high cohesion within them, both code-wise and concept-wise:

  • There is a limit to how many things a person can think about at once (hence low coupling).
  • Incoherent fragments of ideas are as hard to understand as an undifferentiated soup of ideas (hence high cohesion).

Best practices:

  • Give the Modules names that become part of the Ubiquitous Language. Modules and their names should reflect insight into the domain.
  • When creating modules, favor conceptual clarity over technical convenience (if both are not achievable together).

🗃 III. Managing the life cycle of domain objects

The most important concepts for this are Aggregates and Repositories. Note: an Aggregate is always associated with one and only one Repository.

Aggregates

Aggregates add structure to the model by setting boundaries and providing a clear ownership for the objects they contain.

Best practices:

  • Cluster the Entities and Value Objects into Aggregates and define boundaries around each.
  • Choose one Entity to be the root of each Aggregate, and control all access to the objects inside the boundary through the root.
  • Allow external objects to hold references to the root only. This arrangement makes it practical to enforce all invariants for objects in the Aggregate and for the Aggregate as a whole in any state change.

⚠️ Even though aggregates help managing the life cycle by defining ownership and boundaries, they know nothing about the details of the infrastructure and belong in the domain layer.

Repositories

Repository interfaces are declared in the Domain Layer, but the repositories themselves are implemented in the Infrastructure Layer. This makes it easy to switch between different implementations of a repository without impacting any business code (for instance going from SQL to No-SQL storage, or writing in-memory implementations for faster tests).

Going further

There’s actually much more to it than this, so I really recommend to read one or two books before going at it in a production code base. Here are the best resources we have used so far at Inato to help us going with Domain-Driven Design:

inato

Thoughts & experiences from the Inato team