Designing Aggregates
Learn how to design effective aggregates that serve as consistency boundaries in event-sourced systems.
An aggregate is a cluster of domain objects that can be treated as a single unit for data changes. It defines a consistency boundary where business rules (invariants) are enforced. In event sourcing, each aggregate instance corresponds to a single event stream.
The aggregate has a root entity (aggregate root) that serves as the only entry point for modifications. External objects can only hold references to the aggregate root, not to internal entities within the aggregate.
Key principle: One aggregate = one transaction = one event stream = one consistency boundary
- Enforce business invariants and rules
- Process commands and validate preconditions
- Generate domain events when state changes
- Maintain consistency within its boundary
- Coordinate changes to internal entities
- Has a unique identifier (aggregate ID)
- Loaded and saved as a complete unit
- All modifications go through the root
- Internal state is private and encapsulated
- References other aggregates by ID only
Finding the right size for aggregates is one of the most important design decisions. Too large and you create contention and complexity; too small and you struggle to enforce invariants.
Aggregates Too Large
Signs:
- • Long event streams (1000s of events)
- • Frequent concurrency conflicts
- • Complex business logic hard to understand
- • Slow to load and save
Problems:
- • Performance degradation
- • High contention and lock conflicts
- • Difficult to test and maintain
Aggregates Too Small
Signs:
- • Complex orchestration between aggregates
- • Difficulty enforcing business rules
- • Many aggregates for simple operations
- • Eventual consistency everywhere
Problems:
- • Can't enforce cross-entity invariants
- • Complex saga/process managers
- • Loss of transactional boundaries
Right-Sized Aggregates
Design aggregates around business invariants. If a rule must be checked atomically, the related data should be in the same aggregate. Start small and only grow when invariants require it.
1. Design Around Invariants
Business rules that must be enforced atomically define your aggregate boundaries.
Example:
"An account balance cannot go negative"All balance operations belong in the Account aggregate
2. Keep Aggregates Small
Prefer smaller aggregates. It's easier to combine small aggregates later than to split large ones.
Rule of thumb:
If an aggregate commonly has more than a few hundred events, consider splitting it
3. Reference By ID
Aggregates should only reference other aggregates by their ID, never by object reference.
4. Modify One Aggregate Per Transaction
Each transaction should only modify a single aggregate. Use events and eventual consistency to coordinate changes across aggregates.
If you need to modify multiple aggregates together, you probably have the boundaries wrong
5. Name Aggregates After Domain Concepts
Use names from your domain language, not technical terms.
Document-Style Aggregate
A single entity with properties and simple value objects. Most aggregates should start here.
Collection-Style Aggregate
A root entity managing a collection of child entities. Use when children have identity and lifecycle.
State Machine Aggregate
An aggregate that progresses through well-defined states with strict transitions.
Creation
Created by a command that validates preconditions and emits a creation event (e.g., OrderPlaced)
Active State
Processes commands, enforces rules, and emits events as state changes occur
Completion
Reaches a terminal state through a completion event (e.g., OrderCompleted, AccountClosed)
Archive (Optional)
After completion, the aggregate may be archived but events remain immutable
When business operations span multiple aggregates, use eventual consistency and coordination patterns:
Domain Events
Aggregates publish events that other aggregates react to. Simple and decoupled.
Process Managers / Sagas
Coordinate complex workflows that span multiple aggregates with compensating actions.
Policy/Reactor Pattern
Simple event listeners that trigger commands on other aggregates.
- •God Aggregates: Creating massive aggregates that handle too much responsibility
- •Anemic Aggregates: Aggregates with no behavior, just getters and setters
- •Ignoring Invariants: Not identifying and enforcing business rules properly
- •Direct Aggregate References: Holding references to other aggregate instances
- •Multi-Aggregate Transactions: Trying to modify multiple aggregates in one transaction
- Aggregates are consistency boundaries that enforce business invariants
- One aggregate = one event stream = one transaction
- Design aggregates around business invariants, not data relationships
- Start with small aggregates and grow only when invariants require it
- Use eventual consistency and events to coordinate across aggregates