LESSON 8.1
Repository and unit of work
Decouple domain entities from persistence mechanics with Repository, and track transactional changes with Unit of Work.
6 min read
When business logic directly constructs SQL queries or document database calls, domain rules become inextricably tangled with persistence libraries. Swapping from PostgreSQL to DynamoDB or writing fast in-memory unit tests becomes impossible without spinning up real database containers. Furthermore, without a transaction coordinator, partial database updates leave the database in an inconsistent state.
The problem
// Tangled business logic and raw SQL:
class OrderService {
async completeOrder(orderId) {
// Domain rules mixed with DB drivers
const [row] = await db.query("SELECT * FROM orders WHERE id = $1", [orderId]);
if (row.status !== "PENDING") throw new Error("Invalid state");
await db.query("UPDATE orders SET status = $1 WHERE id = $2", ["PAID", orderId]);
await db.query("UPDATE inventory SET stock = stock - 1 WHERE sku = $1", [row.sku]);
// If the 2nd query fails, orders is marked PAID but stock was not deducted!
}
}The pattern
The Repository pattern provides an in-memory collection-like interface (findById, save, delete) for domain entities. Domain services interact with the repository without knowing whether data is backed by Postgres, MongoDB, or an in-memory mock.
The Unit of Work pattern keeps track of all dirty, new, and deleted entities within a business transaction, committing all changes in one atomic database operation.
In code
When to use it, and when not to
Use Repository and Unit of Work when:
- You are developing complex domain logic with Domain-Driven Design (DDD) where domain models should remain decoupled from persistence libraries.
- You need to mock data layers easily for unit tests without running real databases.
- You want to batch database writes or ensure transactional consistency across multiple mutated aggregate roots.
Don't use Repository and Unit of Work when:
- You are building a simple CRUD or reporting application: active record patterns (e.g. Prisma, TypeORM, Rails ActiveRecord) or direct query builders are simpler and faster.
Where you have already seen it
In Java, Hibernate / JPA's EntityManager is the quintessential Unit of Work and Repository implementation. In modern TypeScript, MikrORM and TypeORM provide explicit EntityManager.flush() and repository patterns. In Spring Data, JpaRepository and CrudRepository define repository interfaces implemented at runtime.
Related patterns
- Repository vs DAO: A Data Access Object maps closely to database tables. A Repository operates on domain aggregates.
- Unit of Work vs Memento: Unit of Work can use Mementos to track original entity states for dirty checking during commit.
Try it
Double-entry Ledger Model LLD
Build an accounting ledger service using a Repository and Unit of Work to enforce balance parity across accounts.