Domain Driven Design (DDD) is a software design approach that puts the business domain at its center. Inside a clean architecture system, where the domain doesn't depend on anything, the development team should instantiate objects in memory and have them execute interactions that should be entirely recognizable by a domain expert.
While other areas of the system will be responsible for things that don't concern the domain at all (like persistence and exposing data), the main focus of developers should be delivering business behavior in a way that resembles that with which domain experts think and talk about the problem.
Even though this approach seems a bit hard in the first glance, I hope this text will shine some light on why it has so many acolytes, and how you can use it in your own day to day.
Diving deeper
When I say that the domain doesn't depend on anything, I mean just that. If you already know a bit about [[Clean Architecture]] or [[Hexagonal / Onion Architecture]], it is the area of the code where no arrows originate from.
That means the code doesn't reference any id's, any messaging queue, headers, nothing. You should be able to test it with no mocks, just by calling methods and see the changes happen.
Lets look at an example:
We get this feature request: "We need a new page where users will be able to list, add and edit contract templates, in the listing there will be an button to send and email with the contract, and the user will be able to input the template's variables that will be replaced.".
In this case, the domain entity can look a little like this:
public class ContractTemplate
{
public string name { get; set; }
public string body { get; set; }
public List<string> variables { get; set; }
public string generateContract(object variables)
{
string contract = this.body;
for (string var in this.variables)
{
contract.Replace($"\{{var}\}", variables[var]);
};
return contract;
}
}
So, the ContractTemplate entity has a name, a body and some variables, and a method to generate a contract based on on object that will have all the needed variables (normally we would add some input validation too in this case). Note that the code isn't close to done, the application layer still need to receive requests for the crud operations, and we need some kind of repository to persist the templates.
To persist the templates we will need to break some rules like adding an id:
public class ContractTemplate
{
public int Id { get; set; }
...
}
This is the domain knowing something from the infrastructure layer, but that's just a small infraction.
If we feel like the action of sending the email should be part of the domain, we can also include it this way:
public class EmailRequestedEvent : IDomainEvent
{
string contract;
from string;
to string;
}
Class User
{
...
private List<DomainEvent> events = new List<DomainEvent>();
private List<ContractTemplate> templates;
...
public void sendContractViaEmail(string to, string from, string templateName, object variables)
{
var template = this.templates.firstOrDefault(tem => tem.name == templateName);
var contract = template.generateContract(variables);
var event = new EmailRequestedEvent() { contract = contract, from = from, to = to };
this.domainEvents.Add(event);
}
public List<DomainEvents> FlushEvents()
{
var events = this.domainEvents.clone();
this.domainEvents = new List<DomainEvent>();
return events;
}
}
Now, we are generating domain events that will be dispatched by the application layer, that will call our .FlushEvents method and send the emails probably via http or with a message queue, but the domain layer doesn't need to know about it.
The domain expert probably wouldn't understand our code, but we can see how the phrase "the user can send an email with a contract generated from an template with inputted variables" would be represented by it.
Why go to such lengths
Most of us are more used to what the literature calls "Anemic domains", entities that just hold data that will be persisted on the db and exposed on the api. in this case, the application layer would have services that had the business rules baked into them.
This is not a problem if the system is small and if the entities really don't hold this much business rules, but using this strategy on a core domain that is bound to grow and grow is really risky:
1 - The business rules are dispersed:
With no central place to hold the domain rules of a certain entity or relation, each new service must enforce them. It takes a lot of skill and knowledge of the system to keep this together without having some rules enforced in all method calls, normally some of them break apart and bugs begging to pop in.
On top of that, changing those rules will result in a system wide search for enforcement.
2 - Testing:
If you did work on a system like this you also already saw how hard it is to test them. As the services need a connection with a db we will need to mock it. Files and files of fixtures, json that will be converted to entities, a lot of utilities just to check if the entities where changed in some way. While the tests from a fully fledged domain tend to be so simple enough as to resemble tutorials made for people with little experience with programming.
3 - Separation of concerns:
Having each layer be responsible for a set of things is magical. Even if the codebase grows and grows you will still have a clear idea where things should go, and how to debug the bugs that will eventually appear.
If you need to change the db, the domain wont be affected, if we go from Rest to grpc, back to Rest and them to a messaging queue, the domain will be unaffected. This kind of flexibility is priceless.
As an addendum, if you are working in a system that has "domain services" that make calls straight to the db, the service is from the application layer and you are most likelly using an anemic domain.
But DDD is overrated
Most *DD's are somewhat overrated and underrated, let me explain:
In tech, we have a lot of problems. To solve those problems some companies come up with a solutions like DDD, XP and Scrum. If the solution is successful, its success is spread wide in conventions, articles, books, videos, etc.
When managers from other companies hear about this success, they quickly skin through a 5 minute article about it and try to implement it in a way that wont really change anything about their company or its previous practices. This managers and the employees on this company spread how great the implementation was as if it was really the same thing as the first solution, mostly for personal clout. Other managers hear about it and the cycle repeats.
A lot of our cherished concepts fell victim to that: BDD, TDD, DDD, Scrum, Kanban, Agile, KPIs and OKRs just to name a few.
Focusing in DDD, I see a lot of managers parroting about how important it is, while their whole codebase doesn't have a single method on any of its entities. In this case I would agree that talking about DDD is pointless. If you are just pretending, there is no value in studying about aggregates or anti-corruption layers. Just focus on coding your services without trying to fit in any DDD logic in the way your system handles business rules inside that database or application layer, it's a fool's errand.
And yes, actual DDD is underrated. I've seen senior devs say that putting logic in the domain entities will just lead to garbage code. Managers asking the programmers just to "write rest code, and enforce the rules in the db". I can't stress enough how few implementations of actual DDD I've witnessed.
But DDD is hard
It is, for sure.
Caring to understand the domain tends to slow down the project specially in the beginning, because it is really hard for the product team to be clear enough about how the domain works.
You will need a lot of meetings, whiteboard and new domains terms that even the domain experts don't use on the day to day (because they don't need to be this precise normally).
All of this brings all bad stuff we know happens when we try to do a big design upfront, but it also anticipates a lot mismatches on how the product team, the domain experts and engineering see how the system should work, and this is a big time saver.
Also, you would be astonished to how common it is for new features to be requested that would be a pain to implement normally, but are quick and easy when the implementation is close to what the domain looks like. The bizarre and strange asks from the product team will make sense inside the system, somehow.
When to stick with anemic entities
Abstractions generate indirection of flow of control and more lines of code, so its inclusion must be attached to some kind of value generated by it to be defensible.
To decide if DDD is a good fit, I'd focus on these points:
- How close to the machine must the code be? If you need to do a lot of low level operations to keep the performance on check, just do the operations. This is good for off shoots of the codebase outside of the core domain (like generating reports and so on) or library code.
- How complex is the domain? If the entities are to be just be saved and retrieved, even if you follow DDD the domain will be anemic anyway.
- If its "just crud", how many rules are embedded inside every verb? You can say that most tech in the world is "just crud", but normally creating, updating and even deleting carries a lot of business logic inside them.
The same logic applies to other concepts like clean architecture, solid, clean code, etc. If you can just write and endpoint that does what the PM/PO wants it to do in a single file without it looking painful to maintain, go for it.
Top comments (0)