ADR-0012: Runtime switches stored in Postgres
Status: Accepted · Date: 2026-10-19 · Refines: ADR-0005, ADR-0009
Context
The kill switch, the cohort percentage and the stand-in on/off must change without a deploy, on every instance (A14: ≥ 2 instances share one Postgres), and must survive a restart. An in-memory actuator toggle fails the last two requirements. A config server would add a dependency.
Decision
- Table
runtime_setting (name varchar PK, value varchar NOT NULL, updated_at timestamptz, updated_by varchar NOT NULL), created by a new Flyway migration. - Exactly three switches:
rollout.hold-capture-percent(0–100),rollout.kill-switch(bool), andstand-in.enabled(bool). Floor limits and caps stay inapplication.yml: changing them needs a deploy and Risk sign-off (ADR-0009). - Effective value = the DB row if present, else
application.yml. Each instance caches the switches in anAtomicReference<RuntimeSwitches>and refreshes every 5 s. A change therefore takes effect on every instance within ≤ 5 s. If a refresh fails, the instance keeps the last value and incrementssafeledger.switches.refresh.failures. - Writes go through the actuator endpoint
/actuator/switches(GETlists the effective values,POST {name, value, updatedBy}upserts). It validates ranges, and it rejectsstand-in.enabled=trueunless the floor limit and both caps are > 0. - Working assumption A15: management endpoints are reachable only from the internal network. Authenticating them is out of scope.
DecisionModeRouterand the degradation policy read onlyRuntimeSwitches, neverSafeLedgerPropertiesdirectly.
Consequences
- + One mechanism for every safety switch. Consistent across instances, durable, and auditable
(
updated_by). - − Up to 5 s of propagation delay. That is acceptable, because in-flight payments keep their stored mode (ADR-0005).