Home Design Patterns Repository
Repository
Modern PracticePut a collection-shaped interface in front of storage so the domain never sees the database.
Purpose
A Repository lets the rest of the program ask for domain objects the way it would ask a collection: give me the one with this identity, give me the ones that match this condition, add this one, remove that one. Behind the interface there is a database, an ORM, a web service or a file, and the calling code does not express any opinion about which.
The part that earns its keep is not the hiding, it is the vocabulary. A
repository whose methods are named after questions the business asks
(readyToRun(), overdueFor(site)) puts the definition of
those questions in one place with a name. A repository whose methods are named
after database operations (query, where,
execute) has hidden nothing and named nothing; it is a database
driver with a longer import path.
Design
The interface belongs to the domain and the implementation belongs outside it, so the dependency points inward: the domain declares what it needs to ask, and the persistence layer answers. There is normally one repository per aggregate root rather than one per table, because the unit the domain deals in is a whole consistent object, not a row.
Return domain objects, never rows, cursors or database types, and never a query object either. The moment a repository returns something the caller can go on composing against the database, the boundary is decorative: the query is still being written in the domain, just in a different syntax, and the repository can no longer tell what will actually be executed or promise anything about it.
Writes are the awkward half. A collection-shaped interface implies that
add takes effect immediately, which fights transactions that span
several repositories. The usual answer is a unit of work that owns the transaction
and commits once, with the repositories recording intent rather than issuing
statements, and most ORMs already provide exactly that.
Example
The Autobot Robotics scheduler decides which jobs are ready to run, and "ready" is a real definition: queued, not canceled, its program revision approved, its cell not in maintenance, and no interlock held on the arm it needs. That condition existed as SQL in six places. Three of them were subtly different, because each had been written by copying the nearest one and adding whatever the new caller needed. The version in the overnight batch had never been given the maintenance clause, so on the night a customer took a cell out of service for a gearbox change, the batch dispatched jobs to it and they all timed out.
A JobRepository with a readyToRun(cellId) method
fixed that, and it is worth being precise about why: not because the database is
hidden, but because the definition of "ready" now exists once, has a name, and
changing it is a single edit in a file that a reviewer can find. When the
interlock rule changed the following year, it changed in one place.
The same team also wrote a CellRepository at the same time, with
findById, findAll and save, each of which
forwarded one line to the ORM. It was deleted within a year. It expressed nothing
the ORM did not already express, it had to grow a method every time anyone needed
anything, and its only real effect was that reading the code took one more hop.
The two are the same pattern applied with and without a reason, and the difference
between them is the whole argument.
// Domain side. Named for the question, not for the query.
interface JobRepository
{
Job ofId(JobId id);
Job[] readyToRun(CellId cell);
void add(Job job);
}
class Scheduler
{
private readonly JobRepository jobs;
public Scheduler(JobRepository r) { jobs = r; }
public void dispatchNext(CellId cell)
{
Job[] ready = jobs.readyToRun(cell);
// pick one and send it to the arm
}
}
// Persistence side. The only place that knows what a column is.
class SqlJobRepository : JobRepository
{
private readonly Database db;
public Job[] readyToRun(CellId cell)
{
// ONE definition of "ready", in one file:
// queued, not canceled, revision approved,
// cell not in maintenance, no interlock held
return map(db.query(READY_SQL, cell));
}
}
// Test side. No database, and the domain cannot tell.
class InMemoryJobRepository : JobRepository
{
private readonly List<Job> all;
public Job[] readyToRun(CellId cell)
{
// same rule, expressed in code
}
}
readyToRun is a
sentence the business would recognize, and it is the reason this interface is
worth having; an interface of findAll and save would
not have been.Seen in the Wild
Spring Data JPA is the most widely deployed version: you declare an interface
extending CrudRepository, and method names such as
findByStatusAndCellId are parsed into queries at start-up, so the
domain-language naming is enforced by the framework rather than by discipline.
Doctrine does the same thing for PHP with explicit
EntityRepository classes. In .NET, Entity Framework's
DbSet is itself a repository and DbContext is a unit of
work, which Microsoft's own documentation says plainly, and which is the reason so
many hand-written repository layers in .NET codebases add nothing. Rails takes the
opposite position with ActiveRecord, where the entity is its own persistence
layer, and it is worth reading the two side by side because the trade is a real
one and Rails is not obviously wrong. The name and the definition come from
Fowler's Patterns of Enterprise Application Architecture and Evans's
Domain-Driven Design, both a few years after the Gang of Four book.
Comparisons
- Aggregate is what a repository should be built around. One repository per aggregate root, handing back whole consistent objects, is the version of this pattern that has a rule behind it; one repository per table is a naming convention.
- Facade is the closest structural relative, since both put a small deliberate interface in front of something larger. The difference is that a Facade simplifies a subsystem while keeping its concepts, and a Repository replaces the subsystem's concepts with the domain's.
- Dependency Injection is what makes the seam usable. The domain holds the interface, and which implementation it gets is decided at the composition root, which is the only reason the in-memory version in the example is possible.
- Lazy Load and Proxy are what ORMs put inside the objects a repository returns, so that a Job can expose its history without every query fetching it. That is also where a repository's performance surprises come from.
- Module is the broader idea: a repository is one application of drawing a narrow public surface around a body of knowledge, in this case knowledge about storage.
Why It Exists
My take
Repository exists because query text spreads and because it carries meaning
that nothing else in the system carries. A five-line WHERE clause is
frequently the only written definition of a business concept, and when it is
pasted into six call sites there are six definitions that drift apart silently.
Nobody notices, because each one still returns rows and none of them looks
wrong. The maintenance clause missing from the overnight batch is the whole
pattern in one bug: the cost was not that the code touched the database, it was
that the definition had no home.
The second pressure is the direction the schema pushes. Code written directly against tables ends up shaped like the tables, and then talks in joins and column names, and then only people who know the schema can read it. Over the life of an ERP that is a real cost, because the schema is the thing most likely to be awkward for historical reasons and least likely to be renamed. A repository is a place to translate once, at the edge, so that the awkwardness stops there.
I want to be honest about the version of this pattern I see most often, which
is a wrapper over an ORM that is already a repository. Entity Framework's
DbSet gives you find, add and remove over a mapped aggregate;
wrapping it in an interface with the same three methods and a different name
produces a layer that has no knowledge in it and one more file to open on the way
to the answer. It is usually justified by testability, and that justification is
weaker than it looks, because an in-memory fake does not behave like the database
and the tests that pass against it are not evidence about production.
So my position is that the useful repository is the one expressed in domain
language, and that the test for whether you have written one is to read the
method names aloud to somebody who does not program. readyToRun,
overdueFor, awaitingApproval: those are worth an
interface, because each one is a definition that would otherwise be scattered.
findAll, getById and saveChanges are not
worth an interface, because they are the ORM's vocabulary with a coat of paint,
and you already have the ORM.
Criticisms
The most common repository in the wild is a pass-through that forwards each method to an ORM and adds nothing but a hop. It gets built because the layer is on the architecture diagram, and it survives because deleting a layer feels riskier than keeping it. If every method of your repository is one line long and that line mentions the ORM, the layer is not an abstraction, it is a rename.
Even a good repository leaks. Paging, sorting, eager versus lazy fetching,
projection and transaction scope all have to cross the boundary somehow, and each
one either becomes another parameter on every method or is quietly ignored until
someone profiles the system. The most common escape hatch, returning
IQueryable so the caller can carry on composing, gives up the
boundary entirely: the caller is writing database queries again, the repository
cannot know what will run, and a change to the storage layer can now break code
that never mentioned it. Meanwhile the collection illusion invites N+1 access
patterns, because asking a collection for one item at a time in a loop looks free
and asking a database for one row at a time in a loop is not.
The testing benefit is oversold too. An in-memory implementation has different semantics from the real store on uniqueness, ordering, case sensitivity, null handling, concurrency and transactions, so a green test suite against the fake tells you the domain logic is consistent with itself and nothing about whether it works. And the argument that a repository lets you swap databases later is close to folklore. In twenty years I have seen a schema change many times, a mapping layer change several times, and the database product change essentially never.