Stackbook Logo
data-stateestablished · low operational burden

Repository Pattern

Also known as: repository, domain-repository

Intent

Provide a collection-like interface for retrieving and storing aggregates, hiding persistence details from the domain.

Problem

Domain and application code that talks to SQL, an ORM, or a document store directly becomes tied to storage details and hard to test. Query logic scatters across the codebase.

Forces

  • •Domain code should not know how data is stored
  • •Tests need quick substitutes for the database
  • •Query logic should be centralized and reusable
  • •Abstractions can hide performance characteristics

Solution

Overview

A repository mediates between the domain and data mapping layers. To the caller it behaves like a collection: ask for an order, change it, and put it back. The caller does not see SQL, connection handling, or ORM sessions.

How it works

The interface lives close to the domain and speaks in domain terms. The implementation sits in infrastructure.

class OrderRepository(Protocol):
    def get(self, order_id: OrderId) -> Order: ...
    def add(self, order: Order) -> None: ...

class SqlOrderRepository:
    def get(self, order_id):
        row = db.query("select * from orders where id = %s", order_id)
        return to_domain(row)
UseCase -> OrderRepository (interface) <- SqlOrderRepository -> DB
                                       <- InMemoryOrderRepository (tests)

Pair it with the unit of work to commit changes across several repositories atomically. For read-side screens, bypass the repository and use dedicated query services that return flat read models.

Pitfalls

  • Returning IQueryable or ORM query builders, which leaks persistence into callers.
  • Creating one generic repository with dozens of finder methods.
  • One repository per table, which lets code modify aggregate internals directly.
  • Putting business rules inside the repository.
  • Using repositories for reporting queries where joins and projections matter.

Key takeaways

  • One repository per aggregate root.
  • The interface speaks the domain language.
  • Use in-memory fakes in tests.
  • Keep reporting queries separate from aggregate persistence.

✓ When to Use

  • •Rich domain models using aggregates
  • •Need to swap or fake persistence in tests
  • •Multiple data sources behind one domain concept

✗ When Not to Use

  • •Simple CRUD where the ORM already suffices
  • •Read-heavy reporting that needs flexible queries
  • •When the repository would just wrap ORM calls one to one

Pros

  • +Domain isolated from persistence
  • +Easy in-memory testing
  • +Central place for query logic

Cons

  • −Extra layer and mapping code
  • −Generic repositories leak or hide too much
  • −Complex queries strain the interface

Cost Profile

Infrastructure

None — code abstraction

Operational

Low

Cognitive

Low to medium

Failure Modes

  • Leaky abstraction exposing ORM query objects to callers

  • N+1 queries hidden behind a simple interface

  • Repository per entity breaking aggregate boundaries

Real-World Examples

Alternatives

    Related Patterns

      Competency Domains

      data staterequirements constraints