Skip to content

ADR-002-F: Supervised Modular Monolith (21 OTP Applications)

Status: Accepted Date: 2026-04-16 Decision Makers: Gautham Chellappa Depends on: ADR-001-F (Elixir/Phoenix)

Context

Finnest must serve 19 modules (~365 features) for a 3-person team. The team-size tax of microservices (Kubernetes + service mesh + distributed tracing + saga orchestration) is prohibitive. Yet a traditional layered monolith violates D7 (atomic feature isolation) — a bug in payroll could crash the entire BEAM node and take timekeep and roster with it.

BEAM's OTP runtime offers a third pattern: independently-supervised applications within a single release. Each has its own failure domain, supervision tree, and state; one app's crash doesn't affect another. The pattern was coined in brainstorm-02 as Supervised Modular Monolith.

Decision

Finnest is structured as a single Elixir umbrella project containing 21 OTP applications:

  • 19 domain apps (Tier 1 through Tier 5) — each with its own schema, context, MCP server, Oban queue, LiveView namespace, supervision tree
  • 2 infrastructure appsfinnest_core (foundation) and finnest_agents (orchestrator + MCP registry + AI provider)
  • Plus 1 edge appfinnest_web (Phoenix endpoint) — for a total of 21 OTP apps + 1 web edge

Cross-domain communication is events-only (AR-07). Exactly one synchronous cross-domain read is permitted: Compliance.check/2 in 5 gated domains (roster, timekeep, payroll, clients, assets) — see ADR-011-F.

Dependency rules (enforced by Boundary library per AR-08):

  • Any domain app → finnest_core
  • finnest_web → any domain context ✓ (HTTP edge exception)
  • finnest_agents → any domain MCP ✓ (agent tier exception)
  • Domain A → Domain B ✗ forbidden (events only)
  • finnest_core → any other app ✗ forbidden (pure foundation)

Alternatives Considered

Alternative Rejected because
Microservices 3-person team cannot run the ops tax. OTP supplies 90% of microservices benefits at 10% of cost. MCP is the extraction escape hatch (ADR-004-F) if we ever need it
Traditional layered monolith Fails D7 atomic isolation; boundaries by convention only (violates Commandment #31)
Full event sourcing Replay + projection-rebuild complexity exceeds 3-person team capacity; events-as-bus-and-audit delivers 80% of benefit
Hexagonal-everywhere Violates Commandment #2 (no speculative abstraction); ports only where ≥2 adapters planned

Consequences

Positive:

  • Independent failure domains — a payroll bug doesn't crash roster or timekeep
  • Independent state per domain — no accidental state sharing
  • Independent supervision — restart strategies tuned per domain
  • Independent scaling via process allocation — hot domains get more processes
  • 90% of microservice benefits, 10% of operational cost
  • MCP contracts (ADR-004-F) let us extract a domain to its own deployment in Phase 3+ without touching other domains

Negative:

  • Harder to split across many teams (not our problem at 3 people)
  • Slightly coarser failure domain than pure microservices (OTP gives us granular enough)
  • Umbrella compile-time quirks (manageable in modern Elixir)

Tipping points for re-evaluation:

  • Single domain's load exceeds single-node capacity → extract via MCP (not abandon the pattern)
  • Team grows past 10 engineers → revisit whether independent team ownership justifies microservice split (most likely: keep pattern, split along domain boundaries)

Relationship to Guardrails

Enforces / is enforced by: AR-03 (OTP app structure), AR-06 (no premature extraction), AR-07 (cross-domain events only), AR-08 (Boundary library compile-time enforcement), AR-16 (MCP at every boundary).