Invariants and Aggregates in Domain-Driven Design
- Pavol Megela
- Sep 9, 2022
- 4 min read
Updated: Mar 9

In Domain-Driven Design (DDD), aggregates are a key concept that help maintain the integrity. They act as boundaries, ensuring that critical business rules (invariants) are always followed. But what exactly are invariants, and how do aggregates help enforce them? Let’s break it down.
What are Invariants and why should you care?
An invariant is a rule that must always be true within a system. If an invariant is broken, the system is in an invalid state.
For example:
An order total must always equal the sum of its line items.
A bank account balance cannot go below an allowed overdraft limit.
Enforcing these rules is crucial to ensuring the correctness of the system. But where should these rules be enforced? That’s where aggregates come in.
Aggregates?
An aggregate is a collection of related entities that must be kept consistent. It has a single entry point which ensures that all changes follow the business rules.
For example, in an Order aggregate:
The Order entity is the aggregate root
It contains Order Items, if those items cannot exist independently
The Order aggregate ensures that no new items are added after checkout
This means all changes within the Order aggregate are atomic and follow the necessary business rules.
Designing Aggregates
To design aggregates effectively:
Keep them small - Aggregates should enforce only the most critical invariants
Identify what must be strongly consistent - Some rules (like order totals) require immediate consistency, while others (like stock updates) can be eventual
Ensure the aggregate root controls modifications - No other part (aggregate) of the system should change its internal state directly
Key points to keep in mind:
Use an aggregate root (Order) to control updates instead of modifying OrderItem directly
Business rules (total price calculation) belong in the aggregate, not in the service
Only the aggregate root (Order) should be retrieved from a repository, not its children (OrderItem)
A well-designed aggregate balances consistency, performance, and scalability by grouping entities that share strong invariants.
Should It Be a Separate Aggregate or Part of an Existing One?
When introducing a new entity, ask yourself these questions:
Does it share strong business rules (invariants) with an existing aggregate?Yes: Keep it within the existing aggregate.
No: Consider making it a separate aggregate.
Do changes to this new aggregate always need to be atomic with an existing aggregate ?
Yes: It belongs in the same aggregate.
No: It can be separate.
Does it have a different lifecycle than the existing aggregate?
Yes: Make it a separate aggregate.
No: Keep it together.
Would including it make the aggregate too large and complex?
Yes: Consider separating it.
No: Keep it in the existing aggregate.
Example
When designing aggregates in Domain-Driven Design (DDD), we need to identify the business rules (invariants) that must always hold true. Let’s look at this example:
We have a Product in an e-commerce system, and now we want to add a functionality that will also track its Stock Level. First lets define both.
Product Entity
Represents an item that a business sells. A product has:
ID
Name “Wireless Mouse”
Price 25.99 €
Category “Electronics”
Description "Fits perfectly in your hand"
A product defines the item business is selling, or were selling in the past.
Stock (New Entity we're adding to our Application)
Tracks how many units of a product are available. Stock has:
• Stock ID
• Product ID - Reference to a product
• Quantity - 50 units available
• Warehouse Location - “Warehouse A”
Stock is dynamic, it frequently changes when sales happen, new shipments arrive, or stock is adjusted.
We ask ourselves, what are the key invariants of our new aggregate being added to the system "Stock" ?
Stock levels cannot go below zero (we can't sell what we don't have)
Stock updates should not block product updates and should no depend on them (renaming a product should not require stock adjustments)
Should Stock Be Part of the Product Aggregate?
Let’s apply our rules:
Does stock share strong invariants with product aggregate?
Product Name and Price must always be valid, but Stock does not affect these directly.
Conclusion: Stock doesn’t share strong invariants with Product
2. Does product and stock need atomic updates?
If we update a product’s name or price, we don’t need to update the stock at the same time.
However, if a purchase happens, stock must be updated to prevent overselling. Conclusion: No, the atomic updates are not needed. Stock should be managed separately but communicate with Product when needed.
3. Does product and stock have different lifecycles?
A product can exist without stock (out-of-stock products would still exist).
A stock level can change frequently, while product details remain stable.
Conclusion: Yes product and stock do have different lifecycles.
Final Decision
Since Stock Level:
• Does not share strong invariants with Product,
• Does not require atomic updates with Product details, and
• Has a different lifecycle,
Stock should be a separate aggregate.
Conclusion
Invariants define what must always be true in a system.
Aggregates ensure those rules are enforced consistently.
Use simple rules to decide whether an entity should be part of an existing aggregate or a new one.
A well-structured aggregate prevents invalid states, messy architecture, improves performance, and keeps the system scalable.
Comments