Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 22

    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…

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    text
    ┌─────────────────────────────────────────────────────────┐
    │ 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.

    Clean Architecture — system view
    Controllers
    Edge
    Use cases
    Core
    Entities
    Data
    DB / API drivers
    Async
    High-level topology for Clean Architecture.
    Clean Architecture — request / event flow
    Request DTO
    Ingress
    Interactor
    Store
    Entity rule
    Store
    Presenter
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Ruby clean architecture interactor — Stripe-style gateway injection:

    ruby
    # entities/charge.rb — innermost ring
    class Charge
    def initialize(id:, amount:, state:)
    @id = id
    @amount = amount
    @state = state
    end
    def capture!(at: Time.current)
    raise InvalidTransition unless @state == :authorized
    @state = :captured
    DomainEvent.capture(id: @id, amount: @amount, at: at)
    end
    end
    # use_cases/capture_charge.rb
    class CaptureCharge
    def initialize(charge_gateway:, ledger_gateway:, presenter:)
    @charges = charge_gateway
    @ledger = ledger_gateway
    @presenter = presenter
    end
    def 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)
    end
    end
    # 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::Base in 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.

    3 questions
    1AdvancedQuestionWhat is the Dependency Rule in Clean Architecture?+

    Answer

    Source code dependencies may only point inward. Outer rings (frameworks, UI, DB) depend on inner rings (use cases, entities). Inner rings never import outer. This is enforced by package structure and ArchUnit, not convention.

    Follow-up

    How do you call a database from an entity?
    2AdvancedQuestionWhere do DTOs live in Clean Architecture?+

    Answer

    Request/response DTOs belong in interface adapters or use case boundaries — never in entities. Entities use domain types (Money, not BigDecimal strings). Presenters map entities to API DTOs at the outer edge.

    Follow-up

    How does this differ from anemic domain models in layered architecture?
    3AdvancedQuestionHow would you migrate a Rails monolith to Clean Architecture incrementally?+

    Answer

    Strangler: extract one use case at a time into interactor + entity, wrap existing ActiveRecord behind gateway adapter, keep controller thin. Repeat per API endpoint. Do not big-bang move models — PCI and regression risk too high.

    Follow-up

    Which use case would you extract first at Stripe?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.