CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 18 of 26: Structural Patterns: Repository and Unit of Work
Part 18 of 26 · ~2 min

Structural Patterns: Repository and Unit of Work

Repository hides data-access logic behind an interface, so business logic depends on that abstraction instead of directly on a specific persistence technology — this is the pattern the Dependency Inversion example earlier in this reference was already demonstrating.

public interface IOrderRepository
{
    Order? GetById(int id);
    void Add(Order order);
}

public class EfOrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;
    public EfOrderRepository(AppDbContext context) => _context = context;
    public Order? GetById(int id) => _context.Orders.Find(id);
    public void Add(Order order) => _context.Orders.Add(order);
}

The direct payoff: OrderService (or any class depending on IOrderRepository) can be unit tested against a fake in-memory implementation with zero real database involved, and the concrete persistence technology (Entity Framework here, but it could be Dapper, MongoDB, or a REST call to another service) can be swapped without touching business logic at all.

Unit of Work tracks a set of changes across possibly multiple repositories and commits them together as a single transaction — solving the problem of a caller needing to update an order and decrement inventory and log an audit entry, all as one atomic operation, even though each of those might be handled by a different repository.

public interface IUnitOfWork
{
    IOrderRepository Orders { get; }
    IInventoryRepository Inventory { get; }
    Task<int> SaveChangesAsync();
}

public class EfUnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    public IOrderRepository Orders { get; }
    public IInventoryRepository Inventory { get; }

    public EfUnitOfWork(AppDbContext context)
    {
        _context = context;
        Orders = new EfOrderRepository(context);
        Inventory = new EfInventoryRepository(context);
    }

    public Task<int> SaveChangesAsync() => _context.SaveChangesAsync(); // one commit for every tracked change
}

// Both changes are staged, then committed together in one database transaction.
async Task PlaceOrderAsync(IUnitOfWork uow, Order order)
{
    uow.Orders.Add(order);
    uow.Inventory.Decrement(order.ProductId, order.Quantity);
    await uow.SaveChangesAsync(); // Atomicity, from the ACID section, delivered here
}

In .NET, EF Core's DbContext already functions as a Unit of Work internally — every tracked entity change across every DbSet it owns is committed together by one SaveChanges() call — which is exactly why application code calls SaveChanges() once at the end of a business operation, rather than after every individual property change.