Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 46

    Repository Pattern

    Repository mediates between domain and persistence with a collection-like interface: findById, save, delete — without exposing SQL, ORM, or table shapes.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Repository mediates between domain and persistence with a collection-like interface: findById, save, delete — without exposing SQL, ORM, or table shapes. Services speak domain language; infrastructure translates to Postgres, Mongo, or DynamoDB behind the port.

    The story

    Services called Mongoose directly — switching to Postgres required editing every service. Repositories during migration meant the second swap (Postgres → DynamoDB for hot tables) took days instead of weeks.

    The business problem

    Direct ORM/SQL in services couples domain to persistence technology:

    • Migration cost: DB vendor change touches every service.
    • Untestable services: tests need real database or heavy mocks.
    • Query duplication: same fetch logic copied across services.
    • Leaky persistence: ORM entities become application's domain model.

    The problem teams faced

    Repository fits when:

    • Persistence technology may change or vary (SQL + cache + search).
    • Application services should not contain SQL or ORM queries.
    • Tests need in-memory or fake persistence seam.
    • Query logic should be centralized per aggregate.

    Understanding the topic

    Intent: Collection-like interface for domain objects hiding persistence details.

    • Repository interface — owned by domain/application layer.
    • Concrete repository — ORM/SQL implementation in infrastructure.
    • Aggregate root — repository typically one per aggregate (OrderRepository).
    • Specification/query — optional pattern for complex queries.

    Internal architecture

    Order repository port + adapter:

    ts
    interface OrderRepository {
    findById(id: OrderId): Promise<Order | null>
    save(order: Order): Promise<void>
    }
    class PostgresOrderRepository implements OrderRepository {
    async findById(id: OrderId) {
    const row = await db.query("SELECT ...", [id.value])
    return row ? OrderMapper.toDomain(row) : null
    }
    async save(order: Order) {
    await db.query("UPSERT ...", OrderMapper.toPersistence(order))
    }
    }

    Visual explanation

    Three diagrams cover repository seam, fake for tests, and vs DAO:

    Repository seam
    OrderService
    Application
    OrderRepository
    Interface
    PostgresOrderRepo
    Adapter
    orders table
    Hidden
    Service never sees SQL — repository translates.
    Test with fake repo
    InMemoryOrderRepo
    Map storage
    OrderService test
    No DB
    Contract test
    Real repo vs spec
    Fast unit tests
    CI friendly
    Fake repository for unit tests; contract tests for real impl.
    Repository vs query sprawl
    findByCustomer?
    On repository
    Raw SQL in service?
    Violation
    Specification object
    Complex queries
    One repo per aggregate
    DDD boundary
    Repository per aggregate — not one god repo for entire DB.

    Informative example

    Before / after — Mongoose in service to repository:

    ts
    // ❌ Mongoose everywhere — migration nightmare
    class OrderService {
    async get(id: string) {
    return OrderModel.findById(id) // Mongoose document leaks up
    }
    }
    // ✅ Repository hides persistence
    interface OrderRepository {
    findById(id: OrderId): Promise<Order | null>
    }
    class OrderService {
    constructor(private orders: OrderRepository) {}
    async get(id: OrderId) { return this.orders.findById(id) }
    }
    class InMemoryOrderRepository implements OrderRepository {
    private store = new Map<string, Order>()
    findById(id: OrderId) { return Promise.resolve(this.store.get(id.value) ?? null) }
    save(o: Order) { this.store.set(o.id.value, o); return Promise.resolve() }
    }

    Execution workflow

    1Introduce repository workflow
    1 / 4

    Define interface

    OrderRepository in domain/application package.

    Methods speak domain — save(Order).

    Real-world use

    Spring Data JPA repositories, DDD aggregate repositories, Entity Framework DbContext as repository-ish, TypeORM custom repositories.

    Production case study

    Mongo → Postgres → Dynamo migration:

    • First migration: Mongoose → Postgres via OrderRepository interface.
    • Second migration: hot tables to Dynamo — new adapter, zero service edits.
    • Metric: second migration days vs weeks without repository.

    Trade-offs

    • Pro: persistence swap; testable services; centralized queries.
    • Con: mapping layer overhead; abstraction leak if interface mirrors tables.
    • Con: generic repository anti-pattern tempts god interface.

    Decision framework

    • Use when persistence may change or varies per deployment.
    • Use with Hexagonal/Clean — repository is driven adapter.
    • Avoid generic Repository for everything — per aggregate.
    • Skip when app is prototype with fixed SQLite forever.

    Best practices

    • One repository per aggregate root — not per table.
    • Return domain objects — map ORM rows inside repository.
    • Complex queries: specification pattern or query service — not SQL in service.

    Anti-patterns to avoid

    • Generic IRepository with 20 methods — leaks persistence concerns.
    • Repository returning ORM entities to domain layer.
    • Business logic inside repository — belongs in domain/service.

    Common mistakes

    • Repository interface mirrors database tables not domain model.
    • N+1 queries hidden inside repository methods — profile adapters.

    Debugging tips

    • Log SQL at repository adapter boundary — not scattered in services.

    Optimization strategies

    • Read-optimized query repos separate from write repos when CQRS applies.

    Common misconceptions

    • Repository ≠ DAO. Repository speaks domain collection language; DAO is table-centric.
    • Spring Data @Repository is repository pattern with code generation.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1IntermediateQuestionRepository vs Active Record?+

    Answer

    Active Record: object knows persistence (save on entity). Repository: separate object mediates — domain entities stay persistence-ignorant.

    Follow-up

    Rails ActiveRecord?
    2AdvancedQuestionOne repo per table?+

    Answer

    No — per aggregate root (OrderRepository for Order aggregate). Related entities loaded/saved through root.

    Follow-up

    OrderLine items?
    3IntermediateQuestionTest without DB?+

    Answer

    InMemoryOrderRepository implementing same interface — fast unit tests. Contract/integration tests hit real Postgres adapter.

    Follow-up

    Mapper tests?

    Summary

    You can define repository ports, implement adapters, and migrate databases without touching services. Tell the Mongo→Postgres→Dynamo days-not-weeks story — if you can do that, you own Repository.

    Ready to mark this lesson complete?Track your journey across the entire course.