32. Repository and Unit of Work
Answer as submitted
“Yes — Repository and Unit of Work are patterns I have implemented across .NET, Java and Python codebases.”
Expanded answer
Yes, in all three stacks, and the mechanics differ while the intent does not. Repository: the interface is declared by the domain and named for the question being asked (GetActiveByPlanAsync), never for the mechanism; complex filtering becomes a named specification instead of a leaking IQueryable; and the implementation is the only place that names EF Core, Spring Data JPA or SQLAlchemy. Unit of Work: one transactional boundary per use case — an explicit SaveChangesAsync in EF Core, the proxied @Transactional method and its persistence context in Spring, a session wrapped in a context manager in SQLAlchemy — with the outbox row written inside that same transaction so an event can never be published for a change that rolled back. The two compose with CQRS (write side through aggregates, read side through projections), with decorators for caching and logging so cross-cutting code never enters the repository, and with domain events raised inside the transaction but dispatched only after the commit. And they stop where they stop earning their place: no generic IRepository<T> CRUD over every entity, no repository wrapping a repository, no forcing a reporting screen through the domain model, and no abstraction over an ORM that is already an abstraction.
| Aspect | .NET | Java / Spring | Python |
|---|---|---|---|
| Repository | domain interface + EF Core implementation | port interface + Spring Data / JPA adapter | Protocol + SQLAlchemy implementation |
| Unit of work | DbContext change tracking + one SaveChanges | proxied @Transactional method and persistence context | session wrapped in a context manager |
| Query variation | ISpecification<T> to an expression tree | Spring Data Specification | a select()-returning specification object |
| Cross-cutting | decorator around the interface, registered in the composition root | AOP advice or an explicit decorator bean | decorator implementing the same protocol |
| Events | raised in the aggregate, published after SaveChanges | @TransactionalEventListener(AFTER_COMMIT) | staged on the UoW, published after commit() |
| Reporting read | Dapper projection | projection query, readOnly = true | column projection, aggregate never materialised |
Live repository & unit-of-work workflow proof
The running component below changes its pattern/result view through real event binding. Select an aspect to see the code shape, the reasoning behind the decision, and the evidence that it holds.
Choose an aspect of the two patterns
Select an aspect to inspect the code shape, the reasoning and the evidence.
$ ICustomerRepository → GetActiveByPlanAsync(plan, ct) · matching(ISpecification<Customer>)
The interface belongs to the domain and expresses intent, so the caller names the question it is asking instead of assembling a query object. The implementation lives in infrastructure, where EF Core, Dapper, Spring Data or SQLAlchemy can be swapped without the domain noticing.
query objects reaching the domain layer: 0 repository implementations per stack: 3 (EF Core, Spring Data JPA, SQLAlchemy) public surface: intent-named methods + specifications, no IQueryable
What implementing the patterns buys
Source files
Select a file to inspect the proof with syntax highlighting and line numbers. The first file is the code running the component above; the others implement the two patterns in .NET, Java/Spring and Python, because the mechanics differ while the contract does not.
Acceptance criteria
- Declares the repository in the layer that needs it and names its methods for intent.
- Keeps one transactional boundary per use case, with events staged inside it and dispatched after the commit.
- Shows an interactive proof of choosing an aspect and reading the evidence.
- Implements the same contract in .NET, Java and Python rather than describing it once.
- States where a repository stops earning its place and refuses the generic CRUD base.