Skip to content

Repository Pattern — Data Access Abstraction

DodaTech Updated 2026-06-24 4 min read

What You'll Learn

You will learn how the Repository Pattern abstracts data access behind a collection-like interface, allowing domain logic to be completely agnostic of the underlying persistence mechanism.

Why It Matters

Applications that scatter SQL queries across controllers and services are impossible to refactor when the database changes. Repository centralises all data access logic, making it easy to switch from PostgreSQL to MongoDB, add caching, or introduce a test double without touching business code. Consider migrating from MySQL to PostgreSQL: with repositories, you swap the implementation; without them, you hunt through every service for hardcoded SQL.

Real-World Use

Domain-Driven Design treats Repository as a first-class building block. DodaTech's multi-tenant SaaS platform uses repository classes that transparently route queries to the correct tenant database shard while domain services call find() and save() without knowing about sharding. A new tenant is simply a new repository configuration; the business logic never changes.

The Pattern

Repository interface exposes collection-like methods (add, find, remove). ConcreteRepository implements them using a specific data source. Domain code depends only on the interface.

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Product:
    sku: str
    name: str
    price: float

class ProductRepository(ABC):
    @abstractmethod
    def find_by_sku(self, sku: str) -> Product:
        pass

    @abstractmethod
    def find_all(self) -> list[Product]:
        pass

    @abstractmethod
    def save(self, product: Product) -> None:
        pass

    @abstractmethod
    def delete(self, sku: str) -> None:
        pass

class InMemoryProductRepository(ProductRepository):
    def __init__(self):
        self._products = {}

    def find_by_sku(self, sku: str) -> Product:
        return self._products.get(sku)

    def find_all(self) -> list[Product]:
        return list(self._products.values())

    def save(self, product: Product) -> None:
        self._products[product.sku] = product

    def delete(self, sku: str) -> None:
        self._products.pop(sku, None)

class ProductService:
    def __init__(self, repo: ProductRepository):
        self._repo = repo

    def apply_discount(self, sku: str, percent: float) -> Product:
        product = self._repo.find_by_sku(sku)
        product.price *= (1 - percent / 100)
        self._repo.save(product)
        return product

    def get_products(self) -> list[Product]:
        return self._repo.find_all()
repo = InMemoryProductRepository()
repo.save(Product("A1", "Widget", 10.0))
repo.save(Product("B2", "Gadget", 25.0))

service = ProductService(repo)
service.apply_discount("A1", 20)

for p in service.get_products():
    print(f"{p.sku}: {p.name} — ${p.price:.2f}")
A1: Widget — $8.00
B2: Gadget — $25.00

Structure

classDiagram
    class DomainService {
        +businessLogic()
    }
    class Repository {
        <>
        +findById(id): Entity
        +findAll(): List~Entity~
        +save(entity)
        +delete(id)
    }
    class ConcreteRepository {
        +findById(id): Entity
        +findAll(): List~Entity~
        +save(entity)
        +delete(id)
    }
    class DataSource {
        +query()
        +execute()
    }
    DomainService --> Repository : depends on
    ConcreteRepository ..|> Repository
    ConcreteRepository --> DataSource : uses

Real-World Usage

  • Spring Data JPAJpaRepository interface with CRUD methods, query derivation, and pagination.
  • Entity Framework CoreDbSet<T> acts as a repository; DbContext is the Unit of Work.
  • Python SQLAlchemy — session.query() provides a repository-like interface.
  • TypeORM / MikroORM — entity managers provide repository abstractions over TypeScript/JavaScript databases.
  • DAO Pattern is lower-level; Repository works with domain objects, DAO works with data structures.
  • DTO Pattern often transports data between Repository and service layer.
  • Abstract Factory can create repository implementations for different data sources.
  • Unit of Work coordinates multiple repository operations within a single transaction.
  • Dependency Injection supplies repository implementations to services.

Pros and Cons

Pros Cons
Domain code is persistence-agnostic Adds a layer of indirection for simple CRUD
Centralised query logic and optimisation Repository interfaces can become bloated with query methods
Easy to mock for Unit Testing Complex queries may leak into domain services
Consistent API across different data sources Overfetching and N+1 query problems if not careful
Supports multiple storage backends transparently ORMs already provide repository-like abstractions

The code demonstrates repository purity: ProductService calls find_by_sku() and save() without knowing whether the repository stores data in memory, a SQL database, or a remote API. The InMemoryProductRepository is perfect for testing — you can verify the discount logic without a database. Switching to a PostgresProductRepository requires no changes to ProductService.

Practice Questions

  1. Implement a repository that adds a caching layer on top of a database repository.
  2. How would you handle pagination and sorting in a Repository interface?
  3. Compare Repository with raw DAO — when does the abstraction layer stop providing value?
  4. Implement a find_by_criteria method that accepts a specification object for dynamic queries.

Challenge

Implement a PostgresProductRepository that uses raw SQL. Then add a CachedProductRepository that wraps any repository and caches results. Verify that ProductService works with both.

Real-World Task

Run DodaTech's query analyser on your application to identify scattered data access code. Extract a repository for each aggregate root and measure the improvement in testability.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro