Why your Domain Model might be lying to you — and how to fix it
There's a design mistake that's easy to make and hard to spot: the Fat Aggregate. If you've ever worked with Domain-Driven Design (DDD) and felt like your aggregate root was turning into a God class — responsible for everything, loaded with every related object, growing with every new business rule — this one is for you. **What's a Fat Aggregate?** When modeling domain objects, we tend to think about what they contain before thinking about what they do. This is natural — but it's also where things can go wrong. Take a classic example: a project management system. The `Project` aggregate becomes the root that governs the project's lifecycle and enforces its business rules. A project has tasks, team members, attached documents — so we pull them all in. The aggregate encapsulates behavior like assigning tasks, attaching documents, and marking the project as complete. On the surface, it looks right. The aggregate protects its invariants and prevents invalid state. But as the system grows, the cracks appear. **The real cost** In DDD, repositories load the full aggregate before any write operation, so all invariants can be verified. If `Project` is the central aggregate, then every write — assigning a task, adding a member, attaching a document, adjusting the budget — must load the entire object graph. This causes table locks, performance bottlenecks, and contention between concurrent operations. And it only gets worse: every new business rule gets added to `Project`. The aggregate grows into a God class: bloated, hard to reason about, and increasingly risky to change. **The fix: trim the aggregate down** The key question every designer should ask is: what must be consistent in the same transaction? If two pieces of data do not need to change together, they probably should not live in the same aggregate. For example, attaching a document doesn't require tasks and team members to be loaded in memory. That's the signal to refactor: keep `Project` lean and move documents and tasks into separate aggregates that hold a reference to `ProjectId`. This doesn't mean business rules disappear — it means they find the right home. Local invariants stay in aggregates, cross-aggregate rules live in domain services, and orchestration lives in application services. **Why this matters in practice** The benefits of lean aggregates are concrete: Each write touches fewer tables and rows, so queries are faster and locks are smaller. Two users can update different parts of the same project with less contention. Business rules are grouped by true consistency boundary, which keeps each model smaller and easier to reason about. **Common pitfalls to watch out for** Even after refactoring, teams often fall into two traps. The first is putting orchestration into domain services. A domain service should express business rules. Loading repositories, managing transactions, and persisting changes is orchestration and belongs to an application service or command handler. The second is splitting by tables instead of by consistency boundaries. If we split only because a table is big, we may end up with models that still need to change together. A better signal is business consistency: if two things must always be valid together in one transaction, they likely belong to the same aggregate. Some warning signs that a split was done wrong: you often load multiple aggregates just to enforce a single invariant; you keep adding cross-aggregate checks for operations that feel like one consistency rule; you see temporary invalid states that business users consider unacceptable; you frequently need distributed transactions for one user action. **The bottom line** Lean aggregates start with a simple discipline: model intrinsic behavior first, and include only the data required to protect that behavior. An aggregate is not a container for all related data — it is a consistency boundary around rules that must hold together. When you design your boundaries this way, the model becomes smaller, clearer, and easier to evolve. You avoid God aggregates, reduce write contention, and keep domain rules exactly where they belong. Worth a read if you work with DDD, C#, or just care about keeping your domain models honest. --- Read the full article at: https://deniskyashif.com/2026/04/04/domain-driven-design-lean-aggregates/


