Home Design Patterns Repository

Repository

Modern Practice

Put 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.

UML class diagram: a DomainService depends on the Repository interface, which returns Entity objects; SqlRepository implements it and is the only class that talks to the Database.
The interface sits above the implementation on purpose. Everything that knows the word SQL is below the line, and the domain above it deals only in entities.

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
    }
}
The pattern is the method name. 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.