HT
How Things Work
System Online

Repository & Unit of Work

Abstracting data access behind a collection-like interface, and committing changes across repositories as one atomic transaction — for decoupling, testability, and consistency.

How It Works

The Repository pattern hides persistence behind a collection-like interface so business code depends on an abstraction, not on SQL or an ORM. The Unit of Work coordinates changes across multiple repositories and commits them in a single atomic transaction. Together they decouple the domain from infrastructure, make logic testable with in-memory fakes, and keep multi-entity changes consistent.

1
The Repository Abstraction

A repository exposes a collection-like interface for a domain type — getById, add, remove, query — and hides how data is actually stored. Business code talks to IOrderRepository, not to SQL, an ORM, or an HTTP API. The persistence technology becomes a swappable detail.

2
Why It Helps: Decoupling & Testability

Because the rest of the app depends on an interface, you can swap a SqlOrderRepository for an InMemoryOrderRepository in tests — fast, deterministic, no database. Data-access concerns stay in one place instead of being smeared through the codebase.

3
Unit of Work: One Transaction

Real operations touch multiple repositories (insert an order AND update the customer). The Unit of Work tracks all those changes in memory and commits them together as a single atomic transaction — so either everything persists or nothing does. It owns the transaction boundary the repositories share.

4
Commit or Rollback Atomically

Staged changes live in memory until commit() flushes them in one transaction. If anything fails, rollback discards them all and the database is untouched. This keeps invariants that span entities (an order and its customer's balance) consistent without scattering transaction code through business logic.

Key Concepts

🗂️Repository

A collection-like abstraction over persistence for one aggregate/type, hiding the storage mechanism.

📦Unit of Work

Tracks changes across repositories and commits them as one atomic transaction (or rolls them all back).

🔒Transaction Boundary

The scope within which changes are all-or-nothing. The Unit of Work owns it so business code doesn't.

🙈Persistence Ignorance

Domain and application code that knows nothing about how (or where) data is stored.

🧬Generic vs Specific Repo

A generic Repository<T> covers CRUD; specific repositories add query methods that fit the domain.

♻️ORM as Unit of Work

EF Core's DbContext is already a Unit of Work and DbSet<T> a repository — wrapping them can be redundant.

Repository + Unit of Work
tsx
1// Repository = a collection-like abstraction over storage.
2interface IOrderRepository {
3 getById(id: OrderId): Promise<Order | null>;
4 add(order: Order): void; // staged, not yet saved
5 remove(order: Order): void;
6}
7
8// Unit of Work = one transaction across many repositories.
9interface IUnitOfWork {
10 orders: IOrderRepository;
11 customers: ICustomerRepository;
12 commit(): Promise<void>; // flush ALL changes atomically
13}
14
15// Application code knows nothing about SQL or transactions:
16async function placeOrder(uow: IUnitOfWork, cmd: PlaceOrderCmd) {
17 const customer = await uow.customers.getById(cmd.customerId);
18 const order = customer.placeOrder(cmd.items);
19 uow.orders.add(order);
20 customer.recordOrder(order.id);
21 await uow.commit(); // order INSERT + customer UPDATE in one tx
22}
23
24// NOTE: EF Core's DbContext already IS a Unit of Work, and DbSet<T>
25// is a generic repository — don't blindly wrap them in another layer.
💡
Why This Matters

Repository and Unit of Work are staples of layered and clean architectures, especially in .NET, and they come up constantly in design discussions. Knowing when they add value (test seams, complex queries, atomic multi-entity writes) — and when they're redundant over an ORM that already provides them — is a mark of practical seniority.

Common Pitfalls

Wrapping an ORM that's already a Unit of Work (EF Core's DbContext) in another redundant layer.
Leaky repositories that expose IQueryable, pushing query logic and ORM details back to callers.
A repository per table instead of per aggregate — fragmenting consistency boundaries.
Managing transactions inside individual repositories, so multi-repo operations aren't atomic.
Generic Repository<T> with dozens of one-off methods that becomes a thin, leaky pass-through.
Real-World Use Cases

1Half-Saved State After a Crash

Scenario

Placing an order inserts an order row and then updates the customer's loyalty points in a separate call. A failure between the two leaves orders saved but points never credited — data drifts out of sync.

Problem

The two writes weren't in one transaction. Each repository (or service) managed its own save, so a partial failure left the database in an inconsistent state with no atomic boundary around the operation.

Solution

Wrap both repositories in a Unit of Work and call commit() once. The order INSERT and customer UPDATE now run in a single transaction — either both succeed or both roll back. The business code expresses intent; the Unit of Work guarantees atomicity.

💡

Takeaway: When one operation changes multiple things that must stay consistent, a Unit of Work gives you a single transaction boundary — no more partially-applied changes from a mid-operation failure.

2The Redundant Repository Over EF Core

Scenario

A team wraps EF Core's DbContext in a hand-rolled generic Repository<T> and IUnitOfWork 'for clean architecture'. The wrapper leaks IQueryable anyway, blocks useful EF features, and adds boilerplate for every entity.

Problem

DbContext already implements the Unit of Work pattern and DbSet<T> already is a repository. The extra layer duplicated existing behavior, hid the ORM's strengths, and became a leaky, maintenance-heavy abstraction that bought nothing.

Solution

Use DbContext directly as the Unit of Work and DbSet as repositories, and add small, specific repository interfaces only where they earn their keep — to encapsulate complex queries or to provide a test seam for a particular aggregate.

💡

Takeaway: Don't add a repository layer reflexively. If your ORM already provides Unit of Work and repository semantics, abstract only where you get real value (testing seams, complex queries) rather than wrapping for ceremony.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.