Software Design Reference System Designer · Designing Systems That Scale

Lesson 0023 · System Designer · Module 3

Ports & Adapters for Infrastructure

Lesson 0009 drew one arrow. Now draw them all: database, APIs, filesystem, clock, email — every technology behind a port, wired at a single composition root.

Mission tie-in: "apply Ports & Adapters to isolate infrastructure concerns" — the System Designer's full application of the pattern, and the shape your AI prompts should prescribe.

Knowledge: everything outside is an adapter

The domain speaks only to ports. The infrastructure — storage, gateways, files, the clock itself — implements them. The full inventory of infrastructure in a typical system:

# domain/ports.py — everything the core needs from the world:
class Clock(Protocol):
    def now(self) -> datetime: ...

class OrderRepository(Protocol):
    def get(self, order_id: str) -> Order: ...
    def add(self, order: Order) -> None: ...

# infrastructure/ — the world, conforming:
class SystemClock:
    def now(self) -> datetime:
        return datetime.now()

class SqlAlchemyOrderRepository:
    ...

# composition root — the only place the two meet:
def build_app() -> App:
    clock: Clock = SystemClock()
    orders: OrderRepository = SqlAlchemyOrderRepository(make_session())
    notifier: Notifier = EmailAdapter(smtp_config())
    checkout = CheckoutService(orders, notifier, clock)
    return App(checkout=checkout)

The composition root — one visible function where everything is constructed and injected — is the pattern's capstone. Nothing anywhere else says "SQLAlchemy" or "stripe" or "datetime.now". Swap a technology by editing this one function.

The cost, named honestly: indirection. The payoff: every port is a testing seam, a swap point, and a place where AI prompts (lesson 0010) get their structure. If a technology will never change and never needs faking, ask whether the port earns its indirection — small systems can skip it (KISS, lesson 0002).
Field notes · the infrastructure inventory
In the domain todayIts portIts fake in tests
datetime.now()Clock.now()FrozenClock(datetime(2026, 1, 1))
requests.post(...)PaymentGateway.charge()a fake recording the calls it received
open(path) / S3 uploadInvoiceStore.put()a dict-backed store
os.environ["API_KEY"]a settings object, passed ina plain dataclass built in the test
uuid4() / random.random()IdGenerator.next()a counter, so assertions can name the id

Skill: what gets a port?

The clock deserves a port because:

The composition root is where:

A technology that will never change and never needs faking:

Practice on your own code

List every technology your domain touches today (imports of SDKs, datetime.now(), filesystem, env vars). For each, mark: changeable? needs faking in tests? Give a port to every "yes"; wire them all in a composition root function.

Reveal: the inventory

A reporting service touches: SQLAlchemy (changeable, faked → port), an FTP uploader (changeable → port), and datetime.now() for "as of" timestamps (faked in tests → port). After: three ports, three adapters, one build_reporting_app() root, and the service's tests run offline with fakes for all three.

Your win

You can inventory a system's infrastructure, give each changeable or fakeable technology a port, and wire the whole system at one composition root — with the cost of indirection weighed honestly.

Read and watch deeper

Inventory your domain's technology imports with your agent-teacher and decide each port together.