Lesson 0013 · Core Designer · Module 3
Repository Boundaries That Don't Leak
A repository is a collection, from the domain's point of view. When it starts showing its storage internals, the domain stops being protected.
Mission tie-in: the repository is the port from lessons 0009 and 0010, specialized for storage. It's also the boundary your AI prompts should name explicitly.
Knowledge: a collection, not a query API
The repository's job: let the domain load and save aggregates as if they lived in a collection. The domain never learns about SQL, sessions, or ORMs. The port speaks domain language:
class OrderRepository(Protocol):
def get(self, order_id: str) -> Order: ...
def add(self, order: Order) -> None: ...
def delete(self, order_id: str) -> None: ...
class SqlAlchemyOrderRepository:
def __init__(self, session: Session) -> None:
self._session = session
def get(self, order_id: str) -> Order:
row = self._session.execute(
select(OrderRow).where(OrderRow.id == order_id)
).scalar_one()
return Order(...) # row -> domain aggregate
def add(self, order: Order) -> None:
self._session.add(OrderRow.from_domain(order))
The boundary's proof is the fake — a drop-in replacement with no storage at all:
class InMemoryOrderRepository: # implements the same port
def __init__(self) -> None:
self._orders: dict[str, Order] = {}
def get(self, order_id: str) -> Order:
try:
return self._orders[order_id]
except KeyError:
raise OrderNotFound(order_id) # the port's vocabulary, not the ORM's
def add(self, order: Order) -> None:
self._orders[order.order_id] = order
Twelve lines, no database, and every domain test runs against it in milliseconds. That is also the diagnostic: if your fake cannot be written in a dozen lines, the port is carrying storage semantics it should not — lazy loading, session lifetimes, flush ordering. Note the error too: the fake raises OrderNotFound, not KeyError and not the ORM's NoResultFound. The port owns its failure vocabulary as much as its methods.
What makes this boundary honest is what it refuses:
- No
session,query, or ORM types appear in the port or in domain callers. - Queries are expressed as domain questions:
get_unshipped(), notfilter(status=...)with raw column names. - The domain gets domain objects back — never rows, dicts, or ORM models.
- Persistence details — transactions, lazy loading, caching — live behind the boundary.
Test it the lesson-0009 way: a fake in-memory repository must be a drop-in, and the domain must never know which one it holds. If your fake has to mimic ORM behavior, the boundary leaked.
† Leak test: grep the domain folder for the ORM's import. Any hit is a hole — the dependency arrow from lesson 0009 is pointing the wrong way.| You see this | What it costs | The move |
|---|---|---|
| session in a port signature | Every caller has to know an ORM exists. | Take and return domain objects only. |
| query(Order).filter(...) in a service | SQL vocabulary in domain code; the fake must mimic an ORM. | repo.get_unshipped() — a domain question. |
| a repository returning OrderRow | Rows leak the schema; every caller learns the columns. | Map row to aggregate inside the adapter. |
| repo.save() called twice “to be safe” | Transaction semantics leaked, and were misunderstood. | One unit of work owns the commit. |
Skill: what crosses the boundary?
A repository port may expose:
"Find orders waiting for shipment" should appear in the repository as:
A fake in-memory repository proves the boundary works when:
Practice on your own code
Look at how your code fetches and saves domain objects. Grep the domain folder for your ORM's import. If it appears — or if the boundary doesn't exist yet — introduce a repository port with domain-named methods and move the ORM behind it.
Reveal: the leak audit
A CustomerService calls session.query(CustomerModel).filter_by(...) directly. Grep shows the ORM import inside the domain service. Fix: a CustomerRepository port with get_active(), implemented by an adapter that owns the session. The service now states its data needs as questions, not SQL.
Your win
You can design a repository boundary that speaks domain language, run the leak test (grep the ORM import), and prove it with an in-memory fake.
Read and watch deeper
- Architecture Patterns with Python, Percival & Gregory — ch. 2 "The Repository Pattern": the clearest working treatment, with the fake-repository test.
- Implementing Domain-Driven Design, Vernon — ch. 12 "Repositories": design rules for repository interfaces.
- Domain-Driven Design, Evans — ch. 6: the original repository concept, and why it exists.
- Watch: ArjanCodes YouTube — search "repository pattern".
- Reference: Patterns — Repository and Unit of work, the transaction boundary the repository sits inside.
Show your agent-teacher your data-access code and audit the boundary together — grep included.