Skip to content

Clean Architecture Guide

How the AI Accountant follows Clean Architecture (Robert C. Martin) with Event Sourcing.

The Dependency Rule

The most important rule: source code dependencies point inward only.

Layer (outer → inner) Depends on Never depends on
Frameworks & Drivers Everything
Interface Adapters Use Cases, Entities Frameworks
Use Cases Entities, interfaces Adapters, Frameworks
Entities Nothing Everything else

Inner layers NEVER know about outer layers. Outer layers depend on inner layers, not the reverse.

## The Four Layers

### 1. Entities (Enterprise Business Rules)

The core domain. Pure business logic. No dependencies on anything external.

**Rules:**
- No I/O (no database, no HTTP, no file system)
- No framework dependencies
- Can be used by any application — not tied to web, CLI, or Discord
- Easily unit tested with plain data

**In our system:** `Engines/`

| Engine | What it knows | What it does NOT know |
|--------|--------------|----------------------|
| `ConfidenceEngine` | How to score a proposal against a pattern | Where patterns come from (DB? file? API?) |
| `RulesEngine` | Company policies (thresholds, VAT rates) | How policies are stored or loaded |
| `AmountEngine` | How to calculate VAT from gross/net | What currency or accounting system is used |

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

```csharp
// Pure function — no I/O, no dependencies
var engine = new ConfidenceEngine();
var score = engine.Score(
    new ProposalData("Adobe", 199, VatRate: 25),
    new MerchantPattern("Adobe", "6540", 25, 1.0f)
);
Assert.True(score.Score >= 0.9f);

2. Use Cases (Application Business Rules)

Application-specific business rules. Orchestrate the flow of data to and from Entities.

Rules: - May call Entities (Engines) - Defines interfaces for what it needs from the outside (e.g., IEventStore, IPatternStore) - Does NOT implement those interfaces — that's the outer layer's job - Owns the transaction boundary

In our system: Managers/

Manager Orchestrates Calls
EventManager Propose → Score → Route ConfidenceEngine, IEventStore, IPatternStore
ReviewManager Approve/Reject → Execute IEventStore, IFolioProvider, IFikenProvider

Key: Managers define interfaces (IEventStore, IPatternStore), they don't know about Postgres or HTTP.

// Manager uses interfaces — doesn't know about Postgres
public class EventManager
{
    private readonly IEventStore _eventStore;      // interface, not implementation
    private readonly IPatternStore _patternStore;  // interface, not implementation
    private readonly ConfidenceEngine _confidence; // entity, no interface needed

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

3. Interface Adapters

Convert data between the Use Cases and the outside world.

Rules: - Implement the interfaces defined by Use Cases - Convert external data formats to internal ones - No business logic — only data transformation and protocol handling

In our system: ResourceAccess/ (implements interfaces) + Clients/ (HTTP endpoints)

Component Adapts between
EventStore IEventStore ↔ PostgreSQL
PatternStore IPatternStore ↔ PostgreSQL
FolioProvider IAccountingProvider ↔ Folio REST API
FikenProvider IAccountingProvider ↔ Fiken REST API
HTTP Endpoints HTTP requests ↔ Manager method calls

4. Frameworks & Drivers

The outermost layer. Frameworks, tools, database drivers, web servers.

Rules: - Glue code only — no business logic - Where you configure DI, wire up routes, start the server - The most volatile layer — frameworks change, business rules don't

In our system: Program.cs (ASP.NET Core setup, DI registration, route mapping)

How It Maps to Our System

Clean Architecture Layers

The Key Insight: Dependency Inversion

The EventManager needs to store events. But it doesn't know about Postgres.

Wrong (dependency points outward)

EventManager → EventStore → Postgres

Right (dependency points inward)

EventManager → IEventStore (interface, defined by Manager)

EventStore implements IEventStore (outer layer implements inner layer's interface)

The Manager defines what it needs (IEventStore). The outer layer provides it (EventStore backed by Postgres). If we swap Postgres for MongoDB tomorrow, the Manager doesn't change.

Event Sourcing in Clean Architecture

Event sourcing fits naturally: events are the lingua franca between layers.

Book-E → HTTP Endpoint → EventManager → ConfidenceEngine (pure logic)
                                      → IEventStore.AppendAsync (interface)
                                                ↓
                                      EventStore (Postgres implementation)
                                                ↓
                                      events table (immutable, append-only)

Every action is an event. State is derived from events. The Engines never touch the event store — they receive data and return decisions.

Project Structure

src/AccountingApi/
├── Program.cs                    ← Frameworks & Drivers (DI, routes, server)
│
├── Engines/                      ← Entities (innermost, no dependencies)
│   ├── ConfidenceEngine.cs
│   ├── RulesEngine.cs
│   └── AmountEngine.cs
│
├── Managers/                     ← Use Cases (orchestration via interfaces)
│   ├── EventManager.cs
│   └── ReviewManager.cs
│
├── ResourceAccess/               ← Interface Adapters (implement interfaces)
│   ├── IAccountingProvider.cs    ← interface (defined for Use Cases)
│   ├── FolioProvider.cs          ← implements IAccountingProvider
│   ├── FikenProvider.cs          ← implements IAccountingProvider
│   ├── EventStore.cs             ← implements IEventStore
│   ├── PatternStore.cs           ← implements IPatternStore
│   └── AccountingProviderFactory.cs
│
└── appsettings.json              ← Configuration (Frameworks & Drivers)

src/AccountingApi.Tests/
├── Engines/                      ← Test Entities with plain data (no mocks)
│   ├── ConfidenceEngineTests.cs
│   ├── RulesEngineTests.cs
│   └── AmountEngineTests.cs
└── ...

What Each Layer Can Import

Layer Can use Cannot use
Engines Other Engines, standard library Managers, ResourceAccess, frameworks
Managers Engines, interfaces (IEventStore, etc.) ResourceAccess implementations, frameworks
ResourceAccess Interfaces it implements, external SDKs Engines, Managers
Program.cs Everything (wires it all together)

Testing Strategy

Layer Test type Mocks needed
Engines Unit tests None — pure data in, data out
Managers Unit tests Mock interfaces (IEventStore, IPatternStore)
ResourceAccess Integration tests Real Postgres / real API (or testcontainers)
Endpoints API tests Full stack with test database

Comparison with Other Architectures

Concept Clean Architecture Hexagonal (Ports & Adapters) iDesign DDD
Inner layer Entities Domain Engines Aggregates
Orchestration Use Cases Application Services Managers Application Layer
External I/O Gateways Adapters Resource Access Infrastructure
Entry points Controllers Ports Clients
Key rule Dependencies inward Ports define contracts No layer skipping Ubiquitous language

They're all variations of the same idea: protect the core business logic from external concerns.