Architecture & Patterns110 min total · 26 parts
ACID, SOLID, and Design Patterns: A Complete Software Design Reference
Part 18 of 26 · ~2 min
Structural Patterns: Repository and Unit of Work
Repository is the name for something already built earlier in this reference: tuck data-access code behind an interface so the rest of the application is coupled to that abstraction and never to a specific storage technology. IShiftRepository, back in the Dependency Inversion chapter, was already this pattern in full — this chapter just gives it its formal name.
public interface IShiftRepository
{
ShiftAssignment? GetById(int id);
void Add(ShiftAssignment assignment);
}
public class EfShiftRepository : IShiftRepository
{
private readonly AppDbContext _context;
public EfShiftRepository(AppDbContext context) => _context = context;
public ShiftAssignment? GetById(int id) => _context.ShiftAssignments.Find(id);
public void Add(ShiftAssignment assignment) => _context.ShiftAssignments.Add(assignment);
}
That buys something concrete: swap in the in-memory fake and ShiftAssignmentService runs inside a unit test with no database anywhere near the process. And whatever sits behind IShiftRepository in production — EF Core here, though Dapper or a call out to some other team's API would fit the same slot just as well — can be replaced later without a single line of business logic noticing.
Unit of Work sits a level above any single repository, collecting up everything that changed — potentially spanning several different repositories — and flushing it all to the database as one indivisible operation. That's the tool for a moment like a coordinator marking a shift filled, starting a payroll hour-ledger entry, and logging the assignment, where all three need to land together even though each one is technically owned by a different repository.
public interface IUnitOfWork
{
IShiftRepository Shifts { get; }
IVolunteerRepository Volunteers { get; }
Task<int> SaveChangesAsync();
}
public class EfUnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public IShiftRepository Shifts { get; }
public IVolunteerRepository Volunteers { get; }
public EfUnitOfWork(AppDbContext context)
{
_context = context;
Shifts = new EfShiftRepository(context);
Volunteers = new EfVolunteerRepository(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 AssignVolunteerAsync(IUnitOfWork uow, ShiftAssignment assignment)
{
uow.Shifts.Add(assignment);
uow.Volunteers.RecordHourLedgerEntry(assignment.VolunteerId, assignment.ShiftId);
await uow.SaveChangesAsync(); // Atomicity, from Part 1 of this reference, delivered right here
}
Worth knowing if you're working in .NET specifically: nobody has to hand-build Unit of Work from scratch, because DbContext already behaves like one under the hood. It quietly tracks every change across every DbSet it owns and only actually writes anything when SaveChanges() gets called — which is exactly why idiomatic EF Core code calls it once, after every change for a given operation has been staged, rather than firing it off after each individual assignment.