Module 5 · Architecture and maintainable code

Lesson 24 — SOLID and dependency injection

Layer separation and dependencies pointing in the right direction.

Published
In this lesson
  1. Objectives
  2. 1. SOLID on TicketFlow (each letter, an example of yours)
  3. 2. TicketFlow's layers
  4. 3. Injection in Python (without gory containers)
  5. 4. Architecture debt signals (the smells you read in review)
  6. Self-assessment

Stack: Django/DRF · Project: TicketFlow Status: Published — opening the architecture module Prerequisite: Lesson 23 — Secrets and GDPR


Objectives

  1. Explain each SOLID principle with an example from YOUR project (not with tutorial Shape/Circle).
  2. Separate layers: view (HTTP) → service (domain) → models (persistence), and know what belongs in each.
  3. Apply dependency injection in Python without ceremony (duck typing + protocols).

1. SOLID on TicketFlow (each letter, an example of yours)

S — Single Responsibility: reservar() (10) reserves; it doesn't send emails or compute taxes. The view translates HTTP; the service executes domain; the model persists and maintains invariants. When a function needs "and also", that is the signal.

O — Open/Closed: adding a new payment method (lesson 08's gift card, gateway B) without touching existing code: a PaymentMethod with a common interface (charge(amount, intent)) and a registry of implementations, instead of an if method == "card" elif... that grows forever. Extending = adding a class; modifying = no.

L — Liskov: every implementation of PaymentGateway respects the contract: charge returns or raises, never silently returns None "sometimes". If a simulated gateway returns {"ok": True} and the real one {"status": "ok"}, the code using them breaks — substitutability is the exact contract, exceptions included.

I — Interface Segregation: the availability view doesn't need a "repository that also sends emails". Small interfaces per consumer: AvailabilityReader (reads), ReservationWriter (writes) — clients don't depend on methods they don't use.

D — Dependency Inversion: the reservation service doesn't import psycopg or Redis directly: it receives clock, payment_gateway, cache by parameter/constructor. The domain defines what it needs; the infrastructure fulfills it.

2. TicketFlow's layers

views/serializers  →  HTTP: parse, auth, status codes (13/26)
services/          →  DOMAIN: rules, transactions (10), invariants
models/            →  PERSISTENCE: tables, constraints, state machines (00b)

Boundary rules: the view does NOT query the ORM in loops (that is domain logic); the service does NOT build a Response nor read request (that is HTTP); the model does NOT import Redis or call APIs (that is infrastructure). The cotton test: can you call reservar(user, seats) from a management command, a worker and a test without a Django request? If yes, the layers are right.

3. Injection in Python (without gory containers)

Python doesn't need Spring: dependencies enter by parameter with defaults, and duck typing + typing.Protocol document the contract:

python
class Clock(Protocol):
    def now(self) -> datetime: ...

class SystemClock:
    def now(self) -> datetime:
        return timezone.now()

def reservar(user, seats, *, clock: Clock = SystemClock(), expires_in: timedelta = timedelta(minutes=10)):
    expires_at = clock.now() + expires_in     # the test injects FakeClock and controls time

Lesson 10's test with a fixed expires_at stops depending on timezone.now(); the job's expiration test (29) advances the virtual clock. DI here is: constructor + production default + fake in tests. A container only when the graph truly grows (not this project).

4. Architecture debt signals (the smells you read in review)

  • A 900-line utils.py (the junk drawer): split by domain.
  • Importing requests/redis inside services/reservations.py: a hard infrastructure dependency → inject it.
  • if user.role == "STAFF" repeated in 12 places: the living rule in one place (permissions.py/services).
  • A 150-line view: mixing HTTP+domain: extract a service.
  • Behavior-less (anemic) models with all logic in services: valid, but the schema invariants (00b) must stay in the model — the project's balance: state machines in the model, flows in services.

Self-assessment

  1. Your reservation view has 150 lines with ORM in loops: what do you extract and which layer does each piece go to?
  2. Which concrete problem does the L in PaymentGateway solve, and how does its violation show up in tests?
  3. Why is Clock the first dependency worth injecting in a reservation system?
  4. When is an "anemic" model a problem signal and when is it a legitimate choice in Django?
  5. Which cotton test do you apply to know your layers are properly separated?

Continue with the exercises. The solutions only after trying it yourself.