Skip to content

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), and stand-in.enabled (bool). Floor limits and caps stay in application.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 an AtomicReference<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 increments safeledger.switches.refresh.failures.
  • Writes go through the actuator endpoint /actuator/switches (GET lists the effective values, POST {name, value, updatedBy} upserts). It validates ranges, and it rejects stand-in.enabled=true unless 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.
  • DecisionModeRouter and the degradation policy read only RuntimeSwitches, never SafeLedgerProperties directly.

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).