Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Ports, adapters, repositories

โญโญโญ advancedโณ 22 min read๐Ÿ“ Lesson 140 of 169

Own the interface in the core; translate at the edge.

Ports & adapters with real seams

A port is an interface owned by the application core. An adapter is a piece of infrastructure that fulfills it. The core defines NotificationSender and OrderRepository; the adapters are SendGridSender, SmtpSender, PostgresOrderRepository, InMemoryOrderRepository (for tests).

from typing import Protocol

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

class PlaceOrder:                      # application service โ€” the core
    def __init__(self, repo: OrderRepository, notify: NotificationSender):
        self._repo = repo
        self._notify = notify

    def __call__(self, order: "Order") -> None:
        self._repo.save(order)
        self._notify.order_placed(order)

What this buys, concretely:

  • Business logic tests run in-memory โ€” no Postgres, no HTTP, no mocks of the domain, only fakes at the port.
  • Swapping infrastructure is a wiring change, not a rewrite: Postgres โ†’ DynamoDB touches one adapter.
  • The core compiles without knowing HTTP exists.

The failure mode to avoid: a repository per table leaking SQL-shaped shapes (QuerySet, Row) into the domain. The port speaks the domain's language (Order, OrderNotFound), and the adapter translates.

โšก Now practice

Ready to Code
Repository PracticeA repository that speaks the domain's language โ€” and defends its own state.
1 challenge ยท ยท ~20 min