Repository & Unit of Work — Detailed#
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 |