Domain-Driven Design: For People In a Hurry (with C# examples)
DDD is a fascinating topic that every developer should be familiar with
DDD - A (short) history lesson
Domain-driven design is a book originally written in 2004 by Eric Evans and is still in its first edition. Since then it has seen over 20 printings. I bought my copy in 2016 and it is often considered one of the “essential” books for a developer to read despite its age.
The book is about object-oriented programming and is full of ideas and concepts that may disrupt everything you thought you knew about structuring code and classes.
One of the biggest criticisms of the book I’d like to highlight though, is the lack of code examples. While having no code examples will make the book timeless (and can span multiple languages); it leaves a lot to interpretation.
I highly recommend reading the book but at 500+ pages, I can understand why you may want to get a shorter version. In fact there is a shorter PDF version that is available on the internet called “Domain-Driven Design Quickly”. This comes in at about 100-ish pages and is a distillation of the original book.
I’ve read both the PDF and original versions and I’d recommend them both. However I felt there should be an even shorter version with examples. This is that version.
A Quick Disclaimer
This is my interpretation of DDD in C#. It is only a matter of time before someone comes along and will disagree with some (or all of this). The point of this version is to expose you to the concepts so that you can adjust my interpretation to one that suits you.
The Essence of DDD: Word Soup
In my view DDD is all about encapsulation and keeping the integrity of the business model laid out in code. It is essentially inverting the control of your business logic from a loose model to a closely guarded model. There are some key buzzwords used in DDD that you’ll wanna be familiar with, let’s look at a few.
DDD espouses that techy and non-techy folks (in the same business domain) speak the same verbal language. This Ubiquitous Language avoids any sort of tribal translation needed from a stakeholder to a class/method in code. If it’s called a “whatcha-ma-call-it” in the real world, name your things accordingly in code.
Know what your domain does and doesn’t do. Carve up whose responsibility it is to do what. Let’s call that Bounded Context. Bounded Context applies to any domain and components within your own domain. Assign responsibility to a component/system and make that system the guardian of that business logic.
When driving a car, you don’t reach out the window to apply the brakes with your hands do you? You apply the brakes through something else, the message gets passed to the brakes in some unknown way to the drive system. The driver just knows to press the brake pedal to slow down. This concept illustrates Aggregates. Aggregates are objects that expose the public methods an external force (the driver of the car) can do. Aggregates can have many sub-systems but will be forever encapsulated unknown to the driver. This is because we don’t want the driver to care about all the sub-systems between the brake pedal and the act of the car slowing down. All they care is that the car slows down when the pedal is applied.
If an object has a unique property and is mutable, it is called and Entity. An entity is very familiar to folks using a database. Anything with an Id column (or similar function column that is uniquely identifying), is an entity. In fact just about everything you stick in a DB is going to have a unique key by the nature of how a DB works.
Conversely, if an object is intrinsically unique and immutable; it is called a Value Object. These are a little harder to think of without some good examples:
DateTime - It’s an object that can be operated on yet it is intrinsically unique and parts of my code don’t need any particular instance of DateTime to be unique. For example two parts of your app can conjure a DateTime for the same exact moment yet neither can change the value of it.
Addresses - While not a built-in class to Dotnet, an address does not change ever. Two parts of my app can have the same address and I know neither can ever change the value of it.
We’ll get into Factories, Services and Repositories to round out our word soup, but we’ll do so in the examples next.
The Anemic Model is the villain in our story
In order to tell a story, we need a protagonist and an antagonist. It’s your classic good vs evil — except with code. For our antagonist, we’ll use the Anemic Model. Let’s explain our villain with some classic deeds:
Uses mostly POCO’s (plain ‘ol CLR objects) to model things
Uses the DB as a first-class citizen for modelling
Uses a typical n-tier stack where you have a data-layer, a service layer and some sort of user interactive layer (MVC/WebAPI, etc).
Services hold all the business logic
Any number of services can be created and any number of repositories can be depended on to Do Things ™
The reason it’s called an anemic model is because it is a free-for-all. Any service can depend on any repository and mutate state. There is no centralization/gatekeeping on who can change the underlying models. If you use this model, I hope you like spaghetti. Fret not though, I think this is the most popular model I’ve seen over the last decade.
Our Hero to the rescue
DDD (as you probably guessed) is our protagonist. The next few examples will illustrate key differences between the anemic model and DDD. My hope here is to show you how to leverage the compiler and design patterns; to help protect your business logic from external code.
Factories, Factories, Factories
In the anemic model, creating a new objects are as simple as calling a constructor. Let’s look at a typical POCO model for a user:
namespace DotNetDev.Anemic;
public class UserModel
{
public int Id { get; set; }
}
var userModel = new UserModel();
So far so good right? Seems like code you’ve seen in hundreds of tutorials.
In DDD you’d likely be upset that just anyone can create your model. Rather in DDD we’d likely restrict the constructor of your model and require it to be instantiated in a factory.
If you’re not familiar with the factory pattern, essentially its just a class that creates other classes. Often by returning an abstraction in the method signature.
namespace DotNetDev.DDD;
public interface IUserModel
{
public int Id { get; } //no setter
}
public class UserModel : IUserModel
{
public int Id { get; private set; } //private setter
//internal ctor
internal UserModel(int id)
{
Id = id;
}
}
...
namespace DotNetDev.DDD;
public class UserModelFactory
{
public UserModelFactory()
{
}
public IUserModel Create(int id)
{
var model = new UserModel(id);
}
}
var userModel = new UserModelFactory().Create(id);
Now I know what you’re thinking, that’s way more code. While that is the case, it’s also much more protective code. Since the ctor is internal, that means anything outside of our project cannot instantiate the object. Rather they must use the factory.
The factory pattern is named so because of the real-life connection. You bring the raw materials (in this case the id), and we’ll ensure you get an IUserModel.
Therefore: Use a factory and don’t let objects external to your domain create domain models on their own.
Before we move on, DDD is dependency injection friendly. I’ve discovered that if you use the following pattern, you can use an IoC container with DDD easily.
namespace DotNetDev.DDD;
public interface IUserModel
{
public int Id { get; } //no setter
}
public class UserModel : IUserModel
{
private readonly ISomeDependency _someDependency;
//internal ctor, ISomeDependency from factory
internal UserModel(ISomeDependency someDependency)
{
_someDependency = someDependency;
}
internal void FromId(int id) //hydration method used in factory
{
Id = id;
}
}
...
namespace DotNetDev.DDD;
public class UserModelFactory
{
private readonly ISomeDependency _someDependency;
public UserModelFactory(ISomeDependency someDependency)
{
_someDependency = someDependency;
}
public IUserModel Create(int id)
{
var model = new UserModel(_someDependency);
model.FromId(id);
return model;
}
}
var userModel = new UserModelFactory().Create(id);
Get your hands out of my cookie jar!
I didn’t call it out, but in the previous example you’ll notice that the anemic model had public get/set properties for Id. That means any code can change your properties and have no idea if that has any sort of side-effects. Code that allows this literally is the wild-west.
In DDD, proper usage of access modifiers is a big deal. You’ll notice that the interface has only a public get and the implementation has a private setter. This prohibits any other class that gets its grubby hands on a UserModel to have to play by the rules. Otherwise everything is mutable. Mmmmm spaghetti.
Therefore: Guard mutations to your data model. Make any setting go thru the gauntlet of validations.
Top-Notch Service
So far we’ve seen that the model in DDD is starting to take control of it’s own destiny. This is a big deal in DDD, we are inverting the control of mutation of the model’s state. In the anemic model, we typically have a service that takes a model as an arg.
namespace DotNetDev.Anemic;
public class UserService
{
public UserModel DoSomething(UserModel model)
{
model.Id = 1234;
return model;
}
}
...
//we can change the ID b/c it's a public field
_userService.DoSomething(someModel);
As you can see above, a service can do anything it wants to a model with a public setter. There is no encapsulation and chaos reigns.
In DDD, since the setter is private, we have to expose a method to allow the ID to be changed. If we don’t want someone meddling with the ID, we simply won’t let them do it. The model retains control as we see below:
public interface IUserModel
{
public int Id { get; } //no setter
public string Name { get; } //no setter
public void ChangeName(string name);
}
public class UserModel
{
public int Id { get; private set; } //private setter
public string Name { get; private set; } //private setter
internal UserModel() //internal ctor
}
public void ChangeName(string name)
{
... lots of validation first
Name = name;
}
}
You could implement a public setter and have validation. Not all properties need to be privately set. The idea here is to be able to invert the control of internal state by carefully exposing methods that can mutate state from the outside.
You may be asking yourself, can I ever have a service? The answer is yes, but in DDD a Service is defined as a bit of code that doesn’t really fit in the domain or is a utility. The idea here is that Domain Models are the champions and services are lesser known support characters.
Therefore: The model should expose methods to change its internal state only if necessary. External services should have no ability to mutate internal state.
You may not like me after this next part
I’ve always loathed that most web applications these days are really database centric. It’s typically something like the following:
Something comes in the controller
We fiddle with the input in a service
We store the changes in the DB
Some other process gets the data from the DB
Does Things to it and puts it back in the DB
There’s no cohesion in the model, everyone opens the fridge and gets what they want. I’d like to blame DB-driven design on giving birth to the anemic model.
In my world-view of DDD, I introduce the idea of persistence to my domain models. In this way I expressly prohibit any external code from getting any data from the database (or from a file/cloud bucket/etc my domain owns). If you want data from the database you suspect I have under my model, you must ask my domain model for it. My domain model is the only thing that can load or save a domain object.
When I look at an anemic model, I can’t help but see chaos. There’s no control, only chaos. Services and repositories give the illusion of order, but what is not so obvious is the lack of access control internal to the application.
The repository pattern is super popular in production websites. This pattern allows an easy way to fetch any DB object you want and all you have to do is get a reference to the repository that serves it up. While I agree this is convenient, it’s also the problem.
The other thing I like to do is to firewall the exact repository implementation from the domain. This means when I want to use EntityFramework, I create a persistence class than only accepts the domain model and returns an agnostic entity to be processed into a domain model. First let’s look at the UserModel changes:
using DotNetDev.DDD.Persistence;
namespace DotNetDev.DDD;
public interface IUserModel
{
public int Id { get; } //no setter
public string Name { get; } //no setter
public void ChangeName(string name);
public Task SaveAsync();
}
public class UserModel : IUserModel
{
private readonly ISomeDependency _someDependency;
private readonly IUserPersistenceService _userPersistenceService;
public int Id { get; private set; } //private setter
public string Name { get; private set; } = null!;//private setter
public void ChangeName(string name)
{
//copious amounts of validation goes here
Name = name;
}
public async Task SaveAsync()
{
//pass current instance to the persistence
await _userPersistenceService.SaveAsync(this);
}
//internal ctor, ISomeDependency from factory
internal UserModel(ISomeDependency someDependency, IUserPersistenceService userPersistenceService)
{
_someDependency = someDependency;
_userPersistenceService = userPersistenceService;
}
internal void FromId(int id) //hydration method used in factory
{
Id = id;
}
}
And then we need to implement our UserPersistenceService along with EntityFramework:
namespace DotNetDev.DDD.Persistence;
public interface IUserPersistenceService
{
Task SaveAsync(IUserModel userModel);
Task<PersistedUser> GetByIdAsync(int id);
}
public class UserPersistenceService : IUserPersistenceService
{
private readonly DbContext _dbContext;
internal UserPersistenceService(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task SaveAsync(IUserModel userModel)
{
//EF Code
var entity = new UserEntity
{
Id = userModel.Id
};
_dbContext.Users.Add(entity);
await _dbContext.SaveChangesAsync();
}
public Task<PersistedUser> GetByIdAsync(int id)
{
var entity = _dbContext.Users.SingleOrDefault(x => x.Id == id)!;
//do some null checks
return Task.FromResult( new PersistedUser
{
Id = entity.Id
});
}
}
...
namespace DotNetDev.DDD.Persistence;
//some sort of DB context like EntityFramework
public class DbContext
{
public List<UserEntity> Users { get; set; } = new ();
public Task SaveChangesAsync()
{
//omitted for brevity
throw new NotImplementedException();
}
}
...
namespace DotNetDev.DDD.Persistence;
//user entity used by EF
public class UserEntity
{
public int Id { get; set; }
}
...
namespace DotNetDev.DDD.Persistence;
//class meant to firewall EF from the domain model
public class PersistedUser
{
public int Id { get; set; }
}
Now finally we call it from our external code:
await userModel.SaveAsync();
Our UserModel is now in charge of its persistence instead of an external service.
Therefore: Don’t have a global set of repositories that any random bit of code and fetch and store changes willy-nilly. Make data fetching thru the model the norm and fetching from a repository you don’t own impossible. Don’t leak your DB implementation into your domain model.
The reason I feel this might be a controversial section is because of the extra object (PersistedUser) that insulates the domain model from EntityFramework.
I do this to prevent any sort of accidental lazy-loading and I don’t want my domain to know about EntityFramework other than in the persistence service.
The End is Near
So that’s all the examples I decided to cobble together. DDD in my opinion is a design choice that can coexist with any other design pattern. Is it the best? It has it’s value for sure, but it might also be a bit much for a simple website.
I think DDD excels in environments where you have many teams and the honor system no longer works. I really feel that DDD takes the DB and makes it a 2nd-class citizen so that the focus is on business logic rather than extracting, changing and saving DB entities.
It is much quicker to get to market in an anemic model but the proverbial frog in the water doesn’t realize he’s in trouble until it’s way too late. With DDD you have explicit boundaries (Bounded Context remember?) which are enforced with the help the compiler.
DDD is not for everyone and for those who enjoy it, likely can’t agree on what DDD actually is (agile I’m also looking at you).
Therefore
Encapsulate all-the-things
Use common words with your stakeholders and model the code like it is in real-life
Use factories to mint your domain models
Make your DB a 2nd-class citizen, your first-class citizen becomes your business logic
Hide the details, external code shouldn’t need to know how it all works internally
Feel free to modify the rules, but make sure it’s not a free-for-all
Full source can be found here.