SD System Design In Depth
0%
lesson system-designarchitecturedata-access

Repository pattern

Module 1.1: Foundations & System Invariants Updated Sep 17, 2026
📄 Cheat Sheet
Deep Dive Into the Repository Design Pattern in Python
ArjanCodes Deep Dive Into the Repository Design Pattern in Python Watch on YouTube ↗

Language-light, well-paced explanation of why the pattern exists, how the abstract interface decouples domain code from storage, and how it enables in-memory test doubles.

0:00 / --:--

Under the Hood: Decoupling Domain Logic from Storage Mechanics

The Repository Pattern acts as an in-memory collection-like abstraction between domain business logic and the underlying persistence layer (SQL, NoSQL, or external HTTP APIs). Its primary architectural purpose is to prevent database query semantics and ORM leakages from infecting domain entities.

Data Flow Across Architectural Layers

flowchart LR Controller["HTTP / gRPC Handler"] --> Service["Domain Business Service"] Service --> RepoInterface["OrderRepository (interface)"] RepoInterface -.->|"Dependency Injection"| PostgresRepo["PostgresOrderRepository"] RepoInterface -.->|"Unit Testing"| MockRepo["InMemoryMockOrderRepository"] PostgresRepo --> DB[("PostgreSQL / SQL Engine")]

Engineering Realities & Anti-Patterns

1. The Leaky Abstraction Problem

A frequent anti-pattern is creating a generic Repository<T> that exposes direct query builders, raw IQueryable, or arbitrary predicate filters to the service tier. When service code passes database-specific filter clauses into the repository, the abstraction leaks completely: database schema knowledge bleeds across domain boundaries, defeating the purpose of the pattern.

2. The N+1 Query Trap & Impedance Mismatch

Domain entities often possess nested relationships (e.g., an Order contains multiple OrderItems). Naive repository methods like getOrderById(id) that lazily fetch associated entities trigger classic N+1 network roundtrips to the database. Production repositories explicitly define aggregate root queries (e.g., getOrderWithItems(id)) that execute explicit SQL joins.

3. CQRS Boundary: Reads vs. Writes

Repositories shine for write-heavy domain aggregates enforcing business invariants. For complex read-heavy dashboard queries or multi-table analytical joins, bypassing the repository in favor of a specialized Read Model (CQRS) avoids inflating domain entities with unnecessary read projections.

Worked example: a link repository

For the course's URL shortener, the link operation needs only three storage capabilities. Defining them explicitly keeps the contract small:

  • findByCode(code) returns Found(link), Missing, or throws StorageUnavailable.
  • insertIfAbsent(link) returns Inserted or CodeTaken. The Postgres adapter implements this with INSERT ... ON CONFLICT (code) DO NOTHING and checks the affected row count, so the uniqueness rule stays atomic in the database.
  • disable(code) returns Updated or Missing.

The in-memory implementation is a dictionary plus the same result types. A unit test can run thousands of "generate code, collide, retry" cycles in well under a second. A separate integration test runs the Postgres adapter against a real database (for example in a container) and verifies that two concurrent insertIfAbsent calls with the same code produce exactly one Inserted and one CodeTaken.

Notice what the interface does not offer: no query(sql), no generic filter object. If a reporting page needs "top 100 links by clicks last week", that is a read model query written directly against the database or a warehouse, not a repository method.

Trade-off table

ApproachStrengthsWeaknessesUse when
Direct ORM or SQL in handlersLeast code, easy to follow for small appsRules and queries mixed; hard to test without a databaseSmall CRUD apps, prototypes
Specific repository per aggregateDomain rules testable in memory; storage swappable in one placeMore interfaces; risk of pass-through methodsRich domain rules, several callers sharing rules
Generic repository of TLess boilerplateTends to leak query semantics; weak domain languageRarely the best choice; most ORMs already are this
Repository for writes plus read models (CQRS-lite)Writes protect invariants; reads use efficient tailored queriesTwo code paths to maintainComplex dashboards or reports over the same data

Failure modes

  • Transactions across repositories: saving an order and decrementing stock through two repositories needs a shared transaction (the Unit of Work pattern) or you can commit one without the other.
  • Error translation: letting a driver-specific exception escape forces every caller to know the database. Translate into a small set of domain errors at the adapter.
  • Over-abstraction: wrapping an ORM that already implements identity map and unit of work in another layer that just forwards calls adds code without protection.

How real systems use it

The pattern comes from Martin Fowler's Patterns of Enterprise Application Architecture and Eric Evans's Domain-Driven Design. Frameworks such as Spring Data and Entity Framework provide repository-like abstractions built in, which is why many teams define thin, use-case-specific repository interfaces on top rather than generic wrappers.

Check yourself Question 1 of 3
Leaky generic repository recall
From: Engineering Realities & Anti-Patterns

Why does a generic repository that accepts arbitrary query predicates or query builders from service code defeat the purpose of the pattern?

Key Takeaways

The repository pattern mediates between the domain layer and data mapping using a collection-like interface.
Avoid generic CRUD repositories that leak SQL query builders into domain business logic.
Use explicit aggregate root methods to avoid lazy-loading N+1 query performance degradation.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue