Designing Aggregates

Learn how to design effective aggregates that serve as consistency boundaries in event-sourced systems.

What is an Aggregate?

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

Aggregate Responsibilities
  • 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
Aggregate Characteristics
  • 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
Sizing Aggregates Correctly

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.

Design Guidelines

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.

✓ Order has customerId: string
✗ Order has customer: Customer

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.

✓ Order, ShoppingCart, CustomerAccount
✗ OrderManager, CartService, UserData
Common Aggregate Patterns

Document-Style Aggregate

A single entity with properties and simple value objects. Most aggregates should start here.

Order: orderId, customerId, orderDate, items[], totalAmount, status

Collection-Style Aggregate

A root entity managing a collection of child entities. Use when children have identity and lifecycle.

ShoppingCart (root)CartItems[] (entities with IDs)

State Machine Aggregate

An aggregate that progresses through well-defined states with strict transitions.

Order: DraftPlacedConfirmedShippedDelivered
Aggregate Lifecycle
1

Creation

Created by a command that validates preconditions and emits a creation event (e.g., OrderPlaced)

2

Active State

Processes commands, enforces rules, and emits events as state changes occur

3

Completion

Reaches a terminal state through a completion event (e.g., OrderCompleted, AccountClosed)

4

Archive (Optional)

After completion, the aggregate may be archived but events remain immutable

Handling Cross-Aggregate Consistency

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.

Order.OrderPlacedInventory.ReserveStockPayment.ProcessPayment

Process Managers / Sagas

Coordinate complex workflows that span multiple aggregates with compensating actions.

OrderSaga coordinates: Reserve inventoryProcess paymentShip order

Policy/Reactor Pattern

Simple event listeners that trigger commands on other aggregates.

When PaymentReceivedSendConfirmationEmail
Common Mistakes
  • 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
Key Takeaways
  • 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