<arch.design/>
Principles/Repository Pattern
{ }CodeArchitecturebeginner2003fowlerdata-accessinterfacetestability

Repository Pattern

Decouple business logic from data access by defining a collection-like interface for retrieving and persisting domain objects.

5/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

The Repository pattern, introduced by Martin Fowler in Patterns of Enterprise Application Architecture (2003), sits between the domain model and the data mapping layer. Domain objects interact with a repository interface as if it were an in-memory collection — they call save(), findById(), or delete() without knowing whether data is stored in Postgres, MongoDB, Redis, or anywhere else.

The interface lives in the domain layer. Concrete implementations (e.g. PgUserRepository) live in an adapters or infrastructure layer. Swapping the database requires writing a new adapter, not touching business logic.

This boundary also makes testing clean: replace the Postgres adapter with an InMemoryRepository in tests — no database, no slow I/O, full coverage of business logic.

Project structure

recommended layout
project structure
src/
├── domain/
│ └── user.ts # Entity — pure data + behaviour
├── ports/
│ └── user-repository.ts # Interface (the contract)
├── adapters/
│ ├── pg-user-repository.ts # Postgres implementation
│ └── in-memory-user-repo.ts # Test / dev implementation
└── services/
└── user-service.ts # Business logic — depends on interface

Implementation

TypeScript · Go · Rust
// ports/user-repository.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>;
}

// adapters/pg-user-repository.ts
export class PgUserRepository implements UserRepository {
  constructor(private db: Pool) {}

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

  async save(user: User): Promise<void> {
    await this.db.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]
    );
  }
}

// services/user-service.ts — depends on interface, not implementation
export class UserService {
  constructor(private repo: UserRepository) {}

  async changeEmail(id: string, newEmail: string): Promise<void> {
    const user = await this.repo.findById(id);
    if (!user) throw new Error("User not found");
    user.email = newEmail;
    await this.repo.save(user);
  }
}

// bootstrap: swap PgUserRepository for InMemoryUserRepo in tests
const repo = new PgUserRepository(pool);
const service = new UserService(repo);

Why it matters

SQL queries scattered through business logic are the leading cause of software that's hard to test, change, and reason about. A repository interface provides a clean seam between your domain and your database.

When to use

  • Any application with a meaningful domain model and persistence
  • When you want to unit-test business logic without a database
  • When you anticipate switching or abstracting the storage backend
  • Paired with Clean Architecture, Hexagonal Architecture, or DDD

When NOT to use

  • Simple CRUD apps with no real business logic — direct ORM calls are fine
  • Scripts or one-off tools where the overhead isn't worth it

Trade-offs

+

Business logic is fully testable without a real database

Extra abstraction layer adds boilerplate, especially for simple queries

+

Storage technology can be swapped transparently

Complex queries (joins, aggregations) feel awkward behind a collection interface

+

Clear boundary between domain and infrastructure

ORM-specific features (lazy loading, change tracking) may need workarounds

In production

Spring Framework

Spring Data auto-generates repository implementations from interface definitions

Laravel

Repository pattern commonly layered over Eloquent ORM in large Laravel apps

ASP.NET Core

EF Core DbContext is itself a repository/unit-of-work; custom repos add domain abstraction

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles