# ADR-001 — Phoenix Umbrella over Single App

**Date:** 2026-04-26
**Status:** Accepted
**Deciders:** Architecture Team

---

## Context

MW-Core must simultaneously serve multiple protocols (REST, WebSocket, LiveView, Mobile) on the
north side and speak to heterogeneous systems (Core Banking, DW, file systems, REST APIs) on the
south side. We need an architectural style that keeps these concerns separated while remaining
operationally simple.

Three options were evaluated:

| Option | Description |
|--------|-------------|
| **A. Single Phoenix App** | Everything in one `lib/` tree |
| **B. Phoenix Umbrella** | One BEAM node, multiple OTP apps compiled separately |
| **C. Microservices** | Separate Elixir services, communicate over HTTP/gRPC/RabbitMQ |

---

## Decision

**Option B — Phoenix Umbrella Application.**

---

## Rationale

### Why not Option A (Single App)?

- A single app cannot enforce dependency boundaries. Gateway code will drift into adapter code,
  breaking encapsulation and making individual component testing impossible.
- A failure in one gateway (e.g., file upload handler OOM crash) can cascade and restart the
  entire application.
- Adding a new protocol gateway requires modifying the single application, increasing coupling.

### Why not Option C (Microservices)?

- Inter-service calls across network boundaries add latency that is unacceptable for synchronous
  banking transactions (target P99 < 200ms).
- Distributed transactions and saga choreography are significant complexity for a team that can
  achieve the same isolation on a single BEAM node.
- Operational burden: service mesh, separate CI/CD per service, distributed log aggregation,
  and network security policies — all for a use case that fits comfortably on one node (or
  a small cluster).

### Why Option B (Umbrella)?

- Each app in the umbrella compiles independently and has its own `mix.exs` dependency list.
  The compiler enforces that `adapter_banking` cannot call `gateway_api` — it simply is not
  a declared dependency.
- Apps run under separate supervisors. An adapter supervisor can restart independently
  without affecting the gateway supervisors.
- Zero-cost inter-app communication: Phoenix.PubSub, ETS, and direct function calls all work
  within the same BEAM node — no serialisation, no network hops.
- Single release artifact: one `mix release`, one Docker image, one Kubernetes deployment.
  All operational complexity of microservices is avoided.
- The same umbrella supports multi-node clusters when needed (via `libcluster` + `Horde`)
  without changing the application code.

---

## Consequences

### Positive
- Clear module boundaries enforced by the compiler
- Independent supervision trees per app
- Zero inter-process latency for same-node calls
- Single deployment unit
- Team can work on individual apps with isolated test suites

### Negative
- All apps must use the same Elixir/OTP version
- A node crash (OOM at OS level) takes all apps down — mitigated by clustering
- Mix releases include all apps; you cannot deploy only one app independently
  (mitigated: use `RELEASE_NAME` env to start selective apps from one release if needed)

---

## Compliance Notes

All gateway apps share the same BEAM node memory space. PII data in messages passes through
in-memory structs without network serialisation. Ensure audit logging captures all PII-adjacent
fields before they leave the node, regardless of downstream adapter response.
