Hexagonal / Clean Architecture

Software Architecture Fundamentals

Chapter 7 · Hexagonal / Clean Architecture

Chapter 1's Repository was already an informal instance of this chapter's own idea — an abstraction the business logic depended on, with two swappable implementations behind it. Hexagonal (or "ports and adapters") architecture makes that pattern deliberate and universal: the business logic — the core — defines every external thing it needs as an abstract port, and every piece of infrastructure becomes an adapter plugging into a port from the outside. This chapter verifies the concrete payoff that buys.

Ports: Defined by the Core, Not by the Infrastructure

class InventoryPort: def get_stock_level(self, product_id): raise NotImplementedError class NotificationPort: def notify_low_stock(self, product_id): raise NotImplementedError class PricingEngine: # the core — depends ONLY on the two ports above def __init__(self, inventory_port, notification_port): self.inventory = inventory_port self.notifications = notification_port def calculate_price(self, product_id, base_price): stock = self.inventory.get_stock_level(product_id) if stock < 10: self.notifications.notify_low_stock(product_id) return base_price * 1.15 # scarcity pricing return base_price

Two Adapters per Port: One Fast and Fake, One Real

class FakeInventoryAdapter(InventoryPort): # fast, in-memory — for testing def __init__(self, stock_levels): self.stock_levels = stock_levels def get_stock_level(self, product_id): return self.stock_levels.get(product_id, 0) class RealInventoryAdapter(InventoryPort): # simulates a real DB query's latency def __init__(self, stock_levels): self.stock_levels = stock_levels def get_stock_level(self, product_id): time.sleep(0.01) return self.stock_levels.get(product_id, 0) # RealNotificationAdapter and FakeNotificationAdapter follow the identical shape
Verified directly — identical business result, whether the ports are backed by fakes or something simulating real I/O
Pricing PROD-1 (stock level 5, triggering the low-stock rule) through PricingEngine wired to fake adapters returns 114.99999999999999. The exact same call, through the exact same PricingEngine class, wired to "real" adapters instead, returns 114.99999999999999 — identical. Both correctly recorded the low-stock notification for PROD-1. PricingEngine's own logic never changed at all — only which adapter it was handed did.

Dependency Inversion, Verified — Not Just Named

Verified directly — the core has zero knowledge of any concrete adapter
Scanning PricingEngine's own source via inspect.getsource() for each concrete class name: 'FakeInventoryAdapter'False, 'RealInventoryAdapter'False, 'FakeNotificationAdapter'False, 'RealNotificationAdapter'False. PricingEngine only ever references InventoryPort and NotificationPort — abstractions it defines itself.
The inversion, stated precisely
In Chapter 2's plain layered architecture, business logic called down into a concretely-shaped data layer — the dependency pointed from business logic toward infrastructure. Here, both adapters point inward, toward a port the core itself defines — infrastructure depends on the core's own contract, not the other way around. This is the actual meaning of "dependency inversion": not that dependencies disappear, but that their direction reverses.

The Testability Payoff, Measured

Verified directly — a real, measured speedup from testing against fakes instead of real adapters
Running 50 calls to calculate_price(), each constructing a fresh PricingEngine, through fake adapters: 0.06 ms total. The identical 50 calls through adapters simulating real I/O latency: 1,037.73 ms total. Testing against fakes was ~18,111× faster — for verifying the exact same business logic, with the exact same assertions.
Why this matters beyond one benchmark
A real codebase doesn't run a business-logic test suite 50 times — it runs hundreds or thousands of tests, on every commit. Chapter 4's own measured ~11,661× network-call overhead already showed a network boundary is expensive at runtime; this chapter's own ~18,111× finding shows that exact cost compounding across an entire test suite, every time it runs, unless the core is genuinely decoupled from concrete infrastructure.

Where This Connects

This chapter's findingWhat it connects to
Chapter 1's Repository, generalized into named ports the core itself ownsConfirms this course's own recurring pattern — an early, informal boundary becomes a formal, named architectural style once its own payoff is measured directly
A ~18,111× testability speedup from swapping real adapters for fakesSoftware Testing Strategy (still reserved) — this chapter's own fake/real distinction is exactly what that course's own test-double material builds on
Zero source references from the core to any concrete adapter, verified directlyChapter 6's identical verification technique, applied to OrderService's own ignorance of its subscribers — the same proof technique, reused a second time

Hands-On Exercises

Exercise 1

Add a third port, DiscountPort, with a matching fake and "real" adapter (the real one adding a time.sleep(0.01)), and wire it into PricingEngine so a valid promo code applies an extra 5% off. Verify identical results from the fake and real versions, and confirm PricingEngine's own source still references no concrete adapter by name.

📄 View solution
Exercise 2

Re-run this chapter's own testability benchmark at N=200 instead of 50, for both fake and real adapters. Verify the speedup ratio stays in the same rough order of magnitude as this chapter's own ~18,111× result.

📄 View solution
Exercise 3

Using this chapter's own verified findings, explain specifically why PricingEngine being unable to reference RealInventoryAdapter by name is what makes the ~18,111× testability speedup possible — not just a separate, unrelated finding.

📄 View solution

Chapter 7 Quick Reference

  • Ports: abstract interfaces the core defines and depends on — never a concrete adapter
  • Adapters: concrete implementations plugging into a port from the outside — verified: fake and "real" adapters produced the identical business result (114.99999999999999)
  • Dependency inversion, verified: PricingEngine's own source contained zero reference to any of its four concrete adapter classes
  • Measured payoff: the same business-logic test ran ~18,111× faster through fake adapters than through ones simulating real I/O
  • Next chapter: Client-Server & API-Centric Architecture — REST as an architectural style in its own right