Clean Architecture

Clean Architecture is Robert C. (“Uncle Bob”) Martin’s 2012 synthesis of several pre-existing architectural patterns — Alistair Cockburn’s Hexagonal Architecture, Jeffrey Palermo’s Onion Architecture, Trygve Reenskaug’s DCI (Data, Context, Interaction), and the Boundary–Control–Entity (“BCE”) pattern from Ivar Jacobson’s object-oriented analysis — into a single named, prescriptive layered model. The architecture’s signature image is a set of four concentric rings: the innermost ring is Entities (enterprise-wide business objects), surrounded by Use Cases (application-specific business rules), surrounded by Interface Adapters (gateways, presenters, controllers that translate between use cases and the outside world), surrounded by Frameworks & Drivers (web frameworks, databases, UI toolkits, message brokers — the technology). The single non-negotiable rule is the Dependency Rule: source-code dependencies always point inward, toward higher-level policy, never outward toward lower-level detail. Entities know about nothing external; Use Cases know about Entities but not the rings outside them; Interface Adapters know about Use Cases and Entities but not Frameworks; Frameworks sit at the rim and know about everything inside them but are themselves unknown to the inner rings. Martin’s blog post (https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) and the 2017 book Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Prentice Hall) crystallised the framing; the .NET community in particular adopted it heavily, and Jason Taylor’s Clean Architecture solution template (https://github.com/jasontaylordev/CleanArchitecture) became the canonical reference implementation. This note covers the origin and the four rings, the Dependency Rule and its implementation via Dependency Inversion, the use-case-as-class convention that distinguishes Clean from its hexagonal sibling, real-world adopters (Jason Taylor’s template, Android Jetpack’s recommended architecture, ABP Framework, Quarkus reference apps), the pitfalls (over-prescription, “where does X go?” decision fatigue, the temptation to skip the Use Case ring), and a worked example of a banking transfer.

1. When to Use, When Not To

Clean Architecture is a strong default for enterprise applications with non-trivial business rules and an expected lifespan of years. It is best suited where (a) the domain has rules that are independent of how the user interacts with the system (banking ledger rules, insurance underwriting policies, supply-chain optimisation logic), (b) the technology stack is expected to change at least once over the system’s life (Spring 4 → Spring Boot → Spring Boot 3 → some 2030s framework; SQL Server → Postgres; jQuery → Angular → React → some 2030s UI library), and (c) the team has the maturity to absorb the upfront ceremony cost. Martin’s own framing in the 2017 book is that Clean Architecture is not a prescription but a manifestation of underlying principles (SOLID, the Common Closure Principle, the Stable Dependencies Principle); teams that understand the principles can produce Clean-shaped code in any naming convention.

The architecture also fits well when the team has explicit use-case thinking — that is, when the application’s value is best described as “the system does X for actor Y” with X being a business operation. Banking: “a customer transfers money from one account to another.” Healthcare: “a clinician records a vital sign.” Logistics: “a dispatcher assigns a load to a driver.” In these systems, the use case is the central architectural unit, and Clean’s Use Cases ring elevates that unit to first-class status. Each use case becomes one class, with one method (typically execute(Input) returning Output), and the whole application is a catalogue of use cases that the outer rings dispatch to. This use-case-centric framing is what most cleanly distinguishes Clean from Hexagonal Architecture (which leaves the internal structure of the domain hexagon to the team).

Clean is the wrong choice for systems where the architectural overhead is not repaid. Pure CRUD admin tools — where the use case literally is “expose this database table over HTTP” — should not have a Use Case class for each endpoint; the layered Spring/Express idiom is more honest about the simplicity. Short-lived prototypes whose lifespan is measured in weeks are similarly poor candidates: the time spent creating ring-aligned packages is not recovered. ML model-serving wrappers, ETL scripts, and infrastructure-glue services — anything dominated by data movement and integration with little business logic — should not be Clean-shaped; the architecture’s promises depend on there being substantial domain logic to protect, and these systems do not have it. Khononov’s Learning Domain-Driven Design (2021, ch. 9) names this directly: “Apply Clean Architecture to the bounded contexts that justify it. Apply simpler patterns elsewhere.”

A nuanced indicator is cross-cutting team conventions. Clean Architecture imposes a strong vocabulary (Entities, Use Cases, Interface Adapters, Frameworks); a team that has a different vocabulary already established (e.g., a Vernon-school DDD shop with “Application Services” and “Domain Services”) may find the Clean naming jarring even though the underlying structure matches. Consistency within a team or organisation matters more than which specific name is used; a team that has standardised on Hexagonal vocabulary should not switch to Clean naming for one new service just because the new engineer learned Clean from the book.

A final consideration is the strength of the engineering culture. Clean Architecture is prescriptive and works best in cultures that follow the prescriptions. In cultures where every team interprets architecture differently, Clean’s specificity is an advantage (it gives concrete answers to “what goes where”); in cultures with strong existing architectural vocabulary, the prescription can clash with locally-developed conventions. Honest assessment of the local culture is more useful than dogmatic adherence to any one architectural style.

2. Structure

flowchart TB
    subgraph Frameworks["Ring 4 — Frameworks & Drivers"]
        Web["Spring MVC / Express"]
        DB["JPA / EF Core / Mongo Driver"]
        UI["React / Angular"]
        Mq["Kafka / SQS"]
    end
    subgraph Adapters["Ring 3 — Interface Adapters"]
        Ctrl["Controllers"]
        Pres["Presenters / View Models"]
        Gate["Gateways<br/>(repository implementations)"]
    end
    subgraph UseCases["Ring 2 — Use Cases"]
        UC1["TransferMoney<br/>UseCase"]
        UC2["CancelOrder<br/>UseCase"]
        UC3["Boundary interfaces<br/>(input/output ports)"]
    end
    subgraph Entities["Ring 1 — Entities"]
        E1["Account"]
        E2["Order"]
        E3["Money (Value Object)"]
        E4["Enterprise rules"]
    end

    Web --> Ctrl
    UI --> Ctrl
    Mq --> Ctrl
    Ctrl --> UC3
    UC3 --> UC1
    UC3 --> UC2
    UC1 --> Entities
    UC2 --> Entities
    Gate --> UC3
    DB --> Gate

What this diagram shows. Four concentric rings, drawn as nested subgraphs. The outermost ring (Frameworks & Drivers) contains the technology — Spring, JPA, Kafka, the React UI — and is the only ring permitted to depend on third-party libraries directly. Ring 3 (Interface Adapters) contains the translators: Controllers translate HTTP requests into use-case input boundaries; Presenters translate use-case output into UI-friendly view models; Gateways implement the repository interfaces that Use Cases depend on. Ring 2 (Use Cases) is the application’s business rules — one class per use case, plus the boundary interfaces (input ports, output ports) the use case requires. Ring 1 (Entities) at the centre holds the enterprise-wide business objects — Account, Order, Money — that have meaning across multiple applications in the enterprise (a banking enterprise’s Account might be used by the public web app, the back-office app, and the batch settlement job, all of which are different applications running on the same Account entity).

The arrows in the diagram point inward: Frameworks know about Adapters, Adapters know about Use Cases, Use Cases know about Entities. The reverse is forbidden. Crucially, dependencies sometimes need to point “outward” at runtime — for instance, a Use Case needs to call a Gateway to persist an Entity — and Clean Architecture handles this with the Dependency Inversion Principle (DIP, the D in SOLID, Martin 1996): the Use Case defines an output boundary interface (in Ring 2), the Gateway implements that interface (in Ring 3), and at runtime the Use Case calls the Gateway via the interface. The source-code dependency is from the Gateway (outer) to the interface (inner), preserving the inward-only rule even though control flows outward.

The four-ring count is canonical but not load-bearing. Martin’s book and blog acknowledge that some applications need only two or three rings; others need five. The invariant is the inward-pointing dependency direction. Many .NET Clean Architecture projects use three rings (Domain, Application, Infrastructure) by collapsing Entities and Use Cases together; many Android Clean Architecture projects use four (Domain, Use Cases, Data, Presentation). The naming and partitioning is malleable; the dependency rule is not.

3. Core Principles

The architecture rests on five interlocking principles, each of which has a deeper SOLID-or-Common-Closure-Principle root.

The Dependency Rule. Source-code dependencies always point inward. Stated concretely in Martin’s blog: “Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes functions, classes, variables, or any other named software entity.” This is the architecture’s load-bearing rule; every other rule serves it.

Dependency Inversion at Boundaries. When control needs to flow outward (Use Case calls Gateway), the dependency is inverted via an interface owned by the inner ring. Concretely: the Use Case ring contains an interface IAccountRepository with method findById(AccountId); the Interface Adapter ring contains a class JpaAccountRepository implements IAccountRepository. The Use Case calls accountRepository.findById(id) against the interface; at runtime Spring (a Framework concern) injects the concrete class. The source-code dependency is from JpaAccountRepository (outer) to IAccountRepository (inner) — inward, satisfying the Dependency Rule — even though the runtime call is outward.

Use Cases Are Application-Specific Business Rules. A use case represents one application action — Transfer Money, Cancel Order, Adjudicate Claim — and is implemented as one class in Ring 2. The class typically has one public method, conventionally named execute or handle, taking an Input Boundary (a request DTO) and returning or invoking an Output Boundary (a response DTO or a presenter callback). The use-case class orchestrates entities, calls gateways, and produces output; it does not know about HTTP, JSON, or SQL. Clean Architecture’s commitment to one-class-per-use-case is what most distinguishes it from sibling architectures; in Hexagonal terms, every inbound port has its own dedicated implementation class, not just a method on a shared application service.

Entities Encode Enterprise Business Rules. An Entity is a business object with rules that hold across applications in the enterprise. Martin’s example: a bank has a Loan entity with rules about interest accrual that apply equally to the customer-facing web app, the back-office reporting app, and the regulatory-filings batch job. The Loan’s rules belong to the bank, not to any specific app. In single-application systems the distinction between Entities and Use Cases blurs; in multi-application enterprises it is significant. The corollary: Entities are testable in isolation — instantiate a Loan, call its methods, assert the result — without any application context.

Frameworks Are Details. Martin’s most provocative claim is that the web framework, the database, and the UI library are details of the system, not architectural concerns. The architecture should be expressible — in tests, in design discussions, in onboarding documentation — without naming the framework. A reader looking at the Use Cases ring should not be able to tell whether the application is a Spring Boot service, a Quarkus service, or a plain Java main(). Practically, this means the framework’s annotations and types do not appear in Rings 1 and 2; they live only at the outer rim. The discipline is enforced by build-system separation (the Domain Maven module declares no Spring dependency) or by lint rules (ArchUnit assertions in Java; NetArchTest in .NET).

The combination of these five principles produces the architecture’s headline benefits: the inner rings are framework-independent (and thus survive framework migrations); the inner rings are testable without infrastructure (millisecond unit tests); and the architectural intent is legible in the code’s structure (a new engineer can see the rings and infer where a feature should go).

4. The Four Rings, In Detail

4.1 Ring 1 — Entities

Entities are the enterprise-wide business objects. In a banking system: Account, Loan, Customer, Transaction, plus value objects like Money, AccountNumber, IBAN. Each entity has its rules attached as methods: Account.debit(Money), Account.credit(Money), Loan.accrueInterest(Period). The rules enforce invariants — Account.debit rejects amounts greater than the available balance; Loan.accrueInterest updates the principal according to the loan’s compounding schedule. Entities are plain old objects in the language’s idiom — POJO in Java, POCO in .NET, dataclass-with-methods in Python — with no framework decoration.

The boundary between Entity and Value Object is conventional: entities have identity (an Account is identified by its account number; two accounts with the same balance and currency are different accounts), value objects do not (a Money of 100). The DDD vocabulary calls these out explicitly; Clean Architecture inherits the distinction without belabouring it.

In a multi-application enterprise, entities live in a shared library that all applications import. In a single-application system, entities live in a Domain module of the application itself. The ring structure is the same; the deployment differs.

4.2 Ring 2 — Use Cases

A use case is a single business operation, expressed as a class with an execute(Input) method. The class is named after the action: TransferMoneyUseCase, CancelOrderUseCase, AdjudicateClaimUseCase. The Input is a request DTO with the data the use case needs; the Output is either a return value, an output-port callback, or both, depending on the team’s convention.

Each use case orchestrates: it loads entities via Gateway interfaces (defined in Ring 2 but implemented in Ring 3), exercises the entities’ methods, and persists results via the same Gateway interfaces. It enforces application-level policies — concurrency control, transaction boundaries, authorization checks — that are not part of the entities’ domain rules.

A common Ring-2 element is the boundary interface. Following Jacobson’s Boundary–Control–Entity framing, Use Cases declare:

  • An Input Boundary (the use-case interface itself, e.g., interface TransferMoneyUseCase { execute(Input) }).
  • An Output Boundary (a presenter interface, e.g., interface TransferMoneyOutputPort { presentSuccess(Receipt); presentFailure(Reason) }).
  • Repository / Gateway Boundaries (interfaces for outbound calls, e.g., interface AccountRepository { findById(AccountId): Account }).

The use-case class implements the Input Boundary and depends on the other two boundaries. Spring’s DI or .NET’s IoC container wires concrete implementations at runtime.

4.3 Ring 3 — Interface Adapters

Interface Adapters are translators. They convert data between the convenient form for entities and use cases (Ring 1–2 vocabulary) and the convenient form for whatever framework or external agent is on the rim (Ring 4 vocabulary). Three sub-types are canonical:

  • Controllers. Receive raw input from a framework (e.g., an HTTP request from Spring MVC), translate it into a Use Case Input Boundary call, invoke the use case, and let the use case’s output flow back via a Presenter. Controllers are thin: they parse, validate, dispatch.
  • Presenters. Receive a Use Case Output Boundary call, translate the result into a view model — a structure shaped for the specific UI or response format. The view model is what the UI binds to; the use case never knew the view model existed.
  • Gateways. Implement the Repository or external-system interfaces declared by use cases. A JpaAccountRepository implementing AccountRepository, a StripePaymentGateway implementing PaymentGateway, an S3DocumentStore implementing DocumentStore. Gateways own the framework-shaped entities (JPA @Entity classes), the SQL or NoSQL queries, and the mappers between framework entities and domain entities.

Interface Adapters depend on Rings 1 and 2; they do not depend on Ring 4 except via the interfaces the framework provides. A subtle point: Spring MVC’s @RestController annotation lives in Ring 4; the Controller class that uses the annotation lives in Ring 3 because it imports the annotation. The annotation crosses the ring boundary and so the Controller is part of the boundary, not part of the inner rings. This is why Clean Architecture is sometimes described as “the rings get fuzzier as you move outward” — Ring 3 inevitably touches Ring 4 because it is the translation point.

4.4 Ring 4 — Frameworks and Drivers

The outer ring is “where everything goes that doesn’t matter.” Spring Boot, the JPA implementation, the message-broker client, the UI toolkit, the database. Martin’s framing is deliberate: the framework is a detail, not a partner. Most code in this ring is configuration: Spring @Configuration classes wiring beans, application-properties files, framework-specific bootstrappers. Some teams treat the main method itself as living in Ring 4, which is correct — the main method’s job is to wire the rings together and start the framework.

Putting the framework on the outermost ring is what produces the architecture’s substitutability property: the framework can be replaced (Spring Boot → Micronaut, JPA → MyBatis, RESTGraphQL) without touching Rings 1–3. This is a stronger version of the Hexagonal Architecture adapter-replaceability claim because Clean explicitly factors configuration (which is where framework-specific glue accumulates) into a ring of its own.

5. Request Flow

sequenceDiagram
    autonumber
    participant Client as HTTP Client
    participant Ctrl as TransferController<br/>(Ring 3 — Interface Adapter)
    participant UC as TransferMoneyUseCase<br/>(Ring 2 — Use Case)
    participant SrcAcct as Source Account<br/>(Ring 1 — Entity)
    participant DstAcct as Destination Account<br/>(Ring 1 — Entity)
    participant Repo as AccountRepository<br/>(Ring 2 boundary,<br/>Ring 3 implementation)
    participant DB as JPA / Postgres<br/>(Ring 4 — Framework)
    participant Pres as TransferPresenter<br/>(Ring 3 — Interface Adapter)

    Client->>Ctrl: POST /transfers {from, to, amount}
    Ctrl->>UC: execute(TransferInput)
    UC->>Repo: findById(fromId)
    Repo->>DB: SELECT * FROM account WHERE id=:id
    DB-->>Repo: AccountEntity row
    Repo-->>UC: Account (domain object)
    UC->>Repo: findById(toId)
    Repo-->>UC: Account
    UC->>SrcAcct: debit(amount)
    UC->>DstAcct: credit(amount)
    UC->>Repo: save(srcAccount)
    UC->>Repo: save(dstAccount)
    Repo->>DB: UPDATE ...; UPDATE ...
    UC->>Pres: presentSuccess(TransferReceipt)
    Pres-->>Ctrl: TransferViewModel
    Ctrl-->>Client: 200 OK {receipt JSON}

Walkthrough. The client (1) POSTs a transfer request. The Controller in Ring 3 (2) parses the JSON, builds a TransferInput DTO using domain types (AccountId, Money), and invokes the Use Case via its Input Boundary interface. The Use Case (3–6) loads both accounts via the AccountRepository boundary; the boundary lives in Ring 2 (the use case knows it as an interface), and the implementation lives in Ring 3 (a JPA-backed adapter). The boundary call goes to JPA (4), which goes to Postgres (4–5), and the result is mapped back to a domain Account entity. The Use Case (7–8) then exercises the entities’ rules: srcAcct.debit(amount) enforces that the source has sufficient balance and applies the debit; dstAcct.credit(amount) applies the credit. Both rule enforcements live on the entities, not in the use case — this is the load-bearing decision that distinguishes Clean from procedural service layers. The Use Case (9–11) persists both modified accounts via the same Repository boundary, this time mapping inward-to-outward through JPA. Finally, the Use Case (12) invokes its Output Boundary — presentSuccess on a Presenter — passing a domain Receipt; the Presenter (13) translates the receipt into a view model shaped for the HTTP response, and the Controller (14) returns it.

The sequence has a striking property: the direction of source-code dependency is inward at every step, even though control flows outward (Use Case calls Repository, calls Presenter). The Repository call works because the Use Case depends on the interface, which lives in Ring 2; the JPA implementation depends on the same interface, also in Ring 2; both rings outside know about Ring 2, but Ring 2 knows about nothing outside. This is Dependency Inversion at work, and it is what makes the inner rings replaceable, testable, and durable.

6. Variants and Siblings

Clean Architecture has multiple sibling architectures and variants; the family resemblance is significant.

Hexagonal Architecture (Cockburn 2005). The most direct ancestor. Cockburn’s hexagonal predates Clean by seven years and codifies the same inward-pointing dependency rule. The differences are mostly in vocabulary (Hexagonal uses “ports” and “adapters”; Clean uses “boundaries” and “interface adapters”) and prescriptiveness (Hexagonal does not specify a use-case ring; Clean does). For most production codebases the resulting code is indistinguishable.

Onion Architecture (Palermo 2008). The other direct ancestor. Palermo’s concentric rings (Domain Model → Domain Services → Application Services → Infrastructure & UI) parallel Clean’s rings closely. Clean is sometimes regarded as Onion + the Use Cases ring made explicit; the historical truth is that Clean independently arrived at a similar structure while drawing on Hexagonal more directly than on Onion. .NET shops more often use Onion vocabulary; Java shops more often use Hexagonal; both communities use Clean as a unifying name.

Boundary–Control–Entity (BCE). Ivar Jacobson’s 1992 model from Object-Oriented Software Engineering, where each use case is realised by Boundary objects (UI), Control objects (orchestration), and Entity objects (persistent state). Clean’s Use Case ring is essentially BCE’s Control objects; Clean’s Entity ring is BCE’s Entity objects; Clean’s Interface Adapters are BCE’s Boundary objects. Martin acknowledges the influence in the 2017 book.

DCI (Data, Context, Interaction). Trygve Reenskaug and Jim Coplien’s 2009 framework (https://www.artima.com/articles/dci_vision.html) where data objects (entities) are augmented at runtime with role-based behaviour for the duration of a use case (context). DCI is more academic than industrial but informs Clean’s “use cases are first-class” framing.

Use-Case Driven Object-Oriented Analysis (Rosenberg). Doug Rosenberg’s Use Case Driven Object Modeling with UML (2007) puts use cases at the centre of the design, anticipating Clean’s Ring 2. The book is older and more UML-focused; Clean is the modern, more pragmatic descendant.

Ports and Use Cases (the synthesis). Many production codebases adopt a hybrid: hexagonal vocabulary for inbound/outbound ports, Clean’s use-case-as-class convention for the Ring 2 contents. This is arguably the modern default in Java/Kotlin/.NET enterprise applications and corresponds to what Khononov’s 2021 book treats as the canonical pattern. The naming is Hexagonal; the granularity is Clean.

7. Real-World Examples

Jason Taylor’s Clean Architecture template (.NET). The single most-cited Clean Architecture reference implementation (https://github.com/jasontaylordev/CleanArchitecture). Provides a four-project .NET solution (Domain, Application, Infrastructure, Web) with MediatR-driven CQRS, EF Core, and ASP.NET Core. The template’s structure is so canonical that “Clean Architecture in .NET” effectively means “the Jason Taylor template” in many .NET teams.

Android architecture guide. Google’s Guide to App Architecture (https://developer.android.com/topic/architecture) recommends a layered architecture closely modelled on Clean: a UI layer (Activities, Fragments, ViewModels) on top, a Domain layer (Use Cases, optional) in the middle, a Data layer (Repositories, data sources) at the bottom. The recommendation is explicitly influenced by Clean Architecture and similar layered patterns, and Android’s Jetpack libraries (ViewModel, LiveData, Room) are designed to fit the layering.

ABP Framework (.NET). A widely-used .NET application framework (https://abp.io/) that ships an opinionated Clean Architecture project template with strong DDD integration. ABP-based applications are essentially forced into a Clean structure by the framework’s project layout.

Quarkus reference applications. Red Hat’s GraalVM-native Java framework Quarkus ships several reference applications (https://github.com/quarkusio/quarkus-quickstarts) that adopt Clean-shaped layering. The Quarkus Superheroes sample (https://github.com/quarkusio/quarkus-super-heroes) is particularly clean.

eShopOnContainers (Microsoft). Microsoft’s reference microservices architecture (https://github.com/dotnet-architecture/eShopOnContainers) implements its Ordering and Catalog services with explicit Clean/Onion-shaped layering. The accompanying e-book .NET Microservices: Architecture for Containerized .NET Applications dedicates a chapter to “Designing the application layer” using Clean-style boundaries.

Java Spring Boot enterprise default. Many enterprise Spring Boot codebases adopt Clean Architecture explicitly, with domain, application, adapter Maven modules. Tom Hombergs’ Get Your Hands Dirty on Clean Architecture (2019, Packt) is the most-recommended Java-specific treatment.

Uber’s mobile architecture. Uber’s “Riblets” architecture for mobile apps (publicised at the 2017 Mobile@Scale conference) is a Clean-influenced architecture with Routers, Interactors (≈ Use Cases), Builders, and Components, replacing MVC for the iOS and Android apps. The Riblets pattern preserves the Dependency Rule and the Use Case-as-class idea while specialising for mobile concerns.

8. Tradeoffs

The benefits of Clean Architecture overlap heavily with Hexagonal Architecture’s — testability, technology agnosticism, multi-channel support — and add prescriptiveness: a new engineer reading the package layout immediately understands the structure, because the rings have canonical names and canonical contents. The use-case-as-class convention forces small, single-responsibility classes; the test-first disposition is natural because a use case can be tested with hand-rolled gateway implementations.

The costs are also more pronounced than with hexagonal: boilerplate and ceremony are higher because Clean prescribes more (Input Boundaries, Output Boundaries, Presenter interfaces, view models, request DTOs, gateway interfaces, gateway implementations); decision fatigue is real (“does this go in Use Cases or Interface Adapters?”); and the temptation to skip the Use Case ring is constant, because for trivial CRUD endpoints a Use Case feels like overhead. Skipping it is a Pyrrhic victory: once one CRUD endpoint bypasses the Use Case ring, the architectural integrity is compromised, and the rationale (“just for simplicity”) tends to spread.

A specific cost worth naming is mapper sprawl. A request flows through Web DTO → Use Case Input → Domain Entity → Gateway DTO → Database column, with mappers at every boundary. For a system with many entities and many use cases, the mapper code can rival the business code in line count. MapStruct (Java) and AutoMapper (.NET) reduce the burden but do not eliminate it. The discipline pays off when changes happen in isolation — a database schema change touches only the Gateway-DTO mapper — but feels excessive day-to-day.

A second-order cost is the risk of cargo-cult adoption. A team that reads the Clean Architecture book and adopts the rings without internalising the Dependency Rule produces a Clean-shaped codebase with the discipline silently broken — Spring annotations on Entities, JPA types in Use Case method signatures, framework imports in the Domain module. The architectural promises depend on the discipline; the discipline depends on team understanding and enforcement. Projects without ArchUnit-style enforcement frequently degrade silently.

The honest framing parallels Hexagonal’s: Clean is front-loaded — high upfront cost, payback over years. For long-lived, domain-heavy applications it is worth the investment; for short-lived, simple applications it is overhead.

9. Migration Path

Migrating to Clean from a layered Spring Boot codebase typically proceeds feature-by-feature.

Stage 1 — Identify a target feature. Pick one feature (say, “transfer money between accounts”) and extract its logic into a TransferMoneyUseCase class. The class lives in a new application module that depends on a new domain module. The original Spring service still exists; it now delegates to the use case.

Stage 2 — Move entities to the Domain module. The Account entity, which was probably a JPA @Entity in the Spring codebase, gets a sibling: a domain Account class in the domain module with no JPA annotations, with rules like debit(Money) attached as methods. The JPA @Entity stays in the persistence module; a mapper (MapStruct) converts between them at the boundary. This is the most painful single stage because it duplicates entities and forces explicit translation; it is also where the architectural value is unlocked, because once entities are in the Domain module they are testable in isolation.

Stage 3 — Define gateway boundaries. The AccountRepository interface moves into Ring 2 (the application module’s port package, in many Maven layouts). A JpaAccountRepository adapter in Ring 3 (the infrastructure or persistence module) implements the interface; Spring’s DI wires the adapter into the use case at runtime. The use case now compiles without Spring on its classpath.

Stage 4 — Introduce Output Boundaries and Presenters. For features that need to return data, introduce a Presenter interface (Ring 2) and a concrete Presenter implementation (Ring 3) that produces the view model. The Controller calls the Use Case, the Use Case calls the Presenter, and the Presenter populates a view model that the Controller returns. Some teams skip this stage and have Use Cases return view models directly; this is a violation of strict Clean but is common in pragmatic implementations.

Stage 5 — Repeat per feature. Each subsequent feature follows the same path. Over months, the legacy Spring service layer shrinks until the original code is fully replaced. The migration can run for quarters; what matters is that each step is shippable.

Stage 6 — Add ArchUnit enforcement. Once the target structure is reached, add architecture-fitness tests (ArchUnit in Java, NetArchTest in .NET) that fail the build on inward-pointing-dependency violations. Without enforcement the structure drifts; with enforcement it stays.

The reverse migration (from Clean back to a flatter architecture) is uncommon but happens when the team realises the prescription was excessive for their needs. The path is to inline Use Cases into Controllers (or merge with thin Application Services), inline gateway interfaces with Spring Data interfaces directly, and remove the mappers. The result is more compact, less testable, and lower-ceremony — appropriate for systems where the previous Clean adoption was theatre.

10. Pitfalls

  1. The Anemic Domain Model — entities as DTOs. A team adopts the four-ring structure but writes entities as field-only objects with no methods, putting all business logic into Use Cases. The result is procedural code with the form of Clean Architecture but none of its substance. Fowler’s Anemic Domain Model (2003, https://martinfowler.com/bliki/AnemicDomainModel.html) names this anti-pattern; Clean Architecture does not prevent it. The fix is to push behaviour onto entities — account.debit(amount) rather than accountService.debit(account, amount).

  2. Skipping the Use Case ring for “simplicity.” A team starts skipping Use Cases for trivial CRUD endpoints, with the controller calling the gateway directly. Once one feature bypasses the ring, the discipline erodes; over months, half the application has Use Cases and half does not. The architectural assumptions (Use Cases are the only entry to business rules; cross-cutting concerns are applied in Use Cases) silently break. The fix is consistency: either every entry has a Use Case, or none do (in which case the architecture is layered, not Clean).

  3. Where does X go? — decision fatigue. Engineers spend disproportionate time debating whether a piece of code belongs in Entities or Use Cases, in Use Cases or Interface Adapters. The questions are real (where does email-sending logic go? where does authorisation live?) and there is no universal answer; teams need conventions. The fix is a written team decision document — an ADR specifying answers to the recurring questions (“authorisation policies live in Use Cases; permission checks live on Entities”). Without the document, the same questions are re-litigated weekly.

  4. Use Cases as procedural orchestrators with anemic entities. Related to pitfall 1, but specifically describes the case where the Use Case becomes a 200-line orchestrator that fetches data, applies rules procedurally, and saves. The class is implementing a transaction script (Fowler’s term, PoEAA 2002), not an OO use case. The fix is to push collaboration onto entities and value objects: a domain service for cross-entity coordination, behaviour-rich entities for single-entity rules.

  5. Coupling Use Cases to specific Presenters. A Use Case that calls htmlPresenter.present(...) (specific to a single view technology) has coupled itself to the Presenter implementation rather than to the abstract Output Boundary. The use case is no longer reusable across channels. The fix is to depend only on the Output Boundary interface; concrete Presenters (HTML, JSON, XML) are wired at composition time.

  6. Over-applying the Entity ring in single-application systems. Clean Architecture’s Entity ring is designed for multi-application enterprises where entities are shared across applications. In single-application systems the distinction between Entities and Use Cases has lower value, and trying to maintain it as a strict separation produces awkward partitioning (“does email-recipient validation belong in the Customer entity or the Use Case?”). For single-application systems many teams effectively merge the rings; Khononov’s 2021 book argues this is fine.

  7. Treating Frameworks as Less of a Detail Than Claimed. Martin’s claim that “frameworks are details” is aspirational; in practice, the choice of Spring Boot vs Quarkus vs Micronaut affects the application structure beyond the outer ring. Spring’s DI conventions, AOP weaving, and component-scanning subtly leak into the inner rings (e.g., Use Cases are discovered as @Service beans). Strict Clean rejects this; pragmatic Clean accepts it as the cost of using a framework. Be explicit about which compromise the team is making.

  8. Forgetting that source-code direction and runtime call direction differ. A common confusion in code reviews: “this Use Case calls a Gateway, so it depends on the Gateway.” No: the Use Case depends on the Gateway interface, which is in Ring 2; the Gateway implementation depends on the same interface. The runtime call goes outward; the source-code dependency goes inward. Conflating the two leads to misplaced lint rules and incorrect critiques.

11. Comparison With Sibling Architectures

The most informative comparison is with Hexagonal Architecture. Both share the Dependency Rule. The differences:

  • Number of rings. Hexagonal has two (inside/outside the hexagon); Clean has four. The four-ring count enables Clean to give a name to Use Cases as a distinct concept; Hexagonal folds use cases into “the domain” without distinguishing them from entities.
  • Prescriptiveness. Clean is more prescriptive about ring contents and naming; Hexagonal leaves more to the team. For teams that want guidance, Clean is more comfortable; for teams that want flexibility, Hexagonal is.
  • Use-case granularity. Clean’s “one class per use case” convention is its most distinctive feature. Hexagonal does not mandate this; many hexagonal codebases have a single application service with multiple methods. The Clean convention produces smaller classes and better aligns with the Single Responsibility Principle but at the cost of more files.

Compared with Onion Architecture: very similar; Onion came first (2008) and informs Clean’s structure. The differences are mostly cosmetic — Onion is more explicitly layered with concentric rings; Clean adds the use-case-as-class convention. .NET teams more often say “Onion”; Java teams more often say “Clean” or “Hexagonal.” Substantively the architectures are interchangeable.

Compared with Layered Architecture (N-Tier): Clean is the inversion of layered. In layered, dependencies cascade top-down; the database is the foundation. In Clean, dependencies point inward; the database is one of many peripheral details. The practical outcome is that Clean codebases can swap databases more easily and test without infrastructure; layered codebases typically cannot.

Compared with Microservices Architecture: Clean is internal to a service; microservices is about composition between services. The two are orthogonal and frequently combined: many microservices teams apply Clean within each service.

Compared with Domain-Driven Design Tactical Patterns: Clean is an implementation pattern for what DDD calls bounded contexts. The DDD tactical patterns (Entity, Value Object, Aggregate, Repository, Domain Service, Domain Event, Factory) populate Rings 1–2 of a Clean Architecture; the DDD strategic patterns (bounded contexts, context maps) inform when and how to apply Clean per service.

Compared with the Boundary–Control–Entity (BCE) pattern from Jacobson 1992: Clean is essentially BCE elaborated with the Dependency Rule and the four-ring image. Clean’s Use Cases ≈ BCE’s Control objects; Clean’s Interface Adapters ≈ BCE’s Boundary objects; Clean’s Entities ≈ BCE’s Entity objects. The structural correspondence is direct.

12. Common Interview Discussion Points

“Walk me through Clean Architecture.” A strong answer names the four rings, gives an example of what lives in each, and explicitly states the Dependency Rule. A weak answer recites the rings without explaining the rule; a stronger answer notes that source-code dependencies always point inward even when control flows outward, and explains Dependency Inversion as the mechanism.

“What’s the difference between Clean and Hexagonal?” A nuanced answer notes that they are sibling architectures with the same dependency-direction rule; Clean is more prescriptive (four rings, use-case-as-class), Hexagonal is more flexible (two-zone, no mandated internal partitioning). A weak answer treats them as opposites; a strong answer explains the historical genealogy (Hexagonal predates Clean; Clean synthesises Hexagonal, Onion, BCE, DCI).

“Where does authorisation live?” A great answer says “it depends on whether it’s an enterprise rule (Entity ring) or an application rule (Use Case ring)” and explains the distinction with an example. A weak answer says “it’s a cross-cutting concern, use AOP” without engaging with the architectural placement.

“How would you test a Use Case?” A strong answer instantiates the Use Case in a unit test with hand-rolled gateway implementations (in-memory Map-backed AccountRepository, deterministic Clock, recording EventPublisher) and asserts on entity state plus output-port interactions. A weak answer describes integration tests against the database; this is correct for testing the Gateway adapter, not the Use Case.

“Have you used Clean in a system that didn’t need it?” Candidates with real experience often have war stories about over-engineering — applying Clean to a CRUD admin tool, a glue service, or a short-lived prototype. The willingness to name those mistakes signals architectural maturity.

“How do you enforce the Dependency Rule?” Strong answers cite ArchUnit, NetArchTest, or build-system module separation. Talking about discipline alone is weaker; talking about enforcement is stronger. A candidate who says “we just review carefully” is signalling that the architecture probably degrades over time.

“How does this relate to DDD?” A strong answer connects Clean’s Use Cases to DDD’s Application Services, Clean’s Entities to DDD’s Aggregates, and Clean’s Gateways to DDD’s Repositories. The vocabularies are different but the underlying architecture is closely aligned. A candidate who ties Clean’s bounded ring boundary to DDD’s bounded context shows senior fluency.

The signal value: Clean Architecture vocabulary suggests the candidate has worked in teams with deliberate architectural conventions. Strong familiarity with all four rings and the Dependency Inversion mechanism indicates the candidate has at least read the book; ability to discuss specific pitfalls (#1–8 above) indicates production experience.

13. Worked Example — Banking Transfer in Java

The example targets a banking-transfer use case with a hexagonal-shaped Maven multi-module project structured in Clean’s four rings.

13.1 Domain Module — Entity Ring

// package com.acme.bank.domain.entity
public final class Account {
    private final AccountId id;
    private final Currency currency;
    private Money balance;
 
    public Account(AccountId id, Currency currency, Money initialBalance) {
        this.id = id; this.currency = currency; this.balance = initialBalance;
    }
 
    public void debit(Money amount) {
        if (!amount.currency().equals(currency)) {
            throw new DomainException("currency mismatch");
        }
        if (amount.isGreaterThan(balance)) {
            throw new InsufficientFundsException(id, balance, amount);
        }
        balance = balance.subtract(amount);
    }
 
    public void credit(Money amount) {
        if (!amount.currency().equals(currency)) {
            throw new DomainException("currency mismatch");
        }
        balance = balance.add(amount);
    }
 
    public AccountId id() { return id; }
    public Money balance() { return balance; }
}

The Account entity owns its rules: currency matching, sufficient-funds checks, balance updates. Nothing about HTTP, JPA, or Spring is here.

13.2 Application Module — Use Case Ring

// package com.acme.bank.application.usecase
public final class TransferMoneyUseCase {
    private final AccountRepository accounts;
    private final TransactionLedger ledger;
    private final TransferPresenter presenter;
 
    public TransferMoneyUseCase(AccountRepository accounts,
                                TransactionLedger ledger,
                                TransferPresenter presenter) {
        this.accounts = accounts; this.ledger = ledger; this.presenter = presenter;
    }
 
    public void execute(TransferInput input) {
        Account source = accounts.findById(input.sourceId())
            .orElseThrow(() -> new AccountNotFoundException(input.sourceId()));
        Account dest = accounts.findById(input.destinationId())
            .orElseThrow(() -> new AccountNotFoundException(input.destinationId()));
 
        try {
            source.debit(input.amount());
            dest.credit(input.amount());
            accounts.save(source);
            accounts.save(dest);
            ledger.record(new TransferRecord(source.id(), dest.id(), input.amount(), Instant.now()));
            presenter.presentSuccess(new TransferReceipt(source.id(), dest.id(), input.amount()));
        } catch (InsufficientFundsException e) {
            presenter.presentFailure("insufficient funds in " + e.accountId());
        }
    }
}

The Use Case orchestrates: load entities, exercise rules, save, record audit, present. It has no Spring annotations; the wiring layer instantiates it with concrete adapter beans.

13.3 Application Module — Boundary Interfaces

// package com.acme.bank.application.port
public interface AccountRepository {
    Optional<Account> findById(AccountId id);
    void save(Account account);
}
public interface TransactionLedger {
    void record(TransferRecord record);
}
public interface TransferPresenter {
    void presentSuccess(TransferReceipt receipt);
    void presentFailure(String reason);
}

All boundary interfaces live in Ring 2 and use only Ring 1–2 types. No JPA, no Spring, no JSON.

13.4 Infrastructure Module — Interface Adapters Ring

// package com.acme.bank.infrastructure.persistence
@Component
public class JpaAccountRepository implements AccountRepository {
    private final SpringDataAccountRepository jpa;
    private final AccountMapper mapper;
    public JpaAccountRepository(SpringDataAccountRepository jpa, AccountMapper mapper) {
        this.jpa = jpa; this.mapper = mapper;
    }
    @Override
    public Optional<Account> findById(AccountId id) {
        return jpa.findById(id.value()).map(mapper::toDomain);
    }
    @Override
    public void save(Account account) {
        jpa.save(mapper.toEntity(account));
    }
}
 
interface SpringDataAccountRepository extends JpaRepository<AccountEntity, UUID> {}
 
@Entity @Table(name = "account")
class AccountEntity {
    @Id private UUID id;
    private String currency;
    private BigDecimal balance;
    // JPA-required fields, getters, setters
}

The adapter owns the JPA @Entity and the mapper; the domain Account and the JPA AccountEntity are distinct types that the mapper bridges.

13.5 Web Module — Interface Adapters Ring

// package com.acme.bank.web
@RestController @RequestMapping("/transfers")
public class TransferController {
    private final TransferMoneyUseCase useCase;
    private final HttpTransferPresenter presenter;
    public TransferController(TransferMoneyUseCase useCase, HttpTransferPresenter presenter) {
        this.useCase = useCase; this.presenter = presenter;
    }
    @PostMapping
    public ResponseEntity<TransferResponse> transfer(@RequestBody TransferRequest req) {
        useCase.execute(new TransferInput(
            new AccountId(req.from()), new AccountId(req.to()), Money.of(req.amount(), req.currency())
        ));
        return presenter.lastResponse();
    }
}
 
@Component
class HttpTransferPresenter implements TransferPresenter {
    private ResponseEntity<TransferResponse> last;
    @Override public void presentSuccess(TransferReceipt r) {
        last = ResponseEntity.ok(new TransferResponse("success", r.amount().value()));
    }
    @Override public void presentFailure(String reason) {
        last = ResponseEntity.badRequest().body(new TransferResponse("failed", reason));
    }
    public ResponseEntity<TransferResponse> lastResponse() { return last; }
}

The Controller is thin; the Presenter holds the response shape (which it builds from Use Case callbacks).

13.6 The Pay-Off

Unit tests in the Application module instantiate TransferMoneyUseCase with hand-rolled in-memory implementations of the three boundary interfaces. They run in milliseconds, exercise every rule branch (including currency mismatch, insufficient funds, missing accounts, ledger failure), and never touch Spring or Postgres. Integration tests in Infrastructure use Testcontainers to bring up Postgres and verify the JPA mapping is correct; web tests in Web use @WebMvcTest with a mocked Use Case to verify the controller’s HTTP shape. The architecture is paying for its overhead in test speed (sub-second domain test suite versus minutes-long integration suite), test reliability (no flaky containers in unit tests), and test exhaustiveness (it is feasible to cover every rule branch when the test cost is millisecond-scale).

If the team migrates from Postgres to DynamoDB, they write DynamoAccountRepository in a new Infrastructure module, swap the wiring in the configuration, and run the same domain tests unchanged. If they add a Kafka-listener inbound channel that triggers transfers from external events, they write a listener that constructs a TransferInput and calls the same Use Case — no new business code, no duplicated rules. If they add a CLI for back-office operators, the CLI command builds another TransferInput and calls the same Use Case. The architectural promise — one canonical implementation of the business operation, many channels and infrastructures around it — is realised concretely. This is the moment when the upfront investment in rings, boundaries, and adapters pays back.

A worth-noting subtlety: the Use Case’s presenter.presentSuccess callback couples the Use Case to a single Presenter instance per request, which is fine for synchronous single-request flows but breaks for batch processing or fan-out scenarios. For those, return-value-style Use Cases are simpler. Strict Clean Architecture mandates Output Boundaries; pragmatic Clean lets Use Cases return values when callbacks add no value. The team’s ADR should document which convention applies.

14. Anti-Patterns Catalogue

14. Anti-Patterns Catalogue

Several Clean-specific anti-patterns recur in codebases where the architecture has been adopted but not internalised. They overlap with the pitfalls above but deserve their own naming because they tend to appear together as a cluster rather than individually — diagnosing one usually reveals the others, and the remediation is rarely a single fix but a deliberate re-engagement with the architecture’s underlying principles.

The CRUD-Use-Case Explosion. A team applies one-class-per-use-case strictly, generating CreateUserUseCase, ReadUserUseCase, UpdateUserUseCase, DeleteUserUseCase, ListUsersUseCase for each entity. For an enterprise with twenty entities, this produces a hundred Use Case classes that are 80% boilerplate. The classes have no business logic; they are thin wrappers over Spring Data calls. The fix is to recognise that trivial CRUD does not warrant a Use Case — the Clean Architecture book is explicit that Use Cases capture business operations, not data-shape operations. For pure CRUD endpoints, a thin Controller-to-Repository path is honest about the absence of logic. A team that prevents this anti-pattern frequently uses a hybrid: Use Cases for genuine business operations (TransferMoney, AdjudicateClaim) and direct Controller-to-Repository plumbing for pure CRUD (ListProductCategories, GetUserProfile).

The God Use Case. The opposite anti-pattern: a single Use Case class implements every operation in a domain area, with methods like placeOrder, cancelOrder, updateOrder, applyDiscount, ten methods in one class. Once again the architecture has degraded to a Spring Service with a different name. The fix is to honour the one-class-per-use-case convention; ten use cases means ten classes, even when they share helpers (extract the helpers to a domain service or value object).

Output Boundary Theatre. A team introduces Output Boundary interfaces and Presenters but the Presenters do nothing — they just hold a value the Controller reads later. The Output Boundary adds indirection without reuse, because there is only one Presenter per Use Case and it is never substituted. The fix is to recognise when the Output Boundary is genuinely useful (multiple presentation channels: HTML, JSON, terminal output) versus when a return value would suffice. For single-channel applications, a Use Case returning an Output DTO is simpler than a Use Case calling a single-implementation Presenter.

The Entity-As-DTO Cluster. Related to the Anemic Domain Model, but specifically describes the case where Entities are converted to DTOs at every ring boundary, with multiple parallel DTO hierarchies (request DTOs, response DTOs, view models, persistence DTOs, serialisation DTOs). The mapper code outweighs the business code. The fix is to share immutable, behaviour-rich value objects directly across boundaries while keeping aggregate-shaped entities on the inner-rings side, with translation only where genuinely needed (HTTP serialisation, persistence).

Forgetting to Layer the Tests. A team builds a Clean codebase but writes only end-to-end tests against the Web ring, hitting Postgres via Testcontainers. The Domain and Use Case rings have no unit tests. The CI takes 25 minutes. Engineers stop running tests locally. The architecture’s testability promise is unfulfilled because the team has not exploited it. The fix is a deliberate test-pyramid audit: unit tests in Domain (most), unit tests in Use Cases (many, with hand-rolled gateway implementations), integration tests in Infrastructure (some, against real databases), end-to-end tests in Web (few).

The Framework-Coupled Inner Ring. The most insidious anti-pattern: an inner ring that has accidentally absorbed the framework. A Use Case class is annotated @Transactional (Spring); an Entity has @Entity (JPA); a boundary interface declares Mono<T> return types (Project Reactor / Spring WebFlux). The framework’s types now live inside the rings the architecture exists to protect. The anti-pattern is insidious because the symptoms are subtle: the code still compiles, the tests still pass, the architecture still looks Clean. But the inner rings are no longer framework-independent; swapping the framework now requires touching the rings; the architectural promise is silently broken. The fix is rigorous: ArchUnit rules that assert noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAPackage("org.springframework.."), executed in CI, failing the build on violations. Discipline alone is insufficient because the offending annotations are seductive (one annotation makes ten lines of glue disappear) and accumulate silently over time.

15. Open Questions

  • Where does Clean Architecture sit on the spectrum of “internal application architecture” vs “system architecture”? Most treatments are application-internal; how does it compose at the system level?
  • Is there a principled answer to “where does authorisation belong” — Entity ring or Use Case ring — or is it always team convention?
  • How does Clean Architecture interact with reactive/non-blocking I/O frameworks (Project Reactor, RxJava, Kotlin Flow)? Reactive types tend to leak into inner rings; is this a fundamental tension or just a discipline problem?
  • Is the one-class-per-use-case convention worth the boilerplate for trivial cases, or should the architecture explicitly authorise hybrid layouts?

16. See Also