Domain-Driven Design pitfalls I fell straight into
By Yahya Eddhissa · · Updated · 6 min read
- Domain-Driven Design
- Overengineering
- Architecture
I was sure Domain-Driven Design was the missing piece in how I worked. I applied it to two projects, an IAM system and an HRIS, and in both I ended up with the shape of DDD and very little of the substance. This is what I got wrong, roughly in the order I got it wrong, and what I’d do now.
# How I got hooked
I had heard about DDD for a long time, usually described as a way to model business requirements in code. Eventually I gathered the courage to read Eric Evans’ blue book, and I loved it. It’s slow and dated in places, but it treats the strategic side of DDD (bounded contexts, ubiquitous language, distilling a model from domain experts) as seriously as the tactical side (entities, aggregates, repositories).
Then I read Vaughn Vernon’s Implementing Domain-Driven Design and enjoyed it even more. It was practical, with concrete examples in Java and SQL, which gave me what I needed to turn the ideas into code. By then I was determined. I was convinced DDD was the missing piece in my approach, and I had two projects to try it on.
# Everything became an aggregate
I started by modeling everything as an aggregate. The books describe a lot of building blocks, and I couldn’t tell which ones a given piece of my domain actually needed, so I used all of them on all of it. I was also lost on how to structure the code. Is this an entity or a value object? Is it its own aggregate, or part of another one? I found it hard to tell the concepts apart, so I modeled everything in DDD and, without noticing, blurred the boundaries between them.
The code looked right. It had aggregates and repositories, and it resembled the examples in the books. That made it easy to believe I was doing DDD.
# The Employee that knew everything
The clearest casualty was the Employee aggregate. It held every aspect of an employee: personal information, position in the hierarchy, team memberships, leave balance, and more. Each new requirement that mentioned an employee added another field or another method to the same class.
It turned into an anemic model: a large bag of data touching half the system, with the actual business rules living in services around it. It was named after a domain concept and sat in the domain layer, but it protected almost no invariants.
The cause was that “employee” isn’t one thing. To HR it’s a person with a record. To the org structure it’s a node in a hierarchy. To a team it’s a member. To leave management it’s someone with a balance and a set of requests. Those are different models of the same person, each with its own rules, and they belong in different bounded contexts. I put them all in one, so the model couldn’t be rich. Any behavior I wrote had to make sense for every meaning at once, which meant it made sense for none.
# The experts weren’t the problem
I did the part everyone says to do. I met with domain experts often, in long meetings, and came away with a lot of domain knowledge. That part worked.
What I struggled with was the next step: projecting that knowledge onto code. For me it was more of a technical problem than a conversational one. The knowledge I had gathered was richer than the model I managed to build from it, and with no clear boundaries I had nowhere good to put it.
# Relational habits, tightly coupled aggregates
The other habit I couldn’t shake came from years of working with relational databases. I linked aggregates to each other the way you link tables, with references and relationships wherever a foreign key would go. It felt natural, and it introduced tight coupling, because loading or changing one aggregate dragged others along.
So I started breaking things down. I went through the relationships and removed every reference that didn’t serve a business need. What remained were smaller aggregates with far fewer ties to each other.
# AI pulled in the same direction
I also used AI assistants while writing this code, and they made the problem worse. They produce aggregate roots, value objects, and repository interfaces in seconds, and left to themselves they kept dragging me toward the anemic model. They lack the human insight to separate concerns, and separating concerns was exactly what I was already struggling with. AI can scaffold the tactical side. It can’t decide whether “employee” means one thing or four.
# Dropping DDD where it didn’t belong
The ending is the same as in the rest of that project. Many modules had no strict invariants to protect, and for those, DDD was overhead. I dropped it there and used simpler models, which made the code easier to change and no less correct.
# What I kept
I didn’t throw all of it away. I kept:
- The ubiquitous language. Naming things the way the business does paid off everywhere, including the modules where nothing else from DDD survived.
- A few bounded contexts. Enough to stop concepts like “employee” from collapsing into one model.
- A few aggregates, only where a consistency boundary is strictly necessary. State machines such as leave approval circuits are the best example, along with places where an invariant has to be protected.
Leave is also where I learned a different lesson, about CQRS and event sourcing, but that’s a story for another post.
# What I’d tell myself before starting
- Know the two sides. The tactical half (entities, value objects, aggregates, repositories) isn’t unique to DDD. What makes it domain-driven is how it supports a model derived from the business. The “two sides of DDD” distinction is worth reading.
- Start with boundaries. Evans has said that bounded contexts and ubiquitous language are the most important concepts in the book, more than any tactical pattern. They’re also the easiest to skip, because they don’t produce code you can point at.
- Don’t model on the schema. If your aggregates mirror your tables and are full of relationships, you probably haven’t found your boundaries yet.
- Gathering knowledge isn’t modeling. Talking to experts is necessary. Turning what they say into a model is separate work, and it’s yours.
- Use it only where a consistency boundary is strictly necessary. That is where I’d begin if I started over.
DDD gave me a vocabulary I’m glad to have: ubiquitous language, bounded contexts, the distinction between core and supporting parts of a domain. But applied everywhere, with aggregates standing in for a model I hadn’t built, I hadn’t adopted DDD. I’d adopted its costume.