Software Design Reference Core Designer · Protecting the Domain Core

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:

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.
Field notes · what it looks like in real code
You see thisWhat it costsThe move
session in a port signatureEvery caller has to know an ORM exists.Take and return domain objects only.
query(Order).filter(...) in a serviceSQL vocabulary in domain code; the fake must mimic an ORM.repo.get_unshipped() — a domain question.
a repository returning OrderRowRows 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

Show your agent-teacher your data-access code and audit the boundary together — grep included.