Repository Pattern

4 min read
Foundational3 min read
Rapid overview

Repository Pattern

TL;DR

Mediate between the domain and the data-access layer — expose persistence as an in-memory-like collection behind an interface, so the domain never depends on EF Core, SQL, or the ORM.

How it works


🧩 Example — The domain owns the interface, infrastructure provides the implementation

// Domain / application layer — the contract lives WITH the domain (Dependency Inversion)
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task<IReadOnlyList<Order>> GetPendingAsync();
    Task AddAsync(Order order);
    void Remove(Order order);
}

// Infrastructure layer — EF Core implementation, swappable for Dapper / Mongo / a fake
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;
    public OrderRepository(AppDbContext db) => _db = db;

    public Task<Order?> GetByIdAsync(Guid id) =>
        _db.Orders.FirstOrDefaultAsync(o => o.Id == id);

    public async Task<IReadOnlyList<Order>> GetPendingAsync() =>
        await _db.Orders.Where(o => o.Status == OrderStatus.Pending).ToListAsync();

    public async Task AddAsync(Order order) => await _db.Orders.AddAsync(order);

    public void Remove(Order order) => _db.Orders.Remove(order);
}

// --- Usage (application service) ---
public class CancelOrderHandler
{
    private readonly IOrderRepository _orders;
    private readonly IUnitOfWork _uow;

    public CancelOrderHandler(IOrderRepository orders, IUnitOfWork uow)
    {
        _orders = orders;
        _uow = uow;
    }

    public async Task Handle(Guid orderId)
    {
        var order = await _orders.GetByIdAsync(orderId)
                    ?? throw new NotFoundException(orderId);
        order.Cancel();                 // domain behaviour
        await _uow.SaveChangesAsync();  // Unit of Work commits the transaction
    }
}

✅ Why it matters:

  • The domain/application code depends on IOrderRepository, not EF Core — you can swap Dapper, Mongo, or an in-memory fake without touching business logic.
  • Centralises query logic (GetPendingAsync) so it isn't copy-pasted across controllers and services.
  • Makes the domain trivially unit-testable with an in-memory fake repository — no database, no mocking framework.
  • Pairs with Unit of Work to commit many changes across aggregates in one transaction.

Quick recall Q&A

Q: What problem does the Repository pattern solve?

It decouples the domain from data-access concerns. The domain talks to a collection-like interface (Add, GetById, Remove) and stays ignorant of EF Core, SQL, or the storage engine — honouring the Dependency Inversion Principle.

Q: Where should the repository interface live, and why?

In the domain/application layer, not infrastructure. The domain owns the contract; infrastructure provides the implementation. That inverts the dependency so the inner layers never reference the database.

Q: How does Repository relate to Unit of Work?

Repository handles per-aggregate reads/writes; Unit of Work tracks changes across repositories and commits them in a single transaction. EF Core's DbContext already plays the Unit of Work role and SaveChanges is the commit.

Q: Isn't EF Core's DbContext/DbSet already a repository?

Yes — DbSet<T> is a repository and DbContext is a Unit of Work. A thin generic repository over EF Core often adds little. Add your own only when you want to hide ORM types, centralise complex queries, or keep the domain persistence-ignorant.

Q: Generic IRepository<T> vs. specific repositories — which should you prefer?

Specific repositories (IOrderRepository) express intent and expose only the queries the aggregate needs. A generic IRepository<T> cuts boilerplate but encourages CRUD-for-everything and leaky IQueryable methods. Prefer specific; use a generic base only for shared plumbing.

Q: Why is returning IQueryable<T> from a repository considered a leak?

It lets callers compose queries the repository can't control or test, and leaks provider-specific translation, lazy evaluation, and N+1 risks across the boundary. Return materialised results (IReadOnlyList<T>) or accept a Specification object instead.

Q: How do you stop repositories from exploding with one method per query?

Use the Specification pattern — pass a query object describing the criteria — or expose intent-revealing methods per use case. Don't add a new method for every ad-hoc filter.

Q: How does Repository make testing easier?

Application and domain code depend on the interface, so tests inject an in-memory fake (a List<T>-backed repository) and assert behaviour with no database and fast feedback.

Q: What are the main criticisms of the pattern?

Over-abstraction on top of an ORM that is already a repository/UoW, leaky abstractions (IQueryable escaping), and generic repositories that become a dumping ground. The fix is to apply it deliberately — for domain isolation and query encapsulation — not reflexively.

Q: How does Repository fit Clean Architecture and DDD?

It's the boundary between the domain and the persistence adapter: one repository per aggregate root, interface in the domain, implementation in infrastructure, wired by DI — keeping the dependency arrows pointing inward.

See also