Skip to content

Clean Architecture Guide

How each service follows Clean Architecture (Robert C. Martin). Between services, communication is event-driven.

The Dependency Rule

Source code dependencies point inward only.

Layer (outer → inner) Depends on Never depends on
Frameworks & Drivers (Program.cs, Dockerfile) Everything
Adapters (Gateways, HTTP endpoints) Use Cases, Domain Frameworks
Use Cases (ProposeUseCase, ReviewUseCase) Domain, interfaces Adapter implementations
Domain (ConfidenceScorer, PolicyValidator) Nothing Everything else

The Four Layers

1. Domain (Entities)

Pure business logic. No dependencies. No I/O.

Component Service Responsibility
ConfidenceScorer Event Store API Score proposals against known patterns
PolicyValidator Event Store API Company policy validation (thresholds, limits)
AmountCalculator Accounting API VAT calculations (net/gross/VAT split)
CostCalculator Cost API Aggregate spending, currency conversion, burn rate

Test: Pass in data, get a result. No mocks needed.

var scorer = new ConfidenceScorer();
var score = scorer.Score(
    new ProposalData("Adobe", 199, VatRate: 25),
    new MerchantPattern("Adobe", "6540", 25, 1.0f)
);
Assert.True(score.Score >= 0.9f);

2. Use Cases

Application-specific workflows. Call Domain for logic. Define interfaces for I/O.

Use Case Service Orchestrates
ProposeUseCase Event Store API Propose → Score → Store → Route to review
ReviewUseCase Event Store API Approve/Reject → Store → Dispatch to executor

Key: Use Cases define interfaces (IEventStore, IPatternStore). They don't know about Postgres or HTTP.

public class ProposeUseCase
{
    private readonly IEventStore _events;        // interface, not implementation
    private readonly IPatternStore _patterns;    // interface, not implementation
    private readonly ConfidenceScorer _scorer;   // domain, no interface needed

    public async Task<ProposalResult> ProposeAsync(...)
    {
        var pattern = await _patterns.GetAsync(merchant);  // I/O via interface
        var score = _scorer.Score(proposal, pattern);       // pure domain logic
        var evt = await _events.AppendAsync(...);           // I/O via interface
        return result;
    }
}

3. Adapters (Interface Adapters)

Implement the interfaces defined by Use Cases. Bridge internal and external.

Adapter Service Implements Talks to
EventStoreAdapter Event Store API IEventStore PostgreSQL
PatternStoreAdapter Event Store API IPatternStore PostgreSQL
AccountingGateway Event Store API IAccountingGateway Accounting API (HTTP)
CostGateway Event Store API ICostGateway Cost API (HTTP)
FolioGateway Accounting API IFolioGateway Folio API v2
FikenGateway Accounting API IFikenGateway Fiken API v2
AnthropicGateway Cost API Anthropic API
CloudflareGateway Cost API Cloudflare API
Controllers All HTTP endpoints → Use Cases

4. Frameworks & Drivers

Outermost layer. Glue code only.

  • Program.cs — DI registration, route mapping, server config
  • Dockerfile — container build
  • Npgsql, HttpClient — drivers

How It Maps

Clean Architecture Layers

Dependency Inversion

Wrong

ProposeUseCase → EventStoreAdapter → Postgres

Use Case depends on a concrete implementation.

Right

ProposeUseCase → IEventStore (interface, defined by Use Case)

EventStoreAdapter implements IEventStore (Adapter implements the interface)

Swap Postgres for MongoDB? Change the Adapter. Use Case doesn't change.

Event Sourcing Fits Naturally

Events flow through the layers:

Book-E → Controller → ProposeUseCase → ConfidenceScorer (domain)
                                     → IEventStore.AppendAsync (interface)
                                              ↓
                                     EventStoreAdapter (Postgres)
                                              ↓
                                     events table (immutable)

Project Structure (Target)

src/EventStoreApi/
├── Program.cs              ← Frameworks & Drivers
├── Domain/                 ← Entities (innermost)
│   ├── ConfidenceScorer.cs
│   └── PolicyValidator.cs
├── UseCases/               ← Use Cases
│   ├── ProposeUseCase.cs
│   └── ReviewUseCase.cs
└── Adapters/               ← Interface Adapters
    ├── EventStoreAdapter.cs
    ├── PatternStoreAdapter.cs
    ├── AccountingGateway.cs
    └── CostGateway.cs

src/AccountingApi/
├── Program.cs
├── Domain/
│   └── AmountCalculator.cs
└── Adapters/
    ├── FolioGateway.cs
    └── FikenGateway.cs

src/CostApi/
├── Program.cs
├── Domain/
│   └── CostCalculator.cs
└── Adapters/
    ├── AnthropicGateway.cs
    ├── GoogleGateway.cs
    └── CloudflareGateway.cs

Import Rules

Layer Can use Cannot use
Domain Other Domain classes, standard library Use Cases, Adapters, Frameworks
Use Cases Domain, interfaces Adapter implementations, Frameworks
Adapters Interfaces they implement, external SDKs Domain directly, other Adapters
Program.cs Everything (wires DI)

Testing Strategy

Layer Test type Mocks needed
Domain Unit tests None — pure data in, data out
Use Cases Unit tests Mock interfaces (IEventStore, etc.)
Adapters Integration tests Real Postgres / real API
Controllers API tests Full stack with test database

Between Services: Event-Driven

Clean Architecture is for inside each service. Between services, communication is event-driven through the Event Store API:

graph LR
    B[Book-E] -->|events| ES[Event Store API]
    R[Review-E] -->|events| ES
    ES -->|HTTP| A[Accounting API]
    ES -->|HTTP| C[Cost API]

No service calls another service directly for writes. Read-only queries can use direct HTTP (e.g., Book-E → Accounting API for balance).