Skip to content

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 (@Version in JPA, xmin in 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.

Further reading#

Curated, high-credibility sources for going deeper on this topic.