Architecture Principles
Rules that guide every design decision. When in doubt, refer here.
1. Dependencies Point Inward
The most important rule from Clean Architecture. Inner layers never know about outer layers.
Entities (Engines) — knows nothing about the outside world
↑ used by
Use Cases (Managers) — knows Entities + interfaces for I/O
↑ used by
Adapters (ResourceAccess) — knows Use Cases + external systems
↑ used by
Frameworks (Program.cs) — wires everything together
Test: Can you delete the database and the Engines still compile? Yes → correct.
2. Services Communicate Through Events, Not Direct Calls
Between services, use the event store. Not HTTP calls.
❌ Book-E calls Review-E directly
✅ Book-E writes event → Review-E reads event
Why: - No coupling between services - Services can be offline, restarted, or replaced without breaking others - Natural audit trail - Easy to add new services that react to existing events
3. Every Write Action Is an Event
Nothing happens without an event. No direct database writes, no direct API calls.
PROPOSED → APPROVED → EXECUTED
Why: - Full audit trail (bokføringsloven) - Review before execution - Replay capability - Undo via compensating events
4. Engines Have No I/O
Engines are pure functions. Data in, result out. No database, no HTTP, no file system.
// ✅ Correct: pure logic
var score = confidenceEngine.Score(proposal, pattern);
// ❌ Wrong: Engine fetches its own data
var score = confidenceEngine.ScoreFromDatabase(merchantName);
Why: - Unit testable without mocks - Portable — works in any context - No hidden dependencies - Easy to reason about
5. Secret Isolation
No service has secrets it doesn't need.
| Service | Has | Does NOT have |
|---|---|---|
| Book-E | Discord token, LLM key | Folio/Fiken keys, DB credentials |
| Accounting API | Folio/Fiken keys, DB credentials | Discord token, LLM key |
Why: - Compromising one service doesn't expose the other's secrets - Principle of least privilege
6. Everything Is GitOps
No manual kubectl commands. No SSH to apply changes.
Code change → PR → Review → Merge → CI builds image →
CI updates manifest with SHA → ArgoCD syncs → New pod running
Why: - Reproducible deployments - Audit trail in git - Rollback = revert a commit - No snowflake configurations
7. Agents Propose, They Don't Execute
AI agents are not trusted to execute financial operations directly.
Book-E → PROPOSE (with confidence score)
→ High confidence (known pattern): auto-approve
→ Low confidence (new merchant): Review-E or human approves
→ On approval: system executes
Why: - AI can hallucinate amounts, accounts, VAT rates - Financial errors have real consequences - Trust is built over time (patterns → auto-approve) - Human stays in the loop for new situations
8. One External System Per Resource Access Component
Each external system gets exactly one ResourceAccess component.
FolioProvider ← only thing that talks to Folio API
FikenProvider ← only thing that talks to Fiken API
EventStore ← only thing that talks to events table
PatternStore ← only thing that talks to patterns table
Why: - Change the API? Change one file. - Swap Folio for another bank? Replace one component. - Rate limiting, retries, auth — all in one place.
9. Start Simple, Evolve
Don't build for scale you don't have. Build for the principles, not the infrastructure.
Phase 1: Single pod, in-memory, :latest tags
Phase 2: Postgres, SHA tags, ArgoCD
Phase 3: Event sourcing, review process
Phase 4: Gateway+worker split, KEDA scaling
Each phase adds capability without breaking the previous one. The Clean Architecture layers stay the same.
10. Documentation Lives With Code
Architecture docs are in the repo, deployed automatically, versioned with git.
- Not in Confluence, Notion, or someone's head
- Updated in the same PR as the code change
- Deployed to Cloudflare Pages on merge
- C4 diagrams generated from PlantUML source