Skip to content

4. Solution Strategy

Six architectural moves define Finnest. Each trades complexity against predictability; each is ratified by an ADR.

1. Supervised Modular Monolith

What: 21 independently-supervised OTP applications in one Elixir release, sharing one PostgreSQL cluster.

Why: 90 % of microservice benefits (independent failure domains, independent supervision, independent scaling via process allocation) for 10 % of microservice operational cost (no Kubernetes, no service mesh, no distributed tracing assembly, no saga orchestration).

Ratified: ADR-002-F, origin idea: brainstorm-02.

2. Three-tier AI agent architecture

What: Every agent is classified as conversational (sync, chat), workflow (async, multi-step), or autonomous (long-running, budgeted). Each tier has distinct process lifetime, memory, and tool authorisation.

Why: Prevents a runaway autonomous agent from blocking a chat reply; makes per-tier cost ceilings enforceable; isolates failure domains.

Ratified: ADR-003-F, origin idea: brainstorm-03.

3. MCP at every domain boundary

What: Every one of the 21 domains publishes its capabilities as typed tools via Model Context Protocol. AI agents and the UI (Cmd+K command bar) both consume via the same MCP interface.

Why: One mental model for "how do I invoke X?" whether you're an agent or a human. Domain extraction into a separate service later is a config change, not a rewrite.

Ratified: ADR-004-F.

4. Events-only cross-domain (one documented exception)

What: Domains emit events to the shared event store; downstream domains subscribe. Exactly one synchronous exception: Compliance.check/2, called before write operations in five domains (roster, timekeep, payroll, clients, assets).

Why: Event sourcing satisfies both cross-domain communication and IRAP audit trail from the same substrate. The one synchronous exception is necessary for the compliance auto-blocking constraint.

Ratified: ADR-005-F, reinforcement: ADR-011-F.

5. Hexagonal ports for every external integration

What: Every outbound API sits behind a behaviour module with at least two planned adapters. AI provider, award engine, file storage, secrets, communications, accounting, government APIs, document verification.

Why: Vendor replaceability is an architectural driver (D11). Testing against a stub adapter is trivial when the port is a behaviour.

Ratified: ADR-006-F.

6. Single release, dual deployment

What: Commercial SaaS (Anthropic Direct) and IRAP-certified (AWS Bedrock Sydney) deployments are produced from the same Elixir release via runtime configuration. A small Go proxy (~500 LOC) sits at the IRAP deployment boundary.

Why: Two codebases would double maintenance cost; conditional compilation would poison every module with when :irap branches. Runtime config + edge proxy is the only clean path.

Ratified: ADR-007-F, deep dive: architecture/irap.md.

How the moves compose

The six moves compose into a pattern sometimes called the "boring-core, sharp-edges" pattern:

  • Boring core: one language (Elixir), one database (Postgres 17), one framework (Phoenix), one job system (Oban). No Redis, no Kafka, no Elasticsearch. Postgres does cache, pub/sub, queue, event store, and search.
  • Sharp edges: aggressive isolation at domain boundaries (OTP apps, MCP, events), aggressive enforcement at integration boundaries (hexagonal ports, Compliance gate, IRAP Go proxy).

The trade-off is deliberate: operational simplicity inside, architectural discipline outside. See architecture/architecture.md for the complete synthesis.