ADR-0001 — Architecture style: modular monolith first, services by extraction
Context
The brief mandates a microservice architecture. BitVault has zero users at the start. Microservices solve organizational and independent-scaling problems (many teams, divergent scaling/deploy profiles) — none of which exist yet. Adopting them on day one buys distributed-systems failure modes (partial failure, network latency, eventual consistency, distributed debugging) and ops overhead without the benefits, and is the most likely way the project stalls in plumbing before the core sync product ships (see Overengineering Ledger).
At the same time, demonstrating microservices, eventing, and Kubernetes is an explicit goal of the project.
Decision
Build a modular monolith (bitvaultd): a single deployable composed of modules
aligned 1:1 with bounded contexts (04).
Each module exposes a gRPC API and app/domain/repo layers from day one, and
communicates with others only through that API or through domain events — never by
importing another module’s internals (07 §4).
Microservices is the target architecture, reached by extraction when a documented forcing function appears (05 §5). Because callers already use the gRPC API and events, extraction changes deployment topology, not callers.
The monolith → services migration is itself a primary portfolio deliverable.
Consequences
Positive
- Fastest path to a correct core (sync, commit protocol, isolation) without distributed-systems tax.
- Trivial local dev and self-host (one binary, one process to run/trace/debug).
- Clean seams preserved by enforced import boundaries → low-cost extraction later.
- A stronger portfolio narrative: disciplined, evidence-driven decomposition.
Negative / costs
- Requires discipline (lint-enforced boundaries) to avoid the monolith decaying into a tangle that resists extraction.
- A single process is a single failure/deploy unit until extraction (acceptable at this scale; mitigated by stateless replicas behind K8s).
- We must build the in-process event-bus + outbox abstraction up front so the later swap to NATS is a config change, not a rewrite (see ADR-0006).
Alternatives considered
- Microservices from day one (as briefed): rejected — premature; pays all costs for no current benefit; high stall risk.
- Unstructured monolith: rejected — fast initially but no extraction path; becomes a big ball of mud.
- Serverless/functions: rejected — poor fit for long-lived sync connections, stateful workers, and self-host portability.
Compliance / how we keep ourselves honest
- CI import-boundary linter fails builds on cross-context internal imports.
- Every extraction ships with an ADR naming the forcing function and a before/after trace + load test (09 P4).