# Dynamic config (/docs/operations/dynamic-config)



Ductor has **two** distinct dynamic-config systems, and they are easy to
confuse — the configuration guide and the code both flag the overlap. They
solve different problems, live in different files, and are backed by different
stores. This page disambiguates them. Neither is the same as **static boot
config** (`configs/ductor.yaml`), which is loaded once by viper at process start
and is *not* re-read.

<Callout title="Which config lives where">
  `configs/README.md` is the authoritative ownership rule. Boot-time service
  wiring (DB DSN, bind addresses, OIDC issuer, the AEAD master key) belongs in
  `configs/ductor.yaml`. Restart-free, per-tenant flips belong in the
  dynamicconfig registry. A key must have exactly one canonical owner — the
  other file must not redeclare it.
</Callout>

## 1. The dynamicconfig registry (Postgres/Redis-backed) [#1-the-dynamicconfig-registry-postgresredis-backed]

This is the operator-facing hot-reload plane: **tenant-scoped settings that flip
live**, without a restart, per tenant where the setting's `scope` allows.

* **Codegen source:** `configs/dynamicconfig.yaml` declares each `Setting`
  (its `var_name`, `key`, `type`, `scope`, and `default`). It is consumed by
  `tools/dynamicconfig-gen/` to produce a typed registry; regenerate with
  `make dynamicconfig-generate`.
* **Runtime store:** the live values are in **Postgres + Redis** — *not* in the
  YAML. The YAML only declares the settings and their defaults.
* **Reads:** application code reads a value through the typed registry
  (`dynconfig.DynamicConfig.Get(key, ctx)`), which applies the per-tenant
  override chain.

Typical contents are kill switches, capacity quotas, and cluster rollout gates —
things you want to flip during a canary or an incident without redeploying:

| Example key                    | Scope  | Purpose                                                       |
| ------------------------------ | ------ | ------------------------------------------------------------- |
| `routing.kill_switch`          | tenant | Emergency per-tenant routing kill switch                      |
| `routing.disable_flow_control` | tenant | Disable flow-control checks during routing                    |
| `ductor.queue.shadow_mode`     | global | Shadow-partition queue rollout (`off`/`shadow`/`canary`/`on`) |
| `ductor.scheduling.enabled`    | global | Master kill switch for the scheduling integration             |

## 2. The file-based feature-flag / A-B system [#2-the-file-based-feature-flag--a-b-system]

This is a separate system for **feature flags and A/B experiments**, driven by a
watched file rather than the registry. Enable it under `dynamic_config.*`:

```yaml title="configs/ductor.yaml"
dynamic_config:
  enabled: false
  config_file: /etc/ductor/dynamic.yaml
  watch_interval: 30s   # debounce for file-change detection
```

The referenced file carries three capabilities:

* **Config values** — key/value settings that reload when the file changes.
* **Feature flags** — boolean flags with **percentage rollout** (deterministic
  FNV-1a bucketing on flag key + context ID, so the same tenant always lands the
  same way) and **targeting rules** matching context attributes such as
  `tenant_id` or `region`.
* **A/B tests** — weighted variant selection; weights are proportional ranges in
  `[0, 1)` and must sum to `1.0`, each variant carrying a `config` map your code
  reads.

The file source watches with `fsnotify`. On change, the new file is parsed and
validated, flags and tests are **swapped atomically**, and registered
`OnChange` callbacks fire with the old and new snapshots.

```mermaid
flowchart LR
    Change["File change<br/>(fsnotify)"] --> Parse["Parse + validate"]
    Parse --> Swap["Atomic swap<br/>flags + tests"]
    Swap --> Callbacks["OnChange callbacks<br/>old + new snapshots"]
```

```yaml title="dynamic.yaml"
values:
  routing_timeout_ms: 5000
features:
  - key: experimental_strategy
    enabled: true
    percentage: 25          # 25% rollout
  - key: premium_features
    enabled: true
    rules:
      - attribute: tenant_id
        values: ["acme-corp", "enterprise-inc"]
ab_tests:
  - id: routing_strategy_test
    status: active          # active | paused | completed
    variants:
      - id: round_robin
        weight: 0.6
      - id: weighted
        weight: 0.4
```

## Telling them apart [#telling-them-apart]

|               | dynamicconfig registry                 | file-based feature flags                           |
| ------------- | -------------------------------------- | -------------------------------------------------- |
| Source        | `configs/dynamicconfig.yaml` (codegen) | a watched YAML file (`dynamic_config.config_file`) |
| Runtime store | Postgres + Redis                       | the file itself (via `fsnotify`)                   |
| Enabled by    | always available in the graph          | `dynamic_config.enabled=true`                      |
| Scope         | per-tenant override chain              | percentage rollout + targeting rules               |
| Regenerate    | `make dynamicconfig-generate`          | n/a (edit the file)                                |
| Best for      | kill switches, quotas, rollout gates   | feature flags, A/B experiments                     |

Neither re-reads `configs/ductor.yaml`: boot-time static config is loaded once
and only changes on restart.

## Related [#related]

<Cards>
  <Card title="Configuration reference" href="/docs/reference/configuration">
    The full static configuration surface.
  </Card>

  <Card title="Managing configuration" href="/docs/management/configuration">
    Operating configuration in a running deployment.
  </Card>
</Cards>
