Domain-Driven Design, The Basics

The Domain-Driven Design (DDD) is a software architecture approach that focuses on the core domain of the application. It’s a way to tackle complexity by connecting the implementation to an evolving model of the core business concepts.

One of the most important steps in this process is to gather information from the domain experts. The idea of this post is to show you the fundamental concepts behind this powerful approach.

What is a Domain?

There are several definitions of domain in the literature. The most popular one is the following:

“an area of knowledge or activity; especially one that somebody is responsible for”. Oxford Dictionary,

In this context, the domain is the subject area to which the user applies a program. It is the business you are in.

A quick example of a domain can be:

  • For Banking Services: A Bank can have several services like Loans, Credit Cards, Accounts, Employees, etc. The entire banking operation is the domain.
  • For E-Commerce: A company can have services like Sales, Inventory, Shipment, and Payments. The entire process of selling goods online is the domain.

Basically, the domain is the field or area in which the company operates.

What is a Subdomain?

A domain is rarely a single, monolithic block. It’s better to break it down into multiple parts, or subdomains. This helps manage complexity.

Types of subdomains:

  1. Core Domain: This is the heart of the business. It’s what makes the company special and provides a competitive advantage. For a veterinary business, the core domain might be appointments, surgery scheduling, and patient treatment plans. You should invest your best talent here.
  2. Supporting Subdomain: These are parts of the business that are necessary but not a competitive differentiator. They are typically custom-built because no off-the-shelf solution exists. For our veterinary business, managing the specific stock of specialized animal medicine could be a supporting subdomain.
  3. Generic Subdomain: These are “solved problems” common across many businesses. Think of identity management (logging in users) or sending emails. The best strategy here is to buy a solution or use an open-source library, not build it yourself.

It’s always necessary to work with a domain expert to understand the domain and its subdomains.

So, in the end, What is Domain-Driven Design?

Domain-Driven Design is a set of principles and patterns that help you build software that handles complex business logic effectively.

In his book Domain-Driven Design, Eric Evans describes DDD as a strategy to distill complex problems into manageable models. The idea is to develop software where the code is based on an evolving business model. This model is the representation of:

  • Structure
  • Activities
  • Processes
  • Actors
  • Interactions

DDD helps us to:

  1. Center the business processes in the design and development process. This is achieved by modeling the domain and every subdomain, rather than defining the technological stack at the beginning of the project.
  2. Break the domain into smaller pieces, called subdomains, to build the best model for the business.
  3. Be agile-friendly. The model can start small (as an MVP that everyone understands) and evolve with each iteration as the team’s understanding of the domain deepens.

DDD contains two approaches. The strategic approach focuses on the big picture, defining the landscape of the domain with patterns like Bounded Contexts and the Ubiquitous Language. The tactical approach is about the design of the objects within a single model, using components like the Aggregate pattern or the Repository pattern.

In summary, DDD helps us develop applications for complex business domains and represent this process in a flexible model. When using DDD, you get software that is adaptable to the needs of the company, because the business logic lives in a well-organized, isolated place.

From Theory to Practice: Tactical DDD Patterns in Code

We’ve talked about the “tactical approach” and patterns like Aggregates. Now, let’s see what this looks like in actual code. We’ll use a C# example from an application, where users can track movies they’ve watched and want to watch.

The goal here is to create a rich domain model, not an anemic one. An anemic model has objects that are just bags of data with public getters and setters, and all the business logic lives elsewhere (in “service” classes). A rich model encapsulates both data and behavior, ensuring objects are always in a valid state.

The Building Blocks: Entities and Aggregates

  • Entity: An object defined not by its attributes, but by a thread of continuity and identity. A User is an entity; even if their email changes, they are still the same user because their UserId is the same.
  • Aggregate: A cluster of associated objects that we treat as a single unit for the purpose of data changes. An aggregate has a root and a boundary.
  • Aggregate Root: A specific entity within the aggregate that is the single entry point for all modifications to any object within that aggregate. External objects can only hold references to the aggregate root. This protects the business rules (invariants) of the aggregate.

In our example, the User is the Aggregate Root. A user owns their list of movies (UserMovie) and their Comments. To add a comment, you must go through the User object. You cannot bypass it and create a comment directly.

Case Study: “My Movies Checklist” Domain Model

1. Base Entity Class

To centralize the concept of identity, we can create a base class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Movies.Domain/Common/Entity.cs
public abstract class Entity<TId> where TId : notnull
{
public TId Id { get; protected set; }

protected Entity(TId id)
{
Id = id;
}

// Override equality to compare by identity
public override bool Equals(object? obj)
{
return obj is Entity<TId> entity && Id.Equals(entity.Id);
}

public override int GetHashCode()
{
return Id.GetHashCode();
}
}

2. The User Aggregate Root

Notice the private setters and the private list of _userMovies. Changes to the user’s movie list can only happen through methods like AddToWatchlist and MarkAsSeen, which contain the business logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Movies.Domain/Entities/User.cs
public class User : Entity<long>
{
public string Email { get; private set; }
public string Auth0Id { get; private set; }

// The list is private. Modifications must go through methods.
private readonly List<UserMovie> _userMovies = new();
// A read-only view is exposed for querying.
public IReadOnlyCollection<UserMovie> UserMovies => _userMovies.AsReadOnly();

// Factory method to ensure valid creation
public static User Create(string email, string auth0Id)
{
if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Email is required.");
return new User(0, email, auth0Id);
}

// --- Domain Behavior ---
public void AddToWatchlist(Movie movie)
{
// Business Rule: Don't add if it's already there.
if (_userMovies.Any(um => um.MovieId == movie.Id))
{
return;
}

var userMovie = UserMovie.CreateForWatchlist(this.Id, movie.Id);
_userMovies.Add(userMovie);
}

public void MarkAsSeen(Movie movie, decimal score, string commentText)
{
var userMovie = _userMovies.FirstOrDefault(um => um.MovieId == movie.Id);

if (userMovie is not null)
{
userMovie.MarkAsSeen();
}
else
{
userMovie = UserMovie.CreateAsSeen(this.Id, movie.Id);
_userMovies.Add(userMovie);
}

// The Aggregate Root delegates the comment creation to the child entity.
userMovie.AddComment(score, commentText);
}

// Private constructor for EF Core and factory method
private User(long userId, string email, string auth0Id) : base(userId) { /*...*/ }
private User() : base(0) { }
}

3. The UserMovie and Comment Child Entities

These entities have internal constructors and factory methods. This is crucial: it means they can only be created by other classes within the same project (our domain project), effectively enforcing that the User aggregate root controls their lifecycle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Movies.Domain/Entities/UserMovie.cs
public class UserMovie : Entity<long>
{
public long MovieId { get; private set; }
public long UserId { get; private set; }
public bool SawIt { get; private set; }

private readonly List<Comment> _comments = new();
public IReadOnlyCollection<Comment> Comments => _comments.AsReadOnly();

// Internal factory, can only be called by the User (Aggregate Root)
internal static UserMovie CreateAsSeen(long userId, long movieId) { /*...*/ }

internal void AddComment(decimal score, string text)
{
// Business Rule: Can only comment on a movie that has been seen.
if (!SawIt)
{
throw new InvalidOperationException("Cannot comment on an unwatched movie.");
}
var comment = Comment.Create(this.Id, score, text);
_comments.Add(comment);
}
// ... more methods and private constructor
}

// Movies.Domain/Entities/Comment.cs
public class Comment : Entity<long>
{
public long UserMovieId { get; private set; }
public string Text { get; private set; }
public decimal Score { get; private set; }

// Internal factory enforces creation through UserMovie
internal static Comment Create(long userMovieId, decimal score, string text)
{
// Business Rule (Invariant): Score must be between 0 and 10.
if (score < 0 || score > 10)
{
throw new ArgumentOutOfRangeException(nameof(score), "Score must be between 0 and 10.");
}
return new Comment(0, userMovieId, score, text);
}
// ... private constructor
}

By modeling our domain this way, we have gained clarity, robustness, and maintainability. The business logic is encapsulated where it belongs, and the domain protects its own integrity.

Downsides of DDD

One of the biggest problems of starting with DDD is the learning curve. DDD includes many principles, patterns, and processes that can be difficult to grasp at first. DDD is not the best approach for small or simple projects because it can overcomplicate the development process. For a simple CRUD application, it is likely overkill.

Mapping the Domain into the model: Event Storming

How do you discover the events, commands, and aggregates we just modeled? A powerful technique is Event Storming.

This is a collaborative workshop where technical experts and domain experts work together. Most of the time, technical experts use tools like UML which domain experts might find difficult to understand. The idea is to create a session where both sides share the same language and build a model together.

The Event Storming meeting is a session that uses sticky notes to map out a business process. The idea is to create a detailed list of:

  • Domain Events: Something important that happened in the past (e.g., MovieMarkedAsSeen). These are orange sticky notes.
  • Commands: A user’s intention to do something (e.g., MarkMovieAsSeen). These are blue.
  • Actor: The person or system that executes a command. These are yellow.
  • Aggregate: The entity that processes a command and produces an event (e.g., User). These are larger yellow notes.
  • Read Model: The data an actor needs to make a decision (e.g., a list of movies to choose from). These are green.
  • Policies: Rules that trigger a command in response to an event (e.g., “When a UserRegistered event occurs, then execute the SendWelcomeEmail command”). These are purple.

All these elements help to visualize and understand all the pieces involved in a subdomain process, providing a blueprint for your domain model.

Useful Links