Scaling DDD in ASP.NET: Challenges Beyond 30 Aggregates
When a project exceeds 30 aggregates in ASP.NET with Entity Framework Core, the principle "one query — one aggregate" leads to critical performance issues. Each aggregate requires full loading with nested objects, generating SQL queries with numerous JOINs. This becomes especially problematic in ERP systems or marketplaces where object graphs reach 5–6 levels of nesting.
A typical Order aggregate query looks like this:
var orders = await context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.Customer)
.ThenInclude(c => c.Contacts)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ThenInclude(p => p.Category)
.Include(o => o.Items)
.ThenInclude(i => i.Discount)
.ToListAsync();
The resulting SQL contains dozens of LEFT JOINs, significantly slowing execution. Updating a single field in an order header forces loading all OrderItems as part of the aggregate, compounding the performance burden.
Explosive Growth of Specifications and Their Limits
The Specification pattern encapsulates query logic, but with 30+ aggregates, the collection of Specification classes can grow to hundreds of files. Composition via And/Or results in inefficient SQL due to EF Core’s IQueryable dependency.
Key challenges:
- ORM-specific: Specifications don’t work with Dapper or gRPC without rewriting.
- Complex composition: Real-world scenarios involving roles, statuses, and time zones become unwieldy.
- Abstraction overhead: Time spent on abstractions exceeds actual business logic implementation.
Cross-Aggregate Transactions and Infrastructure Overhead
Scoped DbContext in ASP.NET Core works well for Unit of Work in small projects. In large systems, business workflows often require simultaneous updates across 5+ aggregates: placing an order, processing a payment, reserving inventory, and creating a call center task.
Available solutions contradict DDD principles:
- Single database transaction — creates God services and violates aggregate boundaries.
- Distributed Sagas or Outbox patterns — increase complexity by up to 10x.
As a result, 70% of the codebase becomes infrastructure glue for synchronization, not business logic.
Code Navigation: Thousands of Files and Slowed Iterations
DDD structure (API/Application/Domain/Infrastructure + Features) generates over 1,000 files with 30+ aggregates. Changing a single entity field affects:
- Domain entity
- Fluent API configuration
- Migration
- Command
- Validator (FluentValidation)
- Handler
- DTO
- Mapper (AutoMapper)
- Specification
- Tests
This drastically slows development, complicates onboarding, and hinders long-term maintenance.
What Matters Most
- Performance: Heavy Include/ThenInclude operations produce inefficient SQL, causing API response times to grow exponentially.
- Specifications: Don’t scale, are tied to EF Core, and composition leads to errors.
- Transactions: Cross-aggregate operations force trade-offs between DDD ideals and real business needs.
- Codebase size: Over 1,000 files slow iterations by 50–70%.
- Hybrid approach: Apply DDD only in the core; use CQRS and Vertical Slices for peripheral concerns.
Recommendations for Migration
For projects with 30+ aggregates, adopt hybrid architectures:
- CQRS: Separate commands from queries; use read models without Include clauses.
- Vertical Slices: Group code by feature, minimizing layered complexity.
- Feature-Sliced Design: Achieve modularity without rigid, onion-like architecture.
These approaches preserve DDD principles in complex domains while simplifying everything else.
— Editorial Team
No comments yet.