Repository Pattern in DDD
Fast recall
Quick revision
Core answer
findActiveByCustomerId, save(order)) and returns whole aggregates. One repository per aggregate, owned by the domain layer, implemented in the persistence adapter (which can use Spring Data JPA, JDBC, MongoDB, or in-memory under the hood). The repository persists the aggregate atomically — root and all internal entities together — and is the only way external code touches storage for that aggregate.Repository as a domain abstraction (not an infrastructure interface), aggregate-per-repository, why it's not just `JpaRepository`, and how it preserves the consistency boundary.
One repository per JPA entity, returning JPA entities upward, generic CRUD on every aggregate, or putting business logic in repositories.
Discuss specification pattern for complex queries, CQRS-style query services for read paths, and the relationship between aggregate and repository boundary.
Interview-ready guide
Interview answer
What a DDD repository is
Collection-like abstraction over aggregates. Ask it for an aggregate by ID or domain criteria; hand it an aggregate to save. Internally translates between aggregate's shape and storage; externally speaks domain language.
Is not: wrapper for every table, generic CRUD interface, query API for arbitrary projections, place for business logic.
Is: one per aggregate, returns whole aggregates (root + internals), owned by domain layer (interface), implemented by adapter (class), single point of access to storage for that aggregate.
Anatomy
Port lives in domain (or application) layer; defined in domain language. Adapter lives in persistence package; uses Spring Data, JDBC, MongoDB, anything. Use case depends on port, never adapter.
Port + adapter code
Domain: OrderRepository interface with save(Order), findById(OrderId), findByIdempotencyKey(String), findActiveByCustomerId(CustomerId).
Adapter: OrderJpaEntity with @Entity, @Version, @Embedded MoneyEmbeddable, @ElementCollection items. OrderSpringRepo extends JpaRepository with JPA-specific finders. JpaOrderRepository implements OrderRepository, uses OrderMapper to convert.
Use cases see only OrderRepository. JPA invisible above the adapter.
One repository per aggregate, not per table
Anti-pattern: OrderRepository + OrderItemRepository + ShippingInfoRepository + PaymentMethodRepository. Use case orchestrates four saves for one logical change. Aggregate boundary leaks.
Right: just OrderRepository. Persists Order with its items, shipping info, payment method together. Persistence adapter handles multi-table mapping internally.
Aggregate boundary equals repository boundary. New repository implies new aggregate. If you want OrderItemRepository to update an item in isolation, you're fragmenting an aggregate that should stay whole.
Complex queries
Repositories awkward for: reporting/analytics, complex search, projection DTOs for UI screens that don't match aggregate shape.
Two patterns:
- 1
Specification pattern — build composable query criteria, hand to repository. repo.findAll(activeOrdersFor(customerId).and(placedSince(...))). Useful for moderately complex queries still returning aggregates.
- 2
CQRS-style query services — separate interface returning DTOs. OrderQueryService.findSummary(orderId), search(criteria, page), monthlyStats(month). Returns DTOs not aggregates. Implementation can use SQL views, JPA projections, separate read model. Keeps OrderRepository focused on aggregate lifecycle.
For read-heavy systems, separating commands from queries is the practical move.
What belongs in the repository
Belongs: find/save/delete for the aggregate, domain-meaningful finders, optimistic-lock translation to domain exceptions.
Doesn't belong: business logic (that's domain service), cross-aggregate coordination (use case's job), sending emails/publishing events/calling external APIs, caching policy (push to a cache adapter that wraps the repo), generic findAll returning unbounded result sets.
Repository persists; doesn't decide. Decisions in entity (invariants), use case (orchestration), or domain services (multi-aggregate logic).
Common mistakes
- Use case importing JpaRepository — framework independence lost
- One repository per JPA entity instead of per aggregate
- Returning JPA entities — use case coupled to JPA lifecycle
- Generic findAll returning millions of rows
- Business logic in repositories — cancelExpired() instead of an explicit use case
- Specification API leaking into use cases — adapter handles query construction
- Mixing read and write models — separate query services for read paths
- Inconsistent transaction semantics — silent transactions surprise the use case
Recommendation
Define one repository per aggregate as a port in the domain layer, written in domain language, returning whole aggregates. Implement in the persistence adapter using Spring Data JPA (or whatever) as the underlying tool — wrap, don't expose. For read-heavy queries that don't match the aggregate shape, add separate CQRS-style query services returning DTOs. Repository persists; doesn't decide — keep business logic in entities, use cases, and domain services. The aggregate boundary equals the repository boundary; if you want a new repository, ask whether you actually need a new aggregate.
Repository Pattern in DDD
Direct Answer
A DDD Repository is a collection-like abstraction for retrieving and persisting aggregates (not arbitrary entities or DB rows). It exposes the operations the domain needs in the domain's language (findActiveByCustomerId, save(order)) and returns whole aggregates. One repository per aggregate, owned by the domain layer, implemented in the persistence adapter (which can use Spring Data JPA, JDBC, MongoDB, or in-memory under the hood). The repository persists the aggregate atomically — root and all internal entities together — and is the only way external code touches storage for that aggregate.
Common Mistakes & Pitfalls
One repository per JPA entity, returning JPA entities upward, generic CRUD on every aggregate, or putting business logic in repositories.
Check your understanding
Answer these without looking back at the article.
- Why is one repository per aggregate (not per JPA entity) the right granularity?
- When would you reach for the Specification pattern vs CQRS-style query services?
- How do you persist an aggregate atomically that maps to multiple JPA tables?
- Where does optimistic-lock handling live — the entity, repository, or use case?
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.