This guide documents the configuration store implementations that load, watch, and persist BridgeConfig documents. For the YAML field reference, see Configuration Reference. For the high-level lifecycle overview, see Configuration Overview.
GoBridge separates configuration concerns into composable contracts:
classDiagram
class Loader {
<<interface>>
+Load(ctx) BridgeConfig, error
}
class Watcher {
<<interface>>
+Watch(ctx) chan BridgeConfig, error
}
class Reloader {
<<interface>>
}
Reloader --|> Loader
Reloader --|> Watcher
class FileSource {
+Load(ctx) BridgeConfig, error
}
class FileWatcher {
+Watch(ctx) chan BridgeConfig, error
+Stop()
}
class DynamoDBLoader {
+Load(ctx) BridgeConfig, error
+Watch(ctx) chan BridgeConfig, error
+Save(ctx, cfg) error
+EnsureTable(ctx) error
}
class Manager {
+Load(ctx) BridgeConfig, error
+Watch(ctx) chan BridgeConfig, error
+Stop()
}
FileSource ..|> Loader
FileWatcher ..|> Watcher
DynamoDBLoader ..|> Reloader
Manager ..|> Reloader
Manager o-- Loader : base + overlays
Manager o-- Watcher : watches all layers
The loader/watcher contracts live in ports (ports/blueprint_loader.go) and
operate on *ports.BridgeConfig. Adapters and external consumers implement them
directly; the config package owns only the orchestration and on-disk write path:
| Package | Types | Role |
|---|---|---|
ports |
Loader, Watcher, Reloader, BridgeConfig |
Contracts implemented by adapters, consumed by Manager and httpapi |
config |
Manager, Layer, MergeFunc, DefaultMerge, WriteFile |
Layered orchestration, merge strategy, atomic file persistence |
This keeps the contracts free of any config dependency.
The admin API consumes ports.ConfigStore for load, save, validation, and
merge. Save assigns the current stored version plus one (or 1 for a new
document), rather than trusting the incoming BridgeConfig.Version. A
successful save updates the caller’s Version; a failed save leaves it
unchanged. Restoring earlier content is another commit and advances the
version too.
parser.FileStore implements this contract with atomic file replacement.
It still requires a single writer: the version read and the replacement are
not an atomic compare-and-swap. parser.WriteFile is the lower-level
serialization helper and preserves the supplied version.
Package: github.com/mariotoffia/gobridge/adapters/native/config/file
Reads a YAML or JSON configuration file from disk. Format is auto-detected from the file extension (.yaml, .yml, .json), or can be overridden.
import (
fileconfig "github.com/mariotoffia/gobridge/adapters/native/config/file"
"github.com/mariotoffia/gobridge/ports"
)
// registry carries the plugin decoders (transports, stores, processors) that
// decode each options block. It is a required argument; build it once at the
// composition root and register each adapter's decoder.
source := fileconfig.NewSource("bridge.yaml", registry)
cfg, err := source.Load(ctx)
| Option | Description |
|---|---|
WithSourceFormat(f) |
Override format auto-detection (parser.FormatYAML, parser.FormatJSON) |
ports.Loader (load only, no watch).parser.ParseFile).*ports.BridgeConfig on success.context: a cancelled context short-circuits before any filesystem work.shared.ErrNotFound; parse errors pass through with the parser’s file/stage annotation.Package: github.com/mariotoffia/gobridge/adapters/native/config/file
Watches a configuration file for changes and re-parses it when modifications are detected. Supports two watch modes.
watcher := fileconfig.NewWatcher("bridge.yaml", registry,
fileconfig.WithWatchConfig(baseCfg.ConfigWatch),
fileconfig.WithLogger(logger),
)
ch, err := watcher.Watch(ctx)
// ch receives *ports.BridgeConfig on each detected change
| Option | Description | Default |
|---|---|---|
WithMode(m) |
ModeNotify (hybrid: directory-scoped fsnotify + periodic hash-resync backstop) or ModePoll (SHA-256 hash) |
ModeNotify |
WithDebounce(d) |
Debounce interval for ModeNotify |
100ms |
WithPollInterval(d) |
Poll interval for ModePoll |
30s |
WithResyncInterval(d) |
Notify-mode hash-reconciliation cadence: periodic SHA-256 comparison catching changes fsnotify missed. Non-positive values are ignored | 30s |
WithBaselineHash(h) |
Seed the change-detection baseline with the hash the caller actually loaded (Source.LoadHash), closing the Load↔Watch race |
hash file at Watch |
WithClock(c) |
Inject the clock used for timers and tickers (nil ignored) |
clock.System |
WithFormat(f) |
Override format auto-detection | FormatAuto |
WithLogger(l) |
Logger for diagnostics | nil |
WithWatchConfig(def) |
Apply settings from a ConfigWatchDef (from the YAML config_watch section) |
– |
| Mode | Mechanism | Best for |
|---|---|---|
| Notify (default) | Hybrid: filesystem events via fsnotify on the containing directory, debounced to coalesce rapid writes, plus a periodic SHA-256 hash-resync backstop (default 30s) that catches changes fsnotify missed — this makes Kubernetes ConfigMap ..data symlink swaps and kernel event-queue overflow safe |
Local disks and K8s ConfigMap volume mounts, fast change detection |
| Poll | Periodic SHA-256 content hash comparison | NFS, EFS, network mounts, subPath mounts |
ports.Watcher (watch only, no initial load).Source.Load for the first load.Stop() to halt watching. The channel is closed on stop or context cancellation.Watcher.CoalescedReloads() counts how often this happened — a non-zero value signals a consumer slower than the file’s change rate, not lost reloads.The watcher can be configured directly from the YAML config file itself:
config_watch:
mode: notify # hybrid: fsnotify + hash-resync backstop
poll_interval: 30s # notify mode: resync cadence; poll mode: poll cadence
debounce: 200ms # for notify mode
In notify mode poll_interval doubles as the hash-resync cadence.
Pass this to the watcher with WithWatchConfig(baseCfg.ConfigWatch).
Package: github.com/mariotoffia/gobridge/adapters/aws/config/dynamodb
Stores the full BridgeConfig as a single DynamoDB item with version-based change detection. Useful for centralized configuration management in AWS environments.
A DynamoDB layer is a base, not an overlay on a file. Wire it as the config source programmatically, or select
config_source: dynamodbin the shipped AWS deployment profile’s bootstrap config. Single and DynamoDB HA support it;filesystem_replicatedsupports file only. The profile runs exactly one base layer, eitherfile(the default) ordynamodb, and gives the admin API that same source’s store. It does not support overlays: an overlay changing underneath a transaction could make the running config differ from the document the API commits. See AWS configuration and Overlays and the admin config API do not compose.
import ddbconfig "github.com/mariotoffia/gobridge/adapters/aws/config/dynamodb"
loader := ddbconfig.NewLoader(ddbClient,
ddbconfig.WithTableName("gobridge-config"),
ddbconfig.WithBridgeID("production"),
ddbconfig.WithPollInterval(30 * time.Second),
)
// Load
cfg, err := loader.Load(ctx)
// Watch for changes
ch, err := loader.Watch(ctx)
// Save (admin/test tooling)
err = loader.Save(ctx, cfg)
// Create table if missing (dev/test)
err = loader.EnsureTable(ctx)
| Option | Description | Default |
|---|---|---|
WithTableName(name) |
DynamoDB table name | "gobridge-config" |
WithBridgeID(id) |
Bridge identifier (partition key prefix) | "default" |
WithPollInterval(d) |
Watch polling interval in ModePoll |
30s |
WithWatchMode(m) |
ModePoll or ModeStreams change detection |
ModePoll |
WithStreamPollInterval(d) |
GetRecords interval in ModeStreams |
— |
WithStreamsClient(c) |
DynamoDB Streams client (required for ModeStreams) |
nil |
WithRegistry(r) |
Plugin registry used to decode the stored config’s options blocks | nil |
| Mode | Mechanism | Notes |
|---|---|---|
| Poll (default) | One strongly-consistent GetItem per instance per interval, comparing the version attribute |
Predictable cost; use for clustered deployments |
| Streams | DynamoDB Streams GetRecords, sub-second propagation |
Streams throughput is ~5 GetRecords/sec per shard shared across all consumers; falls back to ModePoll (with a warning) when a streams client is absent or streams are not enabled on the table. Failure semantics: a throttled GetRecords keeps the iterator (no LATEST reset) and sheds load via equal-jittered exponential backoff up to 30s; a genuinely invalid iterator (or 3 consecutive unknown failures) is re-acquired at LATEST followed by a version-check reconciliation covering the gap; 5 consecutive acquisition failures switch to poll fallback for the rest of the Watch with a single Warn |
| Attribute | Type | Description |
|---|---|---|
PK (partition key) |
String | "config#<bridge-id>" |
SK (sort key) |
String | "current" |
data |
String | Full BridgeConfig serialized as JSON |
version |
Number | Monotonically increasing version counter |
The table uses pay-per-request billing. EnsureTable creates the table idempotently (safe to call multiple times). When the loader runs in ModeStreams, EnsureTable provisions the new table with a KEYS_ONLY stream specification so a self-provisioned deployment actually gets the streams-based Watch it configured; an existing table’s stream settings are left untouched.
ports.Loader, ports.Reloader, ports.ConfigStore, ports.ConditionalConfigStore, ports.ConfigInitializer, and ports.ConfigObserver.GetItem by PK/SK (strongly consistent), parse JSON from data, and set BridgeConfig.Version from the row’s authoritative version attribute. An absent version is zero; malformed or negative versions fail with shared.ErrInvalidConfig.ModePoll): a strongly-consistent GetItem at each poll interval compares the version attribute; the full item is re-parsed only when the version changes. ModeStreams consumes Streams records instead. Channel is closed on context cancellation.Version is updated after success.shared.ErrVersionMismatch without changing the stored document or caller’s version. Use this method when editing a previously loaded config.PutItem, reserving room for the other attributes under DynamoDB’s 400 KiB item limit.config.ValidateWithWarnings and config.DefaultMerge, including advisory warnings and non-mutating overlay merges.shared.ErrNotFound when no config item exists.| Operation | DynamoDB Cost |
|---|---|
| Watch poll (no change) | ~1 RCU per poll (strongly-consistent read of the item, ≤ 4 KB) |
| Watch poll (change detected) | ~1 RCU + full item re-parse |
| Save | 1 WCU per save |
Package: github.com/mariotoffia/gobridge/config
The Manager orchestrates multiple configuration sources in a layered stack. A base layer is loaded first, then overlays are merged on top in order. The merged result is validated before being returned.
mgr := config.NewManager(
config.Layer{Name: "file", Loader: fileSource, Watcher: fileWatcher},
config.WithOverlay(config.Layer{Name: "env", Loader: ddbLoader, Watcher: ddbLoader}),
config.WithManagerLogger(logger),
)
// Load all layers, merge, validate
cfg, err := mgr.Load(ctx)
// Watch all layers for changes, re-merge on any change
ch, err := mgr.Watch(ctx)
defer mgr.Stop()
| Option | Description |
|---|---|
WithOverlay(layer) |
Add an overlay layer (applied in registration order) |
WithMergeFunc(fn) |
Override the default merge strategy |
WithManagerLogger(l) |
Logger for diagnostics |
type Layer struct {
Name string
Loader Loader // required
Watcher Watcher // optional (nil if source doesn't support watching)
}
DefaultMerge)| Section | Merge Behaviour |
|---|---|
bridge |
Overlay non-zero fields replace base; cluster replaces base when non-nil (endpoint map cloned) |
config_watch |
Overlay replaces base entirely if non-nil |
http |
Field-level – non-empty overlay scalar fields win, empty fields keep the base value; the admin_api_key / monitor_api_key secrets are preserved when the overlay omits them or echoes back the "[REDACTED]" marker (a partial PATCH never wipes a configured key) |
stores |
Overlay replaces per role (lease, outbox, dlq individually) |
sessions, receivers, senders, bindings |
Merge by ID – new IDs append; a matching ID is merged field-level, and the base entry’s typed plugin Config (broker URL, credentials, options) is carried forward unless the overlay changes the transport/discriminator |
routes |
Merge by ID – new IDs append; a matching ID is wholesale-replaced (routes carry no plugin Config, so nothing can be lost) |
ports.Loader and ports.Watcher.MergeFunc. Validates merged result. Individual layers are not validated independently (a layer may be intentionally incomplete).WithMergeFunc.flowchart TD
Base["Base Layer<br/>(file: defaults.yaml)"]
Env["Overlay: Environment<br/>(DynamoDB: staging)"]
Inst["Overlay: Instance<br/>(file: instance.yaml)"]
Base --> M[DefaultMerge]
Env --> M
Inst --> M
M --> V[Validate]
V --> Merged["Merged BridgeConfig"]
Example: File base + DynamoDB overlay
fileSource := fileconfig.NewSource("defaults.yaml", registry)
fileWatcher := fileconfig.NewWatcher("defaults.yaml", registry,
fileconfig.WithMode(fileconfig.ModePoll),
fileconfig.WithPollInterval(30*time.Second),
)
ddbLoader := ddbconfig.NewLoader(ddbClient,
ddbconfig.WithBridgeID("staging"),
ddbconfig.WithRegistry(registry),
)
mgr := config.NewManager(
config.Layer{Name: "defaults", Loader: fileSource, Watcher: fileWatcher},
config.WithOverlay(config.Layer{Name: "ddb-staging", Loader: ddbLoader, Watcher: ddbLoader}),
)
cfg, _ := mgr.Load(ctx)
watchCh, _ := mgr.Watch(ctx)
Package: github.com/mariotoffia/gobridge/config/parser
The parser.WriteFile function provides atomic YAML writes with permission preservation:
err := parser.WriteFile("bridge.yaml", cfg)
0600 (not world-readable) because a config can embed secrets; an existing file keeps its current permissions.The HTTP admin API provides transactional config editing over the file-based store:
| Endpoint | Method | Description |
|---|---|---|
/api/v1/admin/config |
GET | Read the current effective config (redacted) |
/api/v1/admin/config/transactions |
POST | Open a transaction against the current version |
/api/v1/admin/config/transactions/{txnID} |
GET | Preview the merged config |
/api/v1/admin/config/transactions/{txnID} |
PATCH | Apply a config overlay (merge) |
/api/v1/admin/config/transactions/{txnID}/commit |
POST | Validate, CAS-check version, write to disk |
/api/v1/admin/config/transactions/{txnID} |
DELETE | Roll back (discard) the transaction |
The transaction manager uses config.DefaultMerge to apply patches and config.WriteFile for atomic commits with version-based CAS (compare-and-swap) to prevent lost updates.
See the HTTP API Reference for the full endpoint table, status codes, and merge semantics, and Credentials & HTTP API for authentication.
Implement the ports.Loader and optionally ports.Watcher interfaces:
type Loader interface {
Load(ctx context.Context) (*ports.BridgeConfig, error)
}
type Watcher interface {
Watch(ctx context.Context) (<-chan *ports.BridgeConfig, error)
}
Guidelines:
Load must return a fully parsed *ports.BridgeConfig.Watch must not emit the initial config – callers use Load for the first read.Manager as either the base layer or an overlay.Example: Consul-backed loader (sketch)
type ConsulLoader struct {
client *consulapi.Client
registry *ports.Registry
}
func (l *ConsulLoader) Load(ctx context.Context) (*ports.BridgeConfig, error) {
kv, _, err := l.client.KV().Get("gobridge/config", nil)
if err != nil { return nil, err }
return parser.Parse(bytes.NewReader(kv.Value), parser.FormatJSON, l.registry)
}
| Implementation | Load | Watch | Persist | Watch Mechanism |
|---|---|---|---|---|
file.Source |
Yes | – | – | – |
file.Watcher |
– | Yes | – | Hybrid fsnotify + hash-resync, or SHA-256 poll |
dynamodb.Loader |
Yes | Yes | Yes (Save) |
Version-number poll (default) or DynamoDB Streams (opt-in, auto-fallback to poll) |
config.Manager |
Yes | Yes | – | Multiplexes all layer watchers |
config.WriteFile |
– | – | Yes | – |
| Document | Description |
|---|---|
| Configuration Overview | Lifecycle, layering, bootstrap pattern |
| Configuration Reference | Every YAML/JSON field documented |
| Transport Configuration | MQTT, SQS, AMQP, Azure SB, HTTP transport options |
| Credentials & HTTP API | Secret management and admin API |
| Scenario: Layered DynamoDB Config | Walkthrough of file + DynamoDB layering |
| Scenario: Dynamic Reconfiguration | Live config reload patterns |