Clean Architecture: Make Policy Independent of Details
Reason about Clean Architecture through the Dependency Rule, core business policy, application use cases, interface adapters, frameworks and drivers, boundary data, dependency inversion, composition roots, and testing trade-offs.
Personal learning atlas by Tran Trong Thuc · About this Atlas · Atlas last updated Sep 22, 2026
Clean Architecture: Make Policy Independent of Details
A development team inherits a straightforward user management and onboarding service. Eager to follow "industry best practices," the previous engineers cargo-culted the classic four concentric circles of Clean Architecture down to the letter. Every routine database query was forced through 24 layers of boilerplate: UserController, UserPresenter, CreateUserUseCaseInputPort, CreateUserUseCaseOutputPort, CreateUserInteractor, UserRepositoryInterface, PostgresUserRepository, five distinct DTOs, and tedious bidirectional object mappers. Adding a single nullable column like phone_number required modifying 14 files across 6 directory tiers. Codebase volume tripled, deployment velocity ground to a halt, and time-to-market collapsed—all while integration tests mocked the database away, offering zero real testability or safety against production runtime failures.
This architectural paralysis stems from confusing structural ceremony with architectural intent. Clean Architecture is neither a mandatory folder layout nor a quota of interfaces. Its foundational essence is the Dependency Rule: source-code dependencies must point strictly inward toward high-level business policy, keeping domain rules completely decoupled from databases, web frameworks, external SDKs, and UI mechanisms.
💡 Rule of thumb: Protect stable business policy from volatile mechanisms; source-code dependencies must point strictly inward toward domain rules, crossing boundaries solely through abstractions owned by the inner layer.
TL;DR
- The Dependency Rule is absolute: Source-code dependencies must point strictly inward toward higher-level policies. Inner layers (
EntitiesandUse Cases) must never know, import, or name concrete types from outer mechanisms (Interface Adapters,Frameworks and Drivers). - Concentric rings represent stability gradients: Outermost rings house volatile mechanisms and delivery details (HTTP frameworks, ORMs, message brokers); innermost rings house durable business policies (
Entitiesrepresenting enterprise rules andUse Casesorchestrating application workflows). - Boundary data crossing without leakage: Data crosses boundaries using plain, intent-focused structures (
boundary data, request/response models, or pure DTOs). Never smuggle framework requests, database cursors, or ORM entities across the boundary into core business logic. - Inversion of Control and the Composition Root: Outer adapters implement abstractions owned by inner layers via Dependency Inversion. All concrete implementations and third-party drivers are wired together in an intentionally impure, single location at startup: the composition root.
- Fatal pitfall: Architecture ceremony without volatility (Cargo-culting the circles). Mechanically creating input ports, output ports, interactor classes, repository interfaces, presenters, and mappers for trivial CRUD workflows that have no independent business policy or varying infrastructure. This bloats code by 300% and slows delivery without delivering real testability or decoupling benefits.
The governing relationship between layers is:
source-code dependencies point inward
Frameworks and Drivers
-> Interface Adapters
-> Use Cases (Application-specific policy)
-> Entities (Core business policy)Runtime control can move outward when a use case calls a database, presenter, queue, or payment provider. But source code in an inner policy layer should depend on an abstraction it owns, while the outer detail implements that abstraction.
The circles in common diagrams are schematic. Clean Architecture does not require exactly four projects, exactly four folders, or an enterprise-style class hierarchy. The architectural value comes from the Dependency Rule, not from copying a diagram.
1. The Dependency Rule is the governing constraint
This is primarily a statement about source-code or compile-time dependency.
Suppose a use case needs to save an order.
Bad compile-time dependency:
PlaceOrderUseCase -> PrismaOrderRepositoryBetter dependency direction:
PlaceOrderUseCase -> OrderRepository abstraction
PostgresOrderRepository -> OrderRepository abstractionAt runtime, the call still reaches PostgreSQL:
PlaceOrderUseCase
-> OrderRepository interface
-> PostgresOrderRepository
-> PostgreSQLSo runtime flow and source-code dependency direction are not the same thing.
2. Inner means policy; outer means mechanism
Clean Architecture often uses concentric rings because the rings communicate a gradient:
- outer areas contain mechanisms and details;
- inner areas contain policies and business meaning;
- moving inward should generally increase abstraction and stability.
Typical labels are:
- Entities: core enterprise business rules;
- Use Cases: application-specific business policy;
- Interface Adapters: data translators and boundary bridges;
- Frameworks and Drivers: peripheral tools, databases, and delivery mechanisms.
Those names are useful, but the exact count is not sacred.
The stronger question is:
If a detail changes, how far inward does that change propagate?
If upgrading an ORM changes your order-discount rule, the dependency boundary is weak no matter how beautiful the folder tree looks.
3. “Entities” means high-level business policy, not ORM rows
A major naming trap is assuming Clean Architecture's Entities means "database entities."
That is not the architectural idea.
In the Clean Architecture model, the innermost policy represents the most general, high-level business rules available in the system. In a large enterprise, some rules may be shared across applications. In a single application, they may simply be the application's most durable business objects and policies.
Examples:
Order cannot be paid twice.
Refund cannot exceed captured amount.
Subscription cannot renew after cancellation becomes effective.
Inventory reservation cannot become negative.An ORM annotation, SQL row, HTTP request, or JSON schema should not define those rules merely because it is convenient.
Bad boundary:
class OrderPolicy {
canCancel(order: Prisma.OrderGetPayload<...>): boolean
}Now a persistence representation has entered core policy.
Prefer a core model or input shape whose meaning belongs to the policy itself.
4. Use cases contain application-specific policy
Examples:
- Place Order;
- Cancel Subscription;
- Approve Refund;
- Publish Article;
- Reconcile Payment;
- Generate Monthly Statement.
A use case can decide:
load order
-> verify cancellation policy
-> record cancellation
-> save order
-> publish outcomeBut it should not need to know:
Express Request
Prisma TransactionClient
Stripe SDK response type
Kafka producer config
React component stateThose are mechanisms around the use case.
5. Interface adapters translate representations
An HTTP adapter may translate:
HTTP JSON request
-> PlaceOrderRequestModel
-> PlaceOrder use caseA presenter may translate:
PlaceOrderResult
-> HTTP response model
-> JSON + status codeA persistence adapter may translate:
Order aggregate
-> ORM persistence model
-> database rowsThe goal is not to create a duplicate DTO for every single function call. Map data when the representations have different owners or reasons to change.
6. Frameworks and drivers are details from the core's perspective
The outermost boundary commonly contains things such as:
- web frameworks;
- database engines and ORM configuration;
- message brokers;
- UI frameworks;
- vendor SDKs;
- schedulers;
- device drivers;
- cloud service clients.
Calling them "details" does not mean they are trivial or operationally unimportant.
PostgreSQL transaction semantics still matter. Payment-provider idempotency still matters. Kafka ordering still matters.
"Detail" means the inner business policy should not be structurally owned by those implementation choices.
You still need infrastructure expertise, production observability, integration tests, migrations, retry policies, and operational runbooks.
7. Dependency inversion crosses boundaries without reversing runtime behavior
Suppose a use case must notify a presenter when checkout succeeds.
A direct source dependency would be:
CheckoutUseCase -> HttpCheckoutPresenterThat points outward and violates the Dependency Rule.
Instead, the use-case boundary can own an output abstraction:
CheckoutUseCase -> CheckoutOutput
HttpCheckoutPresenter -> CheckoutOutputRuntime control can still be:
CheckoutUseCase
-> CheckoutOutput method
-> HttpCheckoutPresenter implementationThis is dependency inversion: source dependency points toward the inner-owned abstraction even though runtime execution eventually reaches the outer implementation.
The same technique applies to:
- repositories;
- gateways;
- presenters;
- message publishers;
- clocks;
- object stores;
- external APIs.
8. Dependency Injection is not Clean Architecture
Dependency Injection can help wire implementations to abstractions, but it is not the architecture by itself.
This code uses DI:
new OrderService(prisma, stripe, expressResponseFactory)Yet OrderService still depends directly on infrastructure concepts.
This is closer to the dependency rule:
new PlaceOrderUseCase(orderRepository, paymentGateway, outputBoundary)where those contracts are owned by the application boundary and implemented outside it.
A DI container can wire a poor architecture. Constructor injection can wire a poor architecture. The important question is who owns the abstraction and which way source dependencies point.
9. The composition root is intentionally impure
At startup, some outer code must know concrete classes so the application can run.
For example:
const orderRepository = new PostgresOrderRepository(prisma);
const paymentGateway = new StripePaymentGateway(stripe);
const presenter = new HttpCheckoutPresenter();
const placeOrder = new PlaceOrderUseCase({
orderRepository,
paymentGateway,
presenter,
});That startup location is the composition root.
It is acceptable for this outer edge to know concrete infrastructure because its job is to assemble the graph.
What should be avoided is letting inner use cases reach into a global container or import concrete infrastructure themselves.
10. Boundary data should not smuggle outer dependencies inward
Robert Martin's original article emphasizes simple data structures crossing boundaries.
The practical rule is:
inner code should not be forced to depend on an outer representation merely because that representation is convenient.
Avoid passing these inward when they leak outer ownership:
Express Request
ORM RowStructure
Framework ModelState
Vendor SDK Response
Database cursor
HTTP-specific status enumInstead, translate to plain boundary data shaped for the inner use case:
type ApproveRefundInput = {
paymentId: PaymentId;
amount: Money;
requestedBy: ActorId;
};Likewise, a use case can produce an output model that a presenter converts to HTTP, CLI, or another delivery representation.
Do not turn this into mapper theater. If two layers genuinely share the same stable value object and doing so does not reverse dependency ownership, duplication may be unnecessary.
11. Clean Architecture and Hexagonal Architecture overlap
They are not competing religions.
Both aim to protect application policy from external technology and invert dependencies at meaningful boundaries.
Useful difference in emphasis:
Hexagonal Architecture
-> ports/adapters
-> inside vs outside
-> driving vs driven interactions
Clean Architecture
-> policy vs detail
-> concentric levels of abstraction
-> Dependency Rule: source dependencies inwardA system may be both.
For example:
HTTP adapter
-> use-case input boundary
-> application use case
-> core policy
-> repository/payment output boundary
-> infrastructure adaptersThe diagram vocabulary matters less than whether dependency ownership is correct.
12. Clean Architecture does not require DDD
Domain-Driven Design can complement Clean Architecture, but it is not a prerequisite.
You can have:
- Clean Architecture without aggregates or bounded contexts;
- DDD concepts in a layered architecture;
- a modular monolith using Clean-style dependency inversion;
- a microservice with terrible dependency direction inside the service.
Choose boundaries because they protect real policy and volatility, not because a methodology checklist demands more artifacts.
13. Testing follows the dependency boundaries
A clean dependency structure creates useful test seams.
Typical balance:
Core policy tests
-> pure / fast / infrastructure-free
Use-case tests
-> fake or controlled output dependencies
Adapter integration tests
-> real DB / broker / provider contract where practical
Composition / end-to-end tests
-> verify selected assembled flowsClean Architecture does not remove the need for integration testing.
A fake repository cannot prove:
- database constraints;
- isolation behavior;
- SQL mapping correctness;
- provider timeout behavior;
- broker delivery semantics.
Keep unit test suites for policy fast and isolated, and test details with integration tests where their real semantics live.
14. Avoid architecture ceremony without volatility
A tiny CRUD application may not benefit from:
Controller
-> InputBoundary
-> Interactor
-> OutputBoundary
-> Presenter
-> Gateway
-> Mapperfor every trivial operation.
The Dependency Rule is valuable, but the number of abstractions should match the cost of change.
Introduce stronger boundaries when:
- core policy is valuable and long-lived;
- infrastructure is volatile;
- multiple delivery mechanisms exist;
- testing core policy currently requires heavy infrastructure;
- framework/vendor types leak widely;
- changes to details repeatedly ripple into business behavior.
Architecture is an economic decision, not a contest for maximum interfaces.
15. Production failure: the ORM becomes the business model
A high-growth SaaS subscription platform started with framework-generated ORM models for rapid prototyping. Eager to ship features quickly, the engineering team passed the ORM Subscription entity directly across all layers:
HTTP controller
-> Subscription ORM entity
-> RenewalService
-> BillingService
-> NotificationService
-> API response serializerOver eighteen months, the entity accumulated dozens of database annotations, lazy-loaded relations, validation decorators, Stripe customer IDs, and UI formatting getters.
When a major billing redesign required migrating subscription states from simple database enums to an event-sourced ledger, the change triggered a catastrophic ripple effect. Changing column definitions broke the renewal business rules, caused serialization errors in mobile API endpoints, invalidated billing audit logic, and caused hundreds of unit tests to fail because they could not initialize without a running PostgreSQL container.
Impact: Database schema migrations triggered cascading rewrites across the entire application. Unit tests became impossible to run without heavy database fixtures, delivery and persistence concerns shaped core policy, and replacing one adapter became a business-logic change.
Root cause: An outer persistence representation (the ORM model) was treated as the shared business contract. Source-code dependencies pointed outward from policy toward database mechanisms, directly violating the Dependency Rule.
Correct pattern: Move durable subscription invariants into innermost Entities, express application operations as Use Cases, define repository and payment gateway abstractions owned by the application layer, translate database rows and HTTP DTOs in Interface Adapters, keep Frameworks and Drivers strictly outside the policy boundary, and wire concrete details only at the composition root.
16. Self-check
1. Does runtime control flowing to a database adapter violate the Dependency Rule?
When a use case calls a repository method that ultimately executes queries inside a PostgreSQL adapter, does that outward execution flow violate the Dependency Rule?
Show the reasoning
Not necessarily. The Dependency Rule governs source-code and compile-time dependencies, not runtime call stack execution. The use case depends on an abstraction (an interface or port) that it owns within its boundary. The database adapter in the outer layer implements that abstraction. At runtime, execution flows outward from the use case into the adapter, but at compile time, the source code dependency points inward toward the abstraction.
2. Does Clean Architecture require exactly four concentric layers?
Must every application build four separate projects or folders named Entities, UseCases, Adapters, and Frameworks?
Show the reasoning
No. The four concentric circles in Uncle Bob's famous diagram are purely schematic. Depending on domain complexity and operational requirements, an application might need three layers, five layers, or simply clear module boundaries within a single package. What must remain invariant is the Dependency Rule: source-code dependencies must always point inward toward high-level business policy, regardless of the number of intermediate rings.
3. Is a database class named CustomerEntity an Entity in Clean Architecture?
If your ORM or persistence framework defines a data model called CustomerEntity, does that automatically place it in the innermost layer of Clean Architecture?
Show the reasoning
No. This is a classic terminology collision. In ORMs (such as TypeORM, Hibernate, or Prisma), an "entity" is a database persistence representation tied to table schemas and column types. In Clean Architecture, an Entity represents durable enterprise-wide business rules and invariants that are completely decoupled from persistence details. An ORM model belongs in the outer Interface Adapters or Frameworks layer, not in the core domain.
17. Production checklist
- Dependency direction: Source-code dependencies point strictly inward toward stable business policy rather than outward toward frameworks or infrastructure.
- Core isolation: Core business rules (Entities and Use Cases) do not import ORM packages, web frameworks, vendor SDKs, or transport types.
- Use case focus: Use cases express application operations independently of delivery channels (HTTP, CLI, queues) and persistence mechanisms.
- Boundary ownership: External capabilities cross boundaries through abstractions whose signatures and semantics are owned by the inner layer.
- Interface adapters: Adapters handle translation between external formats and internal domain structures at boundaries with different reasons to change.
- Boundary data discipline: Plain data structures (boundary data, DTOs, request/response models) cross boundaries without leaking database rows, cursors, or framework request objects.
- Wiring discipline: Dependency Injection is used purely as an assembly mechanism, not treated as proof that architectural dependencies are inverted correctly.
- Composition root: All concrete infrastructure dependencies, adapters, and use cases are assembled in an explicit, centralized composition root at startup.
- Fast unit tests: Core business rules and use cases execute in fast, in-memory unit test suites without requiring live databases, web servers, or third-party emulators.
- Adapter integration tests: Real database queries, broker contracts, and third-party integrations are verified with targeted integration tests rather than mocked away entirely.
- Architectural economy: The team can justify every abstraction and boundary by real volatility and change cost, avoiding cargo-cult ceremony for simple CRUD operations.
Agent rule
When recommending Clean Architecture, identify the policy that deserves protection, the outer detail likely to vary, the boundary abstraction owned by the inner side, and the source-code dependency direction. Do not recommend extra rings, DTOs, repositories, or interfaces without a concrete change-isolation benefit.
Sources
- Robert C. Martin, The Clean Architecture
- Microsoft Learn, Common web application architectures
- Microsoft Learn, Architectural principles
Hexagonal Architecture: Put Technology Outside the ApplicationNew
Reason about ports and adapters, driving and driven interactions, dependency inversion, test isolation, composition roots, and when Hexagonal Architecture improves changeability without adding ceremonial interfaces.
Monolith Architecture: One Deployment Boundary, Many Possible DesignsNew
Reason about monolithic architecture through deployment boundaries, in-process collaboration, shared transactions, scaling, release coupling, failure blast radius, internal modularity, team ownership, and decomposition signals.