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:
- Persistence →
OrderRepository(lesson 0013) - External APIs →
PaymentGateway,SupplierFeed - Filesystem →
InvoiceStore - Email/notifications →
Notifier - The clock →
Clock.now()— even time is infrastructure worth a port
# 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).| In the domain today | Its port | Its 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 upload | InvoiceStore.put() | a dict-backed store |
os.environ["API_KEY"] | a settings object, passed in | a 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
- Architecture Patterns with Python, Percival & Gregory — ch. 2–4: the working pattern — repository, service layer, and the composition root.
- "Hexagonal Architecture", Cockburn — the origin, and the "application is a core surrounded by adapters" diagram.
- Clean Architecture, Martin — ch. 17–18: boundaries, the dependency rule, and crossing the boundary cleanly.
- Watch: ArjanCodes YouTube — search "ports and adapters".
- Next: lesson 0024 — keeping async out of domain logic.
- Reference: Patterns — Ports & Adapters, composition root; Design Principles — depend on abstractions.
Inventory your domain's technology imports with your agent-teacher and decide each port together.