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 }
endUse 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
More in Architecture