Repository & Unit of Work#
Problem statement (interviewer prompt)
Design the persistence layer for an order management service: aggregate roots (Order, Payment), one repository per aggregate, a Unit of Work for transactional boundaries, and the ability to swap Postgres for an in-memory fake in tests.
classDiagram
class UseCase {
+place(order)
}
class OrderRepository {
<<interface>>
+findById(id)
+save(order)
}
class SqlOrderRepo
class InMemoryOrderRepo
class UnitOfWork {
+begin()
+commit()
+rollback()
}
UseCase --> OrderRepository
OrderRepository <|.. SqlOrderRepo
OrderRepository <|.. InMemoryOrderRepo
UseCase --> UnitOfWork
A Repository is a collection-like interface for one aggregate type. A Unit of Work scopes one logical transaction across multiple repositories. Together they hide persistence from the domain.
Repository#
A type-specific, collection-like API for one aggregate:
interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId c);
void save(Order o); // insert or update
void delete(OrderId id);
}
- Returns domain entities, not rows or DTOs.
- One repository per aggregate root. No repository for
OrderLine. - Implementations live in adapter layer (SqlOrderRepo, InMemoryOrderRepo, MongoOrderRepo).
Unit of Work#
Scope of a logical transaction. Track all entities touched in a use case; commit / rollback atomically.
try (var uow = unitOfWorkFactory.start()) {
var order = uow.orders().findById(id).orElseThrow();
order.markPaid();
uow.payments().save(payment);
uow.commit();
} catch (Exception e) {
// rollback is automatic on close-without-commit
}
- Backed by a DB transaction (Postgres BEGIN/COMMIT, JPA EntityManager).
- Often also flushes domain events at commit.
Why both#
- Repository = collection-like, type-safe access.
- Unit of Work = transactional boundary, single commit.
- Without UoW, repositories independently commit and you lose atomicity.
Alternatives#
| Repository + UoW | Active Record | DAO | |
|---|---|---|---|
| Concept | aggregate-level, app-owned tx | one class = one table + persistence methods | data-access object per table, no domain shape |
| Domain logic location | domain entity | the row class itself | service layer |
| Test isolation | swap repository for in-memory fake | hard - bound to DB | medium |
| Examples | DDD code, .NET EF + UnitOfWork, SQLAlchemy session | Rails ActiveRecord | classic Java EE |
Specification pattern (companion)#
When queries get complex, push them out of the repository:
interface Spec<T> { boolean isSatisfiedBy(T t); SqlFragment toSql(); }
class OverdueOrders implements Spec<Order> {
public boolean isSatisfiedBy(Order o) { return o.dueDate.isBefore(now); }
}
repo.find(new OverdueOrders().and(new ByCustomer(c)));
The repository stays small; specifications become composable.
Anti-patterns#
- God repository:
findAll,findByX,findByY,findByXAndY... - refactor to specifications or smaller repos. - Repository leaks ORM types: returning
Page<Entity>from Spring Data exposes paging behaviour to the domain; wrap or hide. - Mocking a real DB in unit tests: prefer an in-memory port fake; integration tests use real DB.
- Bypassing the repository: use cases issuing raw SQL - the architectural rule is broken.
Where this shows up#
- Every LLD problem with persistence - order, ticket, library member, room booking.
- Twitter / Instagram / News feed -
TweetRepo,FeedRepo. - Payment gateway -
PaymentIntentRepo,ChargeRepo.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Event sourcing + CQRS | commands -> events; separate read model | event-sourcing-cqrs |
LLD |
Repository + Unit of Work | aggregate repos, transactional boundary | repository-pattern |
LLD |
DDD tactical | entity / value object / aggregate / event | ddd-tactical |
LLD |
Structural patterns | Adapter, Decorator, Facade, Proxy, Composite | structural-patterns |
LLD |
Concurrency primitives | mutex, semaphore, RW lock, atomic, CAS | concurrency-primitives |
LLD |
Error handling | exceptions vs Result, error boundaries | error-handling |
Quick reference#
Sizing#
- One repository per aggregate root, period.
- Repository methods follow named-query convention (
findActiveByCustomer). - Pagination: cursor or page object as parameter, not exposed ORM type.
Concurrency#
- Repository updates should respect aggregate version (
@Versionin JPA,xminin Postgres). - UoW commit catches version-conflict exceptions for retry by caller.
Caching#
- Read-through cache in the repository implementation (transparent to use cases).
- Invalidate on save / delete.
Testing#
- Use cases test against
InMemoryOrderRepo- fast, deterministic. - Adapter test suite runs the same contract against the real DB.
Refs#
- Eric Evans: Domain-Driven Design - chapter on repositories.
- Martin Fowler: Patterns of Enterprise Application Architecture - Repository, Unit of Work, Data Mapper.
- Vaughn Vernon: Implementing DDD.
- "Architecture Patterns with Python" - Cosmic Python.
FAQ#
What is the repository pattern?#
It is a collection-like interface for one aggregate type that hides persistence details. The domain talks to the interface; concrete repositories handle SQL or other storage.
What is the difference between a repository and a DAO?#
A DAO maps one table to CRUD methods. A repository serves whole aggregates and stays close to domain language, often spanning several tables.
What is a Unit of Work?#
A Unit of Work tracks changes across one or several repositories and commits them in a single transaction. It maps to a database transaction or an outbox flush.
Should every entity have its own repository?#
No. Only aggregate roots get repositories. Internal entities are loaded and saved through the root repository so consistency is preserved.
Does the repository pattern hurt query performance?#
It can if you fetch full aggregates for read-only screens. Use a separate read model or specification for queries while keeping the write side aggregate-shaped.
Related Topics#
- Clean Architecture: the repository is the canonical persistence adapter in clean and hexagonal architecture
- Dependency Injection: DI is how repositories are wired into application services without coupling
- DDD Tactical Patterns: repositories are the persistence boundary for DDD aggregates
Further reading#
Curated, high-credibility sources for going deeper on this topic.