Aggregate and Aggregate Root in DDD
Fast recall
Quick revision
Core answer
Aggregate as a consistency boundary, the root as the only allowed reference, the four design rules, and how aggregate size affects performance / concurrency.
Modeling huge aggregates ('Customer with all their orders, addresses, payments') that lock the world on every change, or referencing other aggregates by object instead of by ID.
Discuss optimistic concurrency, eventual consistency between aggregates, and how aggregate boundaries map to repositories and microservices.
Interview-ready guide
Interview answer
The problem aggregates solve
Domain models without explicit consistency boundaries develop two pathologies:
- 1Inconsistent state — two requests both read an order, both add a line, both save; one overwrites; total no longer matches items
- 2Lock contention — a Customer god-object containing all orders/addresses/payments becomes contention hotspot; every order-placement locks the whole customer
Aggregates fix both:
- Aggregate is a small cluster (entities + value objects) that change together
- Aggregate root is the entrance — external code holds reference only to the root
- One transaction modifies one aggregate; multi-aggregate consistency is eventual
- Invariants spanning the aggregate enforced inside the root
Anatomy
Order aggregate: Order (root), OrderItems, ShippingInfo, PaymentMethod (VO). External code only allowed to touch Order. Internal entities reachable only through root. References to other aggregates (Customer, Product) by ID, not by object.
Code example
Order class with OrderId, CustomerId (by ID), List
OrderItem created only through Order.addItem(); no external new OrderItem reachable.
The four rules
Rule 1: Reference other aggregates by ID, not object. private CustomerId, not private Customer. Object reference triggers lazy load, hidden joins, hidden contention.
Rule 2: One transaction, one aggregate. Multi-aggregate updates across transactions, coordinated via domain events and eventual consistency. Don't change customer.recordOrder(order) and orderRepo.save(order) in same transaction — publish OrderPlacedEvent; customer updates in a separate transaction.
Rule 3: Aggregates should be small. Customer with 5000 orders inside = contention, heavy loads, memory pressure. If thousands of internal entities, too big.
Rule 4: All invariants that must hold transactionally live inside one aggregate. 'Order total = sum of items' must always be true → items inside order aggregate. 'Customer max 3 active orders' → either model inside customer or relax to eventually consistent (preferable).
Optimistic locking on the root
@Version on root. JPA increments version on UPDATE, adds WHERE version = ?. Concurrent updates: second sees 0 rows updated → OptimisticLockException. Use case retries or returns 409 Conflict.
Works at aggregate level: root carries version; updating any internal entity bumps root's version. Concurrency detected at consistency-boundary level, not per-row — matches design intent.
Aggregates and repositories
One repository per aggregate. Deals with root only. OrderRepository.save(order), findById(orderId), findActiveByCustomerId(customerId).
Anti-pattern: OrderRepository + OrderItemRepository + ShippingInfoRepository. Fragments the aggregate, leaks persistence model upward, makes use case orchestrate four saves for one logical change.
Right: orderRepo.save(order) persists root + internal entities atomically. Persistence adapter handles multi-table mapping internally.
Cross-aggregate consistency — domain events
Order.submit() raises OrderSubmittedEvent. Use case publishes events after persistence. Other aggregate consumes in its own transaction (Customer.recordOrder).
Eventually consistent. Brief window where Order submitted but Customer hasn't recorded. For reliable delivery: transactional outbox — events written to outbox table in same transaction; background process publishes from outbox to Kafka.
Common mistakes
- Object references to other aggregates — hidden joins/lazy loads/contention
- Multiple aggregates in one transaction — breaks boundary
- Huge aggregates — contention and load explode
- Externally constructing internal entities — breaks invariant enforcement
- No optimistic lock — last-write-wins corruption
- Returning mutable internal collections — callers bypass Order.addItem; use defensive copies
- Repository per JPA entity — fragments aggregate
- Trying to enforce all invariants transactionally — some are inherently cross-aggregate (uniqueness across all customers); model as eventual
Recommendation
Design aggregates as small consistency boundaries: the root is the only external entry point, internal entities are reachable only through the root, references to other aggregates are by ID, and one transaction modifies one aggregate. Use optimistic locking (@Version) on the root, one repository per aggregate that persists root and internals atomically, and domain events plus transactional outbox for cross-aggregate consistency. Keep aggregates small — if you find yourself loading thousands of internal entities or contending on a single root, the aggregate is too big and needs splitting.
Aggregate and Aggregate Root in DDD
Direct Answer
An Aggregate is a cluster of domain objects treated as a single consistency boundary — the unit of transactional change. The Aggregate Root is the one entity inside the aggregate that external code is allowed to reference; all access to inner objects goes through the root, which enforces invariants. Rules: (1) one transaction modifies one aggregate; (2) reference other aggregates by ID, not by object reference; (3) keep aggregates small for concurrency; (4) all invariants that must hold transactionally live inside one aggregate. This is what makes DDD's tactical patterns scale and stay consistent.
Common Mistakes & Pitfalls
Modeling huge aggregates ('Customer with all their orders, addresses, payments') that lock the world on every change, or referencing other aggregates by object instead of by ID.
Check your understanding
Answer these without looking back at the article.
- Why is referencing another aggregate by object instead of by ID a problem?
- How do you implement optimistic locking with JPA at the aggregate root level?
- Walk through cross-aggregate consistency with the transactional outbox pattern.
- What's wrong with a repository per JPA entity instead of per aggregate?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.