Every few years, the industry latches onto a pattern and applies it everywhere. Microservices in 2018. Event-driven architecture in 2020. Domain-Driven Design has been in this position for a while now, and the symptoms are familiar: aggregates without invariants, value objects that wrap a single string, domain events fired for a basic updateUser call.
The code looks serious. The architecture diagram looks impressive. And a capable engineer joining the team still needs too long to understand why a simple content update crosses four layers of abstraction.
This is cargo cult DDD. Teams apply it to signal seniority, not to solve a problem. The pattern becomes the goal instead of the tool.
What DDD Actually Is
Before discussing when not to use it, it helps to be precise about what it is.
DDD is not an architecture. It is not a folder structure. It is not a set of patterns you apply to look sophisticated.
DDD is an investment in conversation. Its real purpose is to create a shared language between the people who understand the business and the people who build the software, so that when everyone sits around the table, they are talking about the same thing. The ubiquitous language is not a technical artifact. It is the proof that the conversation happened.
The tactical patterns - aggregates, value objects, domain events, repositories - are tools for protecting that shared understanding in code. They are not the point. The conversation is the point.
If your team is not having that conversation with the business, DDD will not save you. The patterns without the dialogue are just boilerplate.
The CRUD Problem
Consider two e-commerce systems.
The first is a basic product catalog. Products have a name, a price, a description, and a stock count. Users browse, add to cart, and checkout. The rules are simple and stable. There is no conditional logic, no domain-specific invariant that the code needs to protect, no concept that requires negotiation between the engineering team and the business.
This system has a domain, but not enough domain complexity to justify tactical DDD. Applying DDD here means introducing aggregates that enforce no invariants, value objects that add ceremony without protection, and domain events that fire for operations nobody cares about tracking.
The second system sells veterinary nutrition. Pricing rules depend on the animal's breed, age, weight, and health condition. Promotions apply differently depending on whether the customer is a veterinary clinic, a pet store, or an end consumer. Certain products can only be recommended by certified professionals. The rules are complex, they change frequently, and getting them wrong has real consequences.
I worked on a platform in this space with a consulting delivery team. The business rules were not simple. The conversations between product, veterinary experts, and engineering were genuinely difficult. Terms meant different things to different people until we forced alignment. That friction is exactly where DDD earns its cost.
The difference between these two systems is not the technology. It is the complexity of the domain and the cost of misunderstanding.
The Three Signals That DDD Is Justified
After enough projects, certain signals become recognizable.
The first signal: the business keeps saying "it depends." Simple systems have simple rules. Complex domains have conditional rules: rules that depend on context, state, history, or the combination of multiple factors. When the business answer to almost any question is "it depends on...", the domain has complexity worth modelling explicitly.
The second signal: conversations between technology and business keep breaking down. When a product manager describes a requirement and the engineering team builds something different, not because of poor execution but because of genuine misunderstanding, the shared language is missing. DDD's ubiquitous language exists precisely to close this gap. If the gap does not exist, neither does the need.
The third signal: you need to protect integrity at the business level, not just the database level. A foreign key constraint protects referential integrity. An aggregate protects business integrity: the invariant that a certain state combination should never exist. If your rules can be expressed entirely in database constraints and validation schemas, you probably do not need aggregates.
The Cargo Cult Symptom Checklist
These are the signs that DDD has been applied without justification:
- Aggregates that contain a single entity and enforce no invariants.
- Value objects that exist only to wrap a primitive with no validation logic.
- Domain events fired for operations that nobody consumes or reacts to.
- A
UserDomainServicethat does exactly what a function would do. - New developers spending their first weeks understanding the architecture instead of the product.
- The word "domain" appearing everywhere while nobody can explain what the domain actually is.
None of these are problems with DDD. They are problems with applying DDD to a context that does not need it.
DDD Does Not Have To Be All Or Nothing
This is the pragmatic position that most articles miss.
You do not have to apply DDD to your entire system. A bounded context is, by definition, a boundary. Some contexts in your system may have genuine domain complexity. Others may be glorified CRUD. Both can coexist.
On Fleet, the SaaS platform I have been building, I started with plain CRUD. Vehicles, users, assignments: straightforward data with straightforward operations. No invariants worth protecting, no rules worth modelling explicitly.
As the product evolved, one part of the domain started resisting the simplicity. Fleet assignment rules became conditional. Availability logic started accumulating exceptions. The conversations with stakeholders became harder. We were using the same words to mean different things.
That was the signal. I introduced DDD tactically in that bounded context only. The rest of the system stayed simple. The CRUD parts did not become more complex because one part needed the structure.
This is the honest path: start simple, introduce structure where the problem demands it, leave the rest alone.
The Cost Nobody Talks About
The boilerplate cost of DDD is well documented. The human cost is not.
When a new developer joins a project with full DDD applied to a simple domain, their first weeks are spent understanding the architecture rather than understanding the business. They learn the pattern vocabulary before they learn what the product does. The abstraction becomes the obstacle.
In a genuinely complex domain, this investment pays off. The architecture eventually helps the developer navigate complexity they could not have held in their head otherwise. In a simple domain, the investment never returns. The developer learned an abstraction that protects nothing.
This is a real cost for engineering managers and CTOs. It shows up in onboarding time, in the frustration of capable developers working against unnecessary friction, and in the attrition of people who came to solve business problems and found themselves maintaining ceremonial code instead.
When To Introduce It - And How
Do not apply DDD at the start of a project unless you have strong evidence that the domain is already complex. Evidence means: you have done discovery with the business, the rules have conditions and exceptions, and the conversations are genuinely hard.
Introduce it incrementally when the signals appear. Refactoring toward DDD in a specific bounded context is manageable. Removing DDD from a system that never needed it is painful.
The trigger is not a project size or a team size. It is the moment when the business rules start to resist simple modelling: when a plain function or a database constraint is no longer the right tool for expressing what the business actually means.
The Idea Worth Keeping
DDD is an investment in the conversation, not in the code.
If the conversation between your engineering team and your business stakeholders is working, if everyone around the table understands the same thing when they use the same word, then the tactical patterns are a way to protect that shared understanding in the codebase.
If the conversation is not working, the patterns will not fix it.
Start there. Fix the conversation first. The architecture will follow.