Architecture

Repository Pattern

Abstracting data access behind a collection-like interface to decouple domain logic from persistence.

Overview

The Repository Pattern abstracts the persistence layer behind a collection-like interface. Callers work with domain objects (User, Order) without knowing whether they come from PostgreSQL, Redis, a remote API, or an in-memory store. It separates domain logic from data access logic and makes persistence mechanisms swappable.

Origin

Defined by Eric Evans in "Domain-Driven Design" (2003) and further elaborated by Martin Fowler in "Patterns of Enterprise Application Architecture" (2002). It is the primary abstraction between the domain model and infrastructure in DDD.

Examples

Repository interface and implementations

# The interface (defined by the domain layer)
class UserRepository
  def find(id)            = raise NotImplementedError
  def find_by_email(email)= raise NotImplementedError
  def save(user)          = raise NotImplementedError
  def delete(user)        = raise NotImplementedError
end

# PostgreSQL adapter
class PGUserRepository < UserRepository
  def find(id)
    row = DB[:users].where(id: id).first
    row ? User.new(**row) : nil
  end

  def save(user)
    if user.persisted?
      DB[:users].where(id: user.id).update(user.to_h)
    else
      id = DB[:users].insert(user.to_h)
      user.assign_id(id)
    end
    user
  end
end

# In-memory adapter for tests
class InMemoryUserRepository < UserRepository
  def initialize = (@store = {})
  def find(id) = @store[id]
  def save(user) = tap { @store[user.id] = user }
end

Use Cases

  • 01Testing domain logic without a database: swap the real repository for an in-memory one
  • 02Systems that may switch databases or persistence mechanisms
  • 03Caching: wrap a repository with a caching decorator that checks Redis before hitting the DB
  • 04Multi-tenancy: inject a tenant-scoped repository that filters all queries by tenant ID

When Not to Use

  • //Simple applications tightly coupled to ActiveRecord, adding a repository layer duplicates ActiveRecord's existing abstraction
  • //Read-heavy reporting queries that are inherently persistence-specific (complex JOINs, window functions)
  • //When the abstraction boundary is never tested through an alternative implementation

Technical Notes

  • ActiveRecord is not a Repository, it is an Active Record pattern (Fowler). The model knows how to persist itself. A Repository decouples the model from persistence entirely
  • Repositories should not expose query details (find_all_users_with_unpaid_orders_sorted_by_date). Named query methods belong on the repository; the caller should not construct queries
  • A Specification object (Fowler) can be passed to a repository to express query criteria without coupling the caller to SQL