---
# === IDENTITY ===
id: software/patterns/repository-pattern/2026
canonical_question: "How do I implement the repository pattern?"
aliases:
  - "What is the repository pattern"
  - "Repository pattern best practices"
  - "How to abstract data access layer"
  - "Generic repository vs specific repository"
entity_type: software_reference
domain: software > patterns > repository pattern
region: global
jurisdiction: global
temporal_scope: 2003-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.90
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "One repository per aggregate root — never per database table"
  - "Repository interfaces must live in the domain layer, implementations in the infrastructure layer"
  - "Never expose ORM query builders (IQueryable, QuerySet, Session) through repository interfaces"
  - "Repository methods must return fully materialized domain objects, not lazy-loaded proxies"
  - "Keep repositories focused on persistence — no business logic, no validation, no cross-aggregate queries"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "You need to separate read and write models at scale"
    use_instead: "software/system-design/cqrs-event-sourcing/2026"
  - condition: "You are building a simple CRUD app with <5 entities and no domain logic"
    use_instead: "Use your ORM directly — Active Record or Data Mapper without repository abstraction"

# === AGENT HINTS ===
inputs_needed:
  - key: language
    question: "Which programming language are you using?"
    type: choice
    options: ["TypeScript", "Python", "Java", "Go", "C#"]
  - key: complexity
    question: "How complex is your domain model?"
    type: choice
    options: ["simple CRUD", "moderate business rules", "complex domain with aggregates"]
  - key: testing_priority
    question: "Is unit testing and swappable data sources a primary goal?"
    type: choice
    options: ["yes", "no"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/repository-pattern/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: software/patterns/dependency-injection/2026
      label: "Dependency Injection"
  related_to:
    - id: software/patterns/database-indexing-strategies/2026
      label: "Database Indexing Strategies"
    - id: software/debugging/postgresql-connection-pool/2026
      label: "Connection Pooling"
  solves:
    - id: software/patterns/testable-data-access/2026
      label: "Testable Data Access Layer"
  alternative_to:
    - id: software/system-design/cqrs-event-sourcing/2026
      label: "CQRS + Event Sourcing"
  often_confused_with:
    - id: software/patterns/active-record-pattern/2026
      label: "Active Record Pattern"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Designing the Infrastructure Persistence Layer"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design
    type: official_docs
    published: 2024-11-15
    reliability: high
  - id: src2
    title: "Repository Pattern"
    author: DevIQ
    url: https://deviq.com/design-patterns/repository-pattern/
    type: technical_blog
    published: 2024-06-01
    reliability: moderate_high
  - id: src3
    title: "The Repository Pattern Done Right"
    author: Manuel Navarro
    url: https://blog.mnavarro.dev/the-repository-pattern-done-right
    type: technical_blog
    published: 2024-03-15
    reliability: moderate_high
  - id: src4
    title: "Why the Generic Repository is Just a Lazy Anti-Pattern"
    author: Ben Morris
    url: https://www.ben-morris.com/why-the-generic-repository-is-just-a-lazy-anti-pattern/
    type: technical_blog
    published: 2023-01-10
    reliability: moderate_high
  - id: src5
    title: "The Repository Pattern in Go"
    author: Three Dots Labs
    url: https://threedots.tech/post/repository-pattern-in-go/
    type: technical_blog
    published: 2023-09-20
    reliability: moderate_high
  - id: src6
    title: "Repository Pattern (Cosmic Python)"
    author: Harry Percival & Bob Gregory
    url: https://www.cosmicpython.com/book/chapter_02_repository.html
    type: technical_blog
    published: 2020-03-01
    reliability: high
  - id: src7
    title: "Exploring the Repository Pattern with TypeScript and Node"
    author: LogRocket Blog
    url: https://blog.logrocket.com/exploring-repository-pattern-typescript-node/
    type: technical_blog
    published: 2023-08-14
    reliability: moderate_high
---

# Repository Pattern: Implementation Guide

## TL;DR

- **Bottom line**: Abstract data access behind a clean interface so domain logic stays independent of the database, enabling testability and swappable persistence.
- **Key tool/command**: `interface UserRepository { findById(id): User; save(user): void }` + concrete implementation per data store.
- **Watch out for**: Generic repositories that expose ORM internals (IQueryable, QuerySet) -- this defeats the entire purpose of the abstraction.
- **Works with**: Any language/framework. Most valuable with DDD, hexagonal architecture, and projects requiring extensive unit testing.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- One repository per aggregate root -- never create a repository for every database table. [src1]
- Repository interfaces belong in the domain layer; implementations belong in the infrastructure layer. [src3]
- Never return ORM-specific types (IQueryable, QuerySet, Session) from repository methods -- always return materialized domain objects. [src4]
- Repository methods must be persistence-agnostic in their signatures -- no SQL fragments, no ORM filter objects in parameters.
- Keep repositories focused on collection-like operations (add, remove, find) -- no business logic, no email sending, no cross-aggregate queries. [src2]

## Quick Reference

| Variant | Data Access | Unit of Work | Testability | Complexity | Best For |
|---|---|---|---|---|---|
| Basic Repository | Interface + one impl per aggregate | Manual or none | High (mock interface) | Low | Small-medium projects |
| Generic Repository | Base class with CRUD generics | Manual or none | Medium (often leaks ORM) | Low-Medium | Boilerplate reduction |
| Repository + Unit of Work | Repository delegates save to UoW | Explicit UoW class | High | Medium | Transaction coordination |
| Specification Pattern | Repository accepts Spec objects | Compatible with UoW | High | Medium-High | Complex query composition |
| CQRS Split | Repository for writes, thin reads bypass | Write-side UoW | High (write side) | High | Read-heavy, complex domains |
| Query Objects | Separate class per query | None needed | High | Medium | Many distinct read paths |
| Repository + Mediator | Repository behind command/query handlers | Handler-scoped UoW | High | High | Event-driven architectures |
| Active Record (anti-pattern) | Entity IS the repository | Built into entity | Low (tightly coupled) | Low | Avoid in DDD contexts |

## Decision Tree

```
START
├── Is your domain logic complex (>10 business rules, aggregates)?
│   ├── YES → Use specific repositories per aggregate root
│   │   ├── Need complex query composition?
│   │   │   ├── YES → Add Specification pattern
│   │   │   └── NO → Basic repository is sufficient
│   │   ├── Need separate read/write optimization?
│   │   │   ├── YES → Consider CQRS (repository for writes, direct queries for reads)
│   │   │   └── NO → Standard repository
│   │   └── Multiple data sources in one transaction?
│   │       ├── YES → Add Unit of Work pattern
│   │       └── NO → Repository handles its own persistence
│   └── NO ↓
├── Is testability with mock data stores a primary concern?
│   ├── YES → Use repository interfaces even for simple CRUD
│   └── NO ↓
├── Is this a simple CRUD app with <5 entities?
│   ├── YES → Skip repository pattern, use ORM directly
│   └── NO ↓
└── DEFAULT → Start with basic specific repositories, add complexity only when needed
```

## Step-by-Step Guide

### 1. Define the repository interface in the domain layer

The interface describes what the domain needs from persistence, not how persistence works. Keep method signatures in terms of domain objects only. [src1]

```typescript
// domain/repositories/UserRepository.ts
export interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}
```

**Verify**: The interface imports only domain types -- no ORM, no database driver, no SQL.

### 2. Create the concrete implementation in the infrastructure layer

The implementation translates domain operations into database calls. All ORM/SQL details live here. [src7]

```typescript
// infrastructure/repositories/PostgresUserRepository.ts
import { Pool } from 'pg';
import { User } from '../../domain/entities/User';
import { UserRepository } from '../../domain/repositories/UserRepository';

export class PostgresUserRepository implements UserRepository {
  constructor(private pool: Pool) {}

  async findById(id: string): Promise<User | null> {
    const { rows } = await this.pool.query(
      'SELECT * FROM users WHERE id = $1', [id]
    );
    return rows[0] ? this.toDomain(rows[0]) : null;
  }

  async save(user: User): Promise<void> {
    await this.pool.query(
      `INSERT INTO users (id, email, name) VALUES ($1, $2, $3)
       ON CONFLICT (id) DO UPDATE SET email = $2, name = $3`,
      [user.id, user.email, user.name]
    );
  }

  private toDomain(row: any): User {
    return new User(row.id, row.email, row.name);
  }
}
```

**Verify**: The implementation class imports the interface and implements every method. Domain layer has zero dependency on this file.

### 3. Wire up via dependency injection

Inject the repository interface into domain services. The composition root (main/startup) decides which implementation to use. [src1]

```typescript
// application/services/UserService.ts
export class UserService {
  constructor(private userRepo: UserRepository) {} // interface, not impl

  async registerUser(email: string, name: string): Promise<User> {
    const existing = await this.userRepo.findByEmail(email);
    if (existing) throw new Error('Email already registered');
    const user = User.create(email, name);
    await this.userRepo.save(user);
    return user;
  }
}
```

**Verify**: `UserService` constructor accepts the interface type, not `PostgresUserRepository`.

### 4. Create a test double for unit testing

Because the service depends on an interface, swap in a fake for tests with zero database setup. [src6]

```typescript
// tests/FakeUserRepository.ts
export class FakeUserRepository implements UserRepository {
  private users: Map<string, User> = new Map();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }
  async findByEmail(email: string): Promise<User | null> {
    return [...this.users.values()].find(u => u.email === email) || null;
  }
  async save(user: User): Promise<void> {
    this.users.set(user.id, user);
  }
  async delete(id: string): Promise<void> {
    this.users.delete(id);
  }
}
```

**Verify**: Tests run without any database connection: `const repo = new FakeUserRepository(); const svc = new UserService(repo);`

## Code Examples

### TypeScript: Repository with interface and implementation

> Full script: [typescript-repository-with-interface-and-implement.ts](scripts/typescript-repository-with-interface-and-implement.ts) (29 lines)

```typescript
// Interface — domain layer (no ORM imports)
interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  findByCustomer(customerId: string): Promise<Order[]>;
  save(order: Order): Promise<void>;
# ... (see full script)
```

### Python: Repository with SQLAlchemy

> Full script: [python-repository-with-sqlalchemy.py](scripts/python-repository-with-sqlalchemy.py) (33 lines)

```python
# domain/repositories.py — abstract interface
from abc import ABC, abstractmethod
from domain.entities import User
class UserRepository(ABC):
    @abstractmethod
# ... (see full script)
```

### Java: Repository with Spring Data JPA

> Full script: [java-repository-with-spring-data-jpa.java](scripts/java-repository-with-spring-data-jpa.java) (26 lines)

```java
// domain/repository/OrderRepository.java — domain interface
public interface OrderRepository {
    Optional<Order> findById(String id);
    List<Order> findByCustomerId(String customerId);
    void save(Order order);
# ... (see full script)
```

### Go: Repository with sqlc

> Full script: [go-repository-with-sqlc.go](scripts/go-repository-with-sqlc.go) (33 lines)

```go
// domain/repository.go — interface
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    FindByEmail(ctx context.Context, email string) (*User, error)
    Save(ctx context.Context, user *User) error
# ... (see full script)
```

## Anti-Patterns

### Wrong: Generic repository exposing IQueryable/QuerySet

```csharp
// BAD -- leaks ORM query builder through the interface
public interface IRepository<T> {
    IQueryable<T> GetAll();  // Callers build arbitrary queries
    T GetById(int id);
    void Add(T entity);
}
// Service code now contains ORM-specific LINQ:
var users = repo.GetAll()
    .Where(u => u.IsActive)
    .Include(u => u.Orders)  // EF-specific!
    .ToListAsync();
```

### Correct: Domain-specific methods with materialized results

```csharp
// GOOD -- interface exposes domain operations only
public interface IUserRepository {
    Task<User?> FindByIdAsync(int id);
    Task<IReadOnlyList<User>> FindActiveUsersAsync();
    Task SaveAsync(User user);
}
// Implementation handles all ORM details internally
```

### Wrong: Repository wrapping another repository

```typescript
// BAD -- repository delegates to another repository, adding a useless layer
class CachedUserRepository implements UserRepository {
  constructor(
    private innerRepo: UserRepository,  // wrapping another repo
    private otherRepo: UserRepository   // and another
  ) {}
  async findById(id: string) {
    // Just calls through with no added value
    return this.innerRepo.findById(id);
  }
}
```

### Correct: Decorator or caching at the infrastructure level

```typescript
// GOOD -- caching decorator adds real value
class CachedUserRepository implements UserRepository {
  constructor(
    private delegate: UserRepository,
    private cache: Cache
  ) {}
  async findById(id: string): Promise<User | null> {
    const cached = await this.cache.get(`user:${id}`);
    if (cached) return cached;
    const user = await this.delegate.findById(id);
    if (user) await this.cache.set(`user:${id}`, user, 300);
    return user;
  }
}
```

### Wrong: Fat repository with business logic

```python
# BAD -- repository validates, calculates, and sends emails
class OrderRepository:
    def place_order(self, order):
        if order.total < 0:
            raise ValueError("Invalid total")  # Business rule in repo!
        order.tax = order.total * 0.2           # Calculation in repo!
        self.session.add(order)
        self.session.commit()
        send_confirmation_email(order)          # Side effect in repo!
```

### Correct: Repository only persists, service handles logic

```python
# GOOD -- repository does one thing: persist
class SqlOrderRepository:
    def save(self, order: Order) -> None:
        model = self._to_model(order)
        self._session.merge(model)
        self._session.flush()

# Business logic lives in the domain/application layer
class OrderService:
    def place_order(self, order: Order) -> None:
        order.validate()               # Domain logic
        order.calculate_tax()           # Domain logic
        self.order_repo.save(order)     # Persistence only
        self.email_service.send(order)  # Separate concern
```

## Common Pitfalls

- **One repository per table instead of per aggregate**: Creates tight coupling to the database schema and breaks aggregate boundaries. Fix: identify aggregate roots in your domain model and create one repository per root. [src1]
- **Returning ORM entities instead of domain objects**: Services become coupled to the ORM. If you switch databases, every service changes. Fix: add a `toDomain()` / `toModel()` mapper in the repository implementation. [src6]
- **Generic repository with unused methods**: `IRepository<T>` forces `Delete()` on entities that should never be deleted, violating Interface Segregation. Fix: define specific interfaces per aggregate with only the methods that aggregate needs. [src4]
- **Putting Unit of Work logic inside the repository**: Repositories should not call `commit()` / `SaveChanges()`. Fix: let the application service or a separate Unit of Work coordinate transactions. [src3]
- **Testing against the database through the repository**: The whole point is to enable in-memory fakes. Fix: always write domain logic tests with fake/stub repositories, reserve integration tests for the real implementation. [src6]
- **Repository becoming a query service**: Adding dozens of `findByX` methods for reporting queries. Fix: use CQRS -- repositories for writes, lightweight query objects or direct SQL for reads. [src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Domain has complex business rules and aggregates | Simple CRUD with <5 entities and no domain logic | ORM directly (Active Record or Data Mapper) |
| You need to unit test domain logic without a database | Rapid prototyping or throwaway code | Direct database queries |
| Multiple data source backends are realistic (SQL, NoSQL, API) | Single database that will never change | ORM repository (e.g., Spring Data, Django ORM) |
| Team follows DDD or hexagonal architecture | Read-heavy analytics/reporting queries | Query objects or CQRS read side |
| Aggregate boundaries need strict enforcement | Microservice with a single entity and no joins | Thin data access functions |
| You need a caching decorator or audit logging layer | Framework already provides testable abstractions (e.g., Django TestCase) | Framework-provided test utilities |

## Important Caveats

- The repository pattern adds an abstraction layer -- in simple apps this is unnecessary complexity. If your ORM already provides a clean, testable interface, adding repositories may be over-engineering. [src4]
- Generic repositories (`IRepository<T>`) almost always become a leaky abstraction. Prefer specific repository interfaces per aggregate root. [src4]
- In CQRS architectures, repositories are typically only used on the write/command side. The read/query side bypasses repositories entirely for performance. [src1]
- The pattern originated in Eric Evans' Domain-Driven Design (2003). Its value is highest when your domain model diverges significantly from your database schema.
- When using ORMs like Entity Framework or SQLAlchemy, the ORM session/DbContext already implements a Unit of Work. Adding another UoW layer on top can create confusion about who owns the transaction.

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Dependency Injection](/software/patterns/dependency-injection/2026)
- [Database Indexing Strategies](/software/patterns/database-indexing-strategies/2026)
- [Connection Pooling](/software/debugging/postgresql-connection-pool/2026)
- [CQRS + Event Sourcing](/software/system-design/cqrs-event-sourcing/2026)
- [Active Record Pattern](/software/patterns/active-record-pattern/2026)
