Clean Architecture
Clean Architecture (Uncle Bob) organizes code in concentric rings — Entities, Use Cases, Interface Adapters, Frameworks — with the Dependency Rule: source code dependencies poin…
Introduction
Clean Architecture (Uncle Bob) organizes code in concentric rings — Entities, Use Cases, Interface Adapters, Frameworks — with the Dependency Rule: source code dependencies point inward only. Stripe's payment services use clean boundaries so PCI-scoped logic never imports Rails, HTTP, or SQL types.
Real production story
Stripe's Connect onboarding team inherited a Rails monolith where charge logic, webhook parsing, and ActiveRecord models lived in one directory. A PCI audit flagged that card metadata types appeared in 14 controllers. Remediation blocked all feature work for six weeks.
The rewrite applied Clean Architecture: Entities (Money, Charge) at center, use cases (CaptureCharge, RefundPayment) orchestrating, gateway interfaces (ChargeRepository, LedgerPort) in inner rings, Rails controllers and ActiveRecord in outermost ring. PCI scope shrank to the inner rings plus one adapter directory. New payment methods ship as new outer-ring adapters without touching entity invariants.
Business problem
Business pressure: Stripe processes billions in API calls; payment correctness and compliance are non-negotiable while API surface expands weekly.
- Revenue at risk: A framework leak causing double-capture bugs creates merchant churn and regulatory exposure.
- Engineering velocity: Teams fear touching charge logic because tests require full Rails stack — 20-minute feedback loops.
- Compliance / trust: PCI DSS requires demonstrable separation of payment logic from web framework code.
Architecture overview
Clean Architecture ring order (inside → out): Entities → Use Cases → Interface Adapters → Frameworks & Drivers. Cross-cutting concerns (logging, auth) enter via interfaces defined in inner rings.
- Definition: Dependency Rule — inner rings know nothing of outer rings.
- When to adopt: Regulated domains, long-lived business rules, multiple delivery channels (API, batch, webhooks).
- When to defer: Prototype with 6-week lifespan — rings add mapping cost.
- Operability: Use cases emit domain events; outer ring handles metrics and tracing.
Architecture motivation
Why architects care: Clean Architecture makes frameworks and databases "details" — swappable without rewriting business rules. Critical for regulated domains where audit scope follows code boundaries.
- Force: Long-lived domain invariants (money, idempotency) outlive every framework Stripe has used.
- Constraint: Cannot pause API evolution for multi-year rewrite — strangler inside clean rings.
- Outcome: Testable use cases in milliseconds; PCI boundary is a package graph, not a policy PDF.
Internal architecture
Stripe Connect clean rings — dependency arrows point inward:
┌─────────────────────────────────────────────────────────┐│ FRAMEWORKS & DRIVERS (outermost) ││ Rails controllers, ActiveRecord, Stripe API SDK, Sidekiq ││ ┌───────────────────────────────────────────────────┐ ││ │ INTERFACE ADAPTERS │ ││ │ Presenters, Gateways, Controllers (thin) │ ││ │ ┌─────────────────────────────────────────────┐ │ ││ │ │ USE CASES (application business rules) │ │ ││ │ │ CaptureCharge, CreateConnectAccount │ │ ││ │ │ ┌───────────────────────────────────────┐ │ │ ││ │ │ │ ENTITIES (enterprise business rules)│ │ │ ││ │ │ │ Charge, Money, IdempotencyKey │ │ │ ││ │ │ └───────────────────────────────────────┘ │ │ ││ │ └─────────────────────────────────────────────┘ │ ││ └───────────────────────────────────────────────────┘ │└──────────── ─────────────────────────────────────────────┘◀─── all dependencies point inward ───
Data flow
Primary path: API controller builds request model → interactor validates idempotency key → loads Charge entity via gateway → entity.applyCapture() → gateway persists → presenter formats API response.
- Write path: Use case coordinates entity mutation and outbound gateway calls; single transaction boundary in adapter.
- Read path: Query use cases return output DTOs via presenter — entities never serialized directly to JSON.
- Async path: Webhook adapter parses event → invokes use case with idempotency → entity state machine transitions.
System design diagram
Two diagrams show the Clean Architecture topology and the primary request/event path used in production at scale.
Production code example
Ruby clean architecture interactor — Stripe-style gateway injection:
# entities/charge.rb — innermost ringclass Chargedef initialize(id:, amount:, state:)@id = id@amount = amount@state = stateenddef capture!(at: Time.current)raise InvalidTransition unless @state == :authorized@state = :capturedDomainEvent.capture(id: @id, amount: @amount, at: at)endend# use_cases/capture_charge.rbclass CaptureChargedef initialize(charge_gateway:, ledger_gateway:, presenter:)@charges = charge_gateway@ledger = ledger_gateway@presenter = presenterenddef call(request)charge = @charges.find_by_idempotency_key(request.idempotency_key)charge.capture!(at: request.captured_at)@charges.save(charge)@ledger.post_capture(charge)@presenter.success(charge)rescue => e@presenter.error(e)endend# adapters/active_record_charge_gateway.rb — outer ring only
Enterprise case study
Stripe Connect onboarding refactor (2019): Clean Architecture rings reduced PCI audit scope 40% and cut charge logic test time from 18 min to 12 sec.
- Before: ActiveRecord in controllers; PCI findings on 14 endpoints.
- Decision: Four-ring package structure, interactors per API operation, gateway interfaces for all I/O.
- After: New payment method adapters ship without entity changes; zero PCI regressions in two audits.
Trade-offs
- Purity vs pragmatism: Strict rings mean many mapper classes — Stripe accepts cost for PCI and test speed.
- Interactor granularity: One interactor per use case vs generic command bus — prefer explicit interactors for auditability.
- Shared kernel: Money and IdempotencyKey entities shared across use cases — document as enterprise business rules ring.
Security considerations
Security is architectural: PCI CDE boundary drawn at entity + use case rings; outer rings handle tokenization only.
- Identity: Use cases receive authenticated MerchantContext value object — never raw request env.
- Data: PAN never enters entity ring; only token references cross gateway interfaces.
- Supply chain: Inner ring Gemfile has zero web dependencies — minimal audit surface.
Scalability analysis
Scale dimensions: Inner rings are in-process; scale happens at outer ring (stateless API replicas, sharded ledger adapters).
- Horizontal scale: Rails/API tier autoscales; use cases remain CPU-light if entities stay rich, not anemic.
- Hot spots: Gateway adapter to ledger DB — shard by merchant_id at adapter, not in entity.
- Cost: Mapper allocation on hot path — pool DTOs or use codegen for high-QPS endpoints.
Failure scenarios
What breaks: ActiveRecord models imported into entities ring; use cases return HTTP status codes.
- Framework creep:
include ActiveRecord::Basein entity — PCI scope explodes; ArchUnit fails required. - Interactor god-class: One interactor handles capture + refund + dispute — split by merchant-visible operation.
- Presenter bypass: Controller renders entity JSON — leaks internal fields to API.
Staff engineer insights
- Clean Architecture is not about circles on slides — it is the Dependency Rule enforced in build tooling.
- Stripe interview pattern: given a webhook handler, ask candidate to place each line in the correct ring — fast filter for senior vs staff.
- Entities should be rich — if interactors contain all IF statements, you built layered architecture with extra steps.
Interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionWhat is the Dependency Rule in Clean Architecture?+
Answer
Follow-up
2AdvancedQuestionWhere do DTOs live in Clean Architecture?+
Answer
Follow-up
3AdvancedQuestionHow would you migrate a Rails monolith to Clean Architecture incrementally?+
Answer
Follow-up
Architecture review questions
- Do entities and use cases have zero imports from frameworks (Rails, Spring, Express)?
- Is every external system accessed through a gateway interface defined inward?
- Are controllers/handlers thin — map DTO, call interactor, return presenter output?
- Are interactors one-per-use-case with explicit input/output boundaries?
- Does ArchUnit or equivalent fail CI when dependency rule is violated?
- Is PCI/regulatory scope documented as a specific ring boundary?
Summary
Clean Architecture at Stripe scale protects payment invariants inside entity and use case rings while Rails, ActiveRecord, and webhooks stay in outer adapters. Staff engineers enforce the Dependency Rule in CI, keep entities rich, and treat gateway interfaces as the contract surface for PCI and testing.