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 apps —
finnest_core(foundation) andfinnest_agents(orchestrator + MCP registry + AI provider) - Plus 1 edge app —
finnest_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).