Showing version 1 bot legacy · api · 2026-07-24T07:16:50Z

Clean Architecture

Clean Architecture

When to use it: designing, refactoring, or reviewing code for coupling, testability, or
framework independence — "where does this logic go?", "structure this module", or applying
hexagonal / onion / ports-and-adapters. Language-agnostic. Keyword bait: "clean architecture",
"uncle bob", "dependency rule", "SOLID", "layers", "boundaries".

Apply Clean Architecture from Robert C. Martin's Clean Architecture: A Craftsman's Guide to
Software Structure and Design
(2017). Goal: software that is independent of frameworks, UI,
database, and external agencies
, and testable in isolation. The cost of change stays low
for the life of the system.

The Dependency Rule (the one rule)

Source code dependencies point inward only. Inner circles know nothing about outer
circles. Names, classes, functions, variables, or any entity declared in an outer circle must
not be mentioned by an inner circle. Data crossing a boundary inward must be in a form
convenient for the inner circle — never pass an outer-layer struct (DB row, ORM model, HTTP
DTO) into a use case.

The four layers (inside → out)

Layer Contains Knows about
Entities Enterprise-wide business rules; the highest-level policies. Plain objects/functions. Nothing outside themselves.
Use Cases Application-specific business rules. Orchestrate entities to fulfill a user goal. Entities only.
Interface Adapters Controllers, presenters, gateways, repositories. Convert data between use-case form and external form. Use cases + entities.
Frameworks & Drivers Web framework, DB, UI toolkit, devices, external services. The outermost shell. Everything inward (it plugs in).

Number of circles is not sacred — what matters is the inward dependency direction.

SOLID (class-level)

  • SRP — A module has one reason to change, i.e. one actor it answers to. Conflate two
    actors' rules into one class → they collide.
  • OCP — Open for extension, closed for modification. Achieved by partitioning behavior
    behind interfaces so new behavior is added via new code, not edits to existing code.
  • LSP — Subtypes must be substitutable. Violations break OCP and force if (type==X).
  • ISP — Don't depend on what you don't use. Split fat interfaces; clients shouldn't
    recompile/redeploy when an unused method changes.
  • DIP — Depend on abstractions, not concretions. Volatile concretes (DB, framework, IO)
    sit behind interfaces owned by the inner layer. Inversion means the arrow of source
    dependency points opposite to the arrow of control flow at the boundary.

Component principles (package-level)

Cohesion — what belongs together:

  • REP (Reuse/Release Equivalence) — The unit of reuse is the unit of release. Version
    components.
  • CCP (Common Closure) — Classes that change for the same reasons at the same times go in
    the same component. SRP for components.
  • CRP (Common Reuse) — Classes used together belong together; don't force consumers to
    depend on stuff they don't use. ISP for components.

These three pull against each other — pick the tension point matching the project's maturity:
early systems lean CCP (ease of change); mature, widely-reused systems lean REP/CRP.

Coupling — how components relate:

  • ADP (Acyclic Dependencies) — The component graph must be a DAG. Break cycles with DIP
    or by extracting a new component both depend on.
  • SDP (Stable Dependencies) — Depend in the direction of stability. Volatile components
    depend on stable ones, not the reverse.
  • SAP (Stable Abstractions) — A stable component should be abstract; an unstable
    component should be concrete. Stability and abstractness correlate, else you get the
    Zone of Pain (stable + concrete, e.g. a hard-to-change DB schema) or Zone of
    Uselessness
    (abstract + unstable).

Boundaries

A boundary is a line across which the Dependency Rule is enforced. Cross it with:

  • A polymorphic interface declared by the inner side, implemented by the outer side (DIP).
  • DTOs / simple structs owned by the inner side. Never leak ORM entities, framework
    request objects, or DB rows inward.

Boundaries cost — don't draw one where you don't need it. Draw one where:

  • Two sides change at different rates / for different reasons.
  • One side is volatile (framework, DB, external service); the other is policy.
  • You need to defer a decision (which DB, which framework) without blocking progress.

Key tactics

  • Humble Object — Split hard-to-test code into a humble outer shell (thin, dumb,
    framework-bound, e.g. the View, the DB driver wrapper) and a testable inner core
    (Presenter, Repository logic). Test the core; ignore the humble.
  • Screaming Architecture — Top-level package names should shout the domain
    (billing/, enrollment/), not the framework (controllers/, models/). You should be
    able to tell what the system does without opening a file.
  • Plugins — Outer rings are plugins to inner rings. The DB, the UI, the web are details.
    Code so any one of them could be swapped without touching use cases.
  • The main componentmain is the ultimate plugin, the dirtiest layer. It instantiates
    concrete adapters and injects them into use cases. Use cases never know main exists.
  • Defer decisions — A good architecture maximizes the number of decisions not yet
    made
    . Pick the DB late, pick the framework late. Keep them behind boundaries until forced
    to commit.

Crossing a boundary (control flow vs. source dependency)

At a boundary, control flows out → in → out (e.g. Controller calls UseCase calls Presenter).
But source dependencies all point inward: the Controller depends on the UseCase input port;
the UseCase depends on the output port (an interface it defines); the Presenter implements
that port. This dependency inversion is the architect's main lever.

Controller ──▶ <<UseCase Input Port>> ◀── UseCaseInteractor
                                              │
                                              ▼
                                      <<UseCase Output Port>>
                                              ▲
                                              │
                                         Presenter ──▶ View

Solid arrows = source dependency. Control flows left→right top, then top→bottom, then
bottom→right.

Common smells (review checklist)

  • Entity or use case imports a framework, ORM, HTTP library, or env config. ❌
  • Use case takes a DB row / ORM model as a parameter. ❌ Wrap in DTO.
  • Controller contains business rules (validation beyond shape, calculations, policy). ❌
    Move to use case.
  • Repository interface lives in the adapter layer, not in the use-case layer. ❌ Invert.
  • if (type == ...) chains over a known closed set of subtypes. ❌ LSP/OCP — polymorph.
  • One class edited by two different teams for two different reasons. ❌ SRP — split.
  • Package graph has cycles (a → b → a). ❌ ADP — break with an interface.
  • Stable, widely-depended-on component is concrete and full of business detail. ❌ SAP —
    extract abstractions.
  • Top-level folder layout reads controllers/ models/ services/ and tells you nothing about
    the domain. ❌ Screaming Architecture.
  • Tests require booting the framework or hitting the DB to assert a business rule. ❌ Humble
    Object — extract the rule.

When applying to a task

  1. Name the actors the change serves. Different actors → different modules (SRP).
  2. Identify the layer the change touches. A new business rule → entity/use case. A new
    endpoint shape → adapter. A new persistence engine → frameworks & drivers.
  3. Check the direction of every new dependency. Arrow points inward? If not, invert with
    an interface owned by the inner side.
  4. Decide whether a new boundary is justified (different rate of change, deferred
    decision, volatility). If not, don't add one — boundaries cost.
  5. Confirm the test strategy: business rules unit-testable without framework, DB, UI?
    If no, apply Humble Object.
  6. For new packages, place them so the stability gradient is correct: abstract+stable
    inward, concrete+volatile outward (SAP).

When NOT to apply heavily

  • Throwaway scripts, spikes, one-shot migrations — boundaries cost more than they save.
  • Tiny CRUD services with no policy — a transaction script is fine; don't manufacture use
    cases around INSERT.
  • Pre-product-market-fit prototypes — defer architecture investment until the domain
    stabilizes; CCP says group by what changes together, which you don't yet know.

Architecture is about the cost of change over the system's lifetime. If lifetime is short
or change is rare, the rule still holds — it just prescribes less structure.

Output style when used in review/design

  • Cite the specific principle (SRP, DIP, ADP, etc.) and the specific line/module
    where it's broken or upheld.
  • Propose the smallest move that restores the rule (extract interface, move type,
    invert dependency) — not a rewrite.
  • Distinguish policy (business rules, high-level) from detail (framework, IO,
    format). Most architectural mistakes confuse the two.

See also

References

  • Martin, R.C. Clean Architecture (2017), chs. 5 (SOLID), 13–14 (Components), 15–22
    (Architecture), 23 (Presenters/Humble Objects), 34 (The Missing Chapter).
  • Cockburn, A. Hexagonal Architecture (2005) — same shape, ports & adapters vocabulary.
  • Jeffrey Palermo, Onion Architecture (2008) — same shape, layered vocabulary.