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 configDockerfile— container buildNpgsql,HttpClient— drivers
How It Maps
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).