The repository pattern: hiding storage behind a domain interface
What a repository is, how it keeps business logic independent of the database, how to write one well, and the traps: leaky abstractions and generic repositories.
On the roadmap:Software Design · step 2: Design patterns
Lesson 5 of 6 · 9 min read · quiz · exercise
The problem
Business logic such as "an order can only be cancelled before shipping" should not care whether orders live in PostgreSQL, DynamoDB or a test double. If the rule code is full of SQL, ORM calls and table names, it is hard to read, hard to test and hard to change.
A repository is an object that looks like an in-memory collection of domain objects, and hides how they are loaded and saved.
Figure SDA-SQR-05-1 · Layers around a repository · © forcsguy.com
A small example
interface OrderRepository {
nextId(): string;
findById(id: string): Promise<Order | null>;
findOpenForCustomer(customerId: string): Promise<Order[]>;
save(order: Order): Promise<void>;
}
class InMemoryOrderRepository implements OrderRepository { /* a Map, for fast tests */ }
class PgOrderRepository implements OrderRepository { /* SQL here, and only here */ }
The application service receives an OrderRepository, so tests can pass the in-memory version and production passes the PostgreSQL one.
Guidelines
- One repository per aggregate root. An order and its line items are saved together; do not give every table its own repository.
- Name methods after business questions (
findOpenForCustomer), not storage mechanics (selectWhere). - Return domain objects, not ORM entities or rows. Keep mapping inside the implementation.
- Keep transactions in the application layer (or a unit of work), so one use case can change several aggregates atomically.
- Mind the queries. Load what the use case needs in one go to avoid the N+1 problem, and give read-heavy screens a separate read model instead of bending the repository.
Figure SDA-SQR-05-2 · Writes through repositories, reads through a read model · © forcsguy.com
When it is not worth it
- Tiny CRUD apps where the domain is just the table: the ORM already is the abstraction, and a repository adds ceremony.
- Generic repositories (
Repository<T>withfind(filter)) just re-export the database through another name and defeat the purpose. - Pretending storage is interchangeable. Switching from SQL to a document store changes consistency and query models, not just the class behind the interface. Design the interface for what your domain needs and accept that some choices are real commitments.
Real-world reading
Check your understanding
Question 1 of 3
0 correct so far
What does a repository give the domain code?
Go deeper
- Repository by Martin Fowler: the original catalogue entry
- Domain-Driven Design by Eric Evans: repositories in the context of aggregates
- Patterns of Enterprise Application Architecture by Martin Fowler: unit of work and data mapper
Practise it: The database goes down
Everything below is already set up for this lesson: change the design, run the traffic, and see whether you meet the goal.
- 1The task
- 2Build and test
Ten seconds in, the primary database fails. Traffic is 600 requests per second and 30% of it is writes. A failover would take a minute or more.
Goal: Keep errors under 5%: reads should keep working and writes must not be lost.
Two things can still serve while the primary is down: something that already holds a copy of the data, and something that can hold writes until the primary returns.
Try it yourself
Design a repository for an Order aggregate (order with line items). Decide its methods, where transactions live, and how you avoid the N+1 query problem.
Click what a strong design here would need:
More in Building blocks
© 2026 forcsguy.com. Original lesson text and diagrams. Books and sites linked above belong to their authors.