02 — Clean & Hexagonal Architecture (Ports & Adapters)

At a glance

After completing this lesson you can:

Time: ~50 minutes · Prerequisites: 01 — SOLID & Cohesion

Reading Companion

Why this matters in production

When domain business logic is intertwined directly inside FastAPI route handlers or Django ORM models, upgrading a database driver, changing web frameworks, or adding a CLI runner requires refactoring everything. Under Hexagonal Architecture, the core domain model has zero external imports. Changing PostgreSQL to DynamoDB or adding a gRPC server merely means writing a new Adapter—the core business logic remains untouched.

Deep dive

1. The Hexagonal / Clean Architecture Onion

           [ HTTP / FastAPI ]      [ CLI ]
                     \               /
                      v             v
             +-------------------------------+
             |       Primary Adapters        |
             |  +-------------------------+  |
             |  |      Service Layer      |  |
             |  |   (Application Ports)   |  |
             |  |  +-------------------+  |  |
             |  |  |   Domain Model    |  |  |
             |  |  | (Entities, Rules) |  |  |
             |  |  +-------------------+  |  |
             |  +-------------------------+  |
             |       Secondary Adapters      |
             +-------------------------------+
                      ^             ^
                     /               \
            [ PostgreSQL / SQL ]   [ S3 / Email ]

Hands on

python3 phase1-cs-and-architecture/03-software-architecture/02-clean-and-hexagonal-architecture/examples/01_demo.py
python3 phase1-cs-and-architecture/03-software-architecture/02-clean-and-hexagonal-architecture/examples/02_trap.py

AI Trap & Code Review

# BAD AI-GENERATED CODE — DOMAIN LEAKING INTO INFRASTRUCTURE
from fastapi import HTTPException
from models import DBOrder  # Direct database model import

def cancel_order(db_order: DBOrder):
    if db_order.status == "SHIPPED":
        raise HTTPException(status_code=400, detail="Cannot cancel shipped order") # HTTP in domain logic!
    db_order.status = "CANCELLED"

Exercises

  1. Rung 1 (Domain Entity Validation): Implement a pure domain entity Subscription with invariant methods (cancel(), renew()) raising domain exceptions (never HTTP exceptions).
  2. Rung 2 (Port Definition — Fix the AI Bug): Define clear SubscriptionRepository and PaymentGateway ports as typing.Protocol.
  3. Rung 3 (Service Layer Use Case): Implement the RenewSubscriptionUseCase service coordinating the domain entity, repository, and gateway.
  4. Rung 4 (Fake Adapters): Build in-memory fake adapters satisfying the ports for rapid unit verification.
  5. Rung 5 (Primary CLI Adapter): Build a command-line entrypoint adapter that drives the use case and translates domain exceptions into standard POSIX exit codes (0 or 1).

Wrapping up

Can you explain?

  1. What is the Dependency Rule in Clean Architecture?
  2. Why should domain entities never raise HTTPException?
  3. What is the difference between a Primary (Driving) Adapter and a Secondary (Driven) Adapter?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 02 Clean & Hexagonal Architecture

  1. Isolation of Domain State: The Subscription entity enforces its own invariants (credits >= 0, status checks). No external service can put the entity into an illegal state.
  2. Deep Copy in In-Memory Fakes: Always copy.deepcopy() in repository fakes to prevent caller mutations from silently updating stored state without calling save().

Self-check

Run python3 -m unittest phase1-cs-and-architecture/03-software-architecture/02-clean-and-hexagonal-architecture/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.