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
Two Adapters per Port: One Fast and Fake, One Real
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
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 Testability Payoff, Measured
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.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
Chapter 1's Repository, generalized into named ports the core itself owns | Confirms 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 fakes | Software 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 directly | Chapter 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
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.
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.
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.
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