ADR-018: Progressive service extraction behind stable contracts
- Status: Accepted
- Date: 2026-06-18
- Deciders: Principal Architect, Engineering Lead
- Relates to: ADR-001, ADR-003, ADR-011, ADR-019, ADR-020
Context
DemozPay's long-term target is a federation of independently-deployable, independently-scalable, language-replaceable services (TS + Go). But we are pre-launch with a small team, and standing up a dozen services + a service mesh before the first customer is the enterprise-before-customers trap. ADR-001 already chose a modular monolith with a few Go satellites; this ADR makes the evolution rule explicit so "when do we extract a service" is decided once, not per-argument.
Decision
The contract is the permanent architecture; the deployment topology is not. Every bounded-context boundary is expressed as a stable contract — a gRPC proto for synchronous calls and a registered event schema for asynchronous ones (ADR-020) — from day one, even while two contexts share a single deployable. Whether a context runs in-process or as its own service is an operational decision that can change without a redesign.
A context is extracted into its own deployment only when a concrete pressure justifies it: a different scaling profile, a different team owning it, a language switch (e.g. Payroll TS→Go), or a genuinely external party. Until then it stays an in-process module behind its contract.
Alternatives considered
- Full microservices now — rejected: ops cost + distributed-transaction complexity a 5-person team can't carry pre-launch, with no validated load to justify it.
- Monolith with no extraction seams — rejected: makes the eventual carve-out a rewrite instead of a transport swap, and forfeits the polyglot goal.
Consequences
- Positive: launch-velocity now, clean extraction later; a service rewrite (incl. language change) is a transport swap behind an unchanged contract; the team only operates what load actually requires.
- Negative / accepted: discipline tax — boundaries must be contract-shaped even when convenient to reach across; an in-process call must still go through the port, not a direct import (ADR-011).
- Follow-ups: every new context lands with its proto/event schema first (ADR-020); DB boundaries follow the same context lines (ADR-019).
Revisit when
- A context hits a distinct scaling profile, gets its own team, or needs a language switch → extract it to its own deployment + DB (ADR-019), no contract change.