Repository
Put a collection-like interface between domain logic and data access, so business code depends on `find`/`save` instead of SQL, an ORM, or an API client.
The problem
OrderService.cancelOrder() needs to look up an order, cancel it, and save the change. The
most direct way to write that also welds the method to SQL: a query string, a row-to-object
conversion, an UPDATE statement, all inline in business logic that has nothing to do with
databases.
Two consequences follow immediately. Testing cancelOrder() now requires a real database,
because the SQL is inseparable from the logic. And if the company ever swaps Postgres for
something else, or adds a cache in front of it, every method that touched the database
directly needs surgery.
The solution
Describe the storage need the way the domain actually thinks about it - not "run this query" but "find an order by id" and "save this order" - and put that description in an interface. Business logic depends on the interface, never on the database driver underneath it.
One implementation of that interface talks to the real database. A second, much simpler one
keeps everything in a plain in-memory map. Both honor the exact same contract, so
OrderService runs identically against either one - it has no way to tell which storage
technology, if any, is on the other side.
- 1In production, the service is handed a repository backed by a real database.
- 2cancelOrder() asks the repository for the order, through the interface, never through SQL directly.
- 3PostgresOrderRepository does whatever a real database call requires. The service never sees this line.
- 4Business logic runs entirely in domain terms: an Order object, a cancel() method, no rows or columns in sight.
- 5The service persists the change the same way it read it: through the interface.
- 6A unit test wires up the exact same service, but with an in-memory stand-in - no database container, no network.
- 7Identical call, identical service code. This time it hits a plain map instead of a table.
- 8The test runs in milliseconds and never touched a database - because the domain logic never knew there was one.
Structure
OrderService has exactly one dependency in this diagram, and it is an interface. Both
concrete repositories sit beside each other as interchangeable implementations - the diagram
has no arrow at all between OrderService and either concrete class.
Code
Same example three ways: an order-cancellation flow that never mentions SQL.
When to use it
- Business logic currently mixes domain rules with query strings, ORM sessions, or storage-specific error types, and you want the two concerns physically separated.
- Tests need to run fast and offline against real logic, not against mocks that fake what a database would have done.
- The storage technology is genuinely likely to change, or already varies across environments (a real database in production, an in-memory store in tests).
Pitfalls
- The leaky repository. Returning ORM entities, accepting raw SQL fragments as parameters, or exposing pagination cursors tied to one database engine all defeat the point - the domain layer is still coupled to the storage technology, just through an extra hop.
- One repository per query. A repository is a collection abstraction, not a home for
every ad-hoc report. A dashboard query that joins six tables for a chart usually belongs
in its own read-side query object, not bolted onto
OrderRepository. - Fighting the ORM. If the team already uses an ORM with its own unit-of-work and querying story, wrapping it a second time in a hand-rolled repository can add a layer that answers a question nobody asked.
Don't confuse it with
- Facade. A Facade picks one simple door into a larger, messier subsystem; it is not collection-shaped and does not promise find/save semantics. A Repository specifically models "a collection of domain objects", nothing broader.
- Adapter. Adapter exists because two interfaces already exist and need to cooperate. Repository does not adapt an existing storage interface - it invents a domain-shaped one that the storage layer never had.
- Data Access Object (DAO). In practice the two overlap heavily; the usual distinction is that a DAO tends to mirror the storage schema (table-shaped methods), while a Repository is defined in the domain's vocabulary and may aggregate several tables behind one method.
Check yourself
What is the domain layer allowed to import in a properly applied Repository pattern?