How configuration works
The :runtime module loads configuration from a layered set of sources: a bundled application.yaml, external overlay files, environment variables, and JVM system properties — merged in a fixed precedence order. Non-serializable settings (serdes, exception handlers, callbacks) are supplied in code through a streamsConfigOverrides block. This page shows each source, the override forms, and the order in which they win.
For why the model is layered this way — and how it maps onto the engine config versus the runtime infrastructure config — see the configuration model. For the actual keys, see core config and runtime config.
The config tree
All configuration lives under two top-level keys (plus an optional logging):
stoatflow:— the stream-processing engine: Kafka connection, lanes, commit barriers, state, changelog, watermarks, license.runtime:— the runtime infrastructure: HTTP server, metrics, health checks, endpoint visibility.logging:— per-logger levels, overridinglogback.xml.
A minimal application.yaml only needs an application-id; everything else has a default. Place it on the classpath at src/main/resources/application.yaml:
stoatflow:
application-id: word-count
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:-localhost:9092}
license:
key: ${STOATFLOW_LICENSE_KEY}
runtime:
http:
enabled: true
port: ${HTTP_PORT:-8080}
metrics:
enabled: true
${VAR} and ${VAR:-default} are resolved by the config loader at startup — handy for keeping secrets and environment-specific values out of the committed YAML. This is the placeholder syntax, distinct from the env-var override mechanism described below.The loader reads both application.yml and application.yaml from the classpath if present; .yml takes precedence over .yaml when both exist. Both are optional — a config built entirely from environment variables and overlay files is valid as long as stoatflow.application-id resolves.
Layered precedence
When the same key is set in more than one place, the highest-priority source wins. From highest to lowest:
| # | Source | Form |
|---|---|---|
| 1 | Environment variables, __ path separator | STOATFLOW__LANES__COUNT=32 |
| 2 | Environment variables, legacy STOATFLOW_ prefix | STOATFLOW_STOATFLOW_LANES_COUNT=32 |
| 3 | Overlay files from STOATFLOW_CONFIG_FILES (later file wins) | STOATFLOW_CONFIG_FILES=base.yaml,prod.yaml |
| 4 | Bundled application.yml on the classpath | src/main/resources/application.yml |
| 5 | Bundled application.yaml on the classpath | src/main/resources/application.yaml |
| 6 | Default values in the config data classes | e.g. runtime.http.port = 8080 |
This is the order applied by the config loader (ConfigLoader.kt). The two env-var conventions in rows 1-2 are layered together as the highest-priority sources, ahead of files.
-Dstoatflow.license.key=...). For the license specifically, env vars and -D system properties take precedence over the YAML stoatflow.license.* fields. System properties are not a general override layer for the whole config tree; use environment variables or overlay files for that.Environment variables
The recommended convention uses __ (double underscore) as the path separator and a single _ as a word boundary within a property name (converted to kebab-case). The first segment must be a top-level key — STOATFLOW, RUNTIME, or LOGGING:
STOATFLOW__LANES__COUNT=32 # stoatflow.lanes.count
STOATFLOW__COMMIT_BARRIER__INTERVAL_MS=5000 # stoatflow.commit-barrier.interval-ms
STOATFLOW__VALIDATION__PRE_FLIGHT_CONSUMER_GROUP_CHECK=false # stoatflow.validation.pre-flight-consumer-group-check
RUNTIME__HTTP__PORT=9090 # runtime.http.port
The double-underscore form is the one to use because it can express multi-word property names (pre-flight-consumer-group-check). The convention matches Docker Compose and Quarkus (RelaxedEnvironmentPropertySource.kt).
STOATFLOW_ prefix. A single-underscore form (STOATFLOW_STOATFLOW_LANES_COUNT=32, STOATFLOW_RUNTIME_HTTP_PORT=9090) also works but cannot set multi-word property names because it uses _ as both separators. Prefer the __ form for anything new.Kafka client and logging maps
Kafka client properties and logging levels are Map<String, String> entries whose keys must keep their dots and word boundaries (linger.ms, org.apache.kafka.clients). Those are read directly from the environment with their own conventions:
STOATFLOW__KAFKA__PRODUCER__LINGER_MS=5 # producer linger.ms = 5
STOATFLOW__KAFKA__CONSUMER__MAX_POLL_RECORDS=1000 # consumer max.poll.records = 1000
LOGGING__LEVEL__ORG_APACHE_KAFKA_CLIENTS=DEBUG # org.apache.kafka.clients = DEBUG
Inside these segments, _ becomes . — so LINGER_MS resolves to linger.ms and env-var entries take precedence over the same key set in YAML (ConfigMapper.kt). See Kafka client config for the full surface.
Overlay files
For deployment-specific configuration that you don't want baked into the application JAR, set STOATFLOW_CONFIG_FILES to a comma-separated list of file paths. The files are deep-merged on top of the bundled classpath YAML, and later files in the list override earlier ones:
# Single external file
export STOATFLOW_CONFIG_FILES=/etc/myapp/application.yaml
# Multiple files — last wins
export STOATFLOW_CONFIG_FILES=/etc/myapp/base.yaml,/etc/myapp/prod-overrides.yaml
Supported extensions are .yaml and .yml. Every file listed is mandatory — if any path doesn't exist (or isn't a regular file) the runtime fails to start with a clear error naming the missing path (ConfigLoader.kt). This is the recommended way to inject a Kubernetes ConfigMap or a mounted secrets file into the runtime.
Programmatic overrides
YAML and environment variables cover everything that can be expressed as a string, a number, or a class name. Things that can't — live serde instances, exception-handler objects, callbacks — are configured in code through the streamsConfigOverrides block on the runtime builder. These overrides are applied last, taking precedence over every YAML and env-var source (ConfigMapper.kt):
val runtime = StoatFlowRuntime.fromConfig(
topologyBuilder = { buildTopology(it) },
configure = {
streamsConfigOverrides {
defaultKeySerde(Serdes.String())
defaultValueSerde(Serdes.String())
}
},
)
runtime.start()
runtime.awaitTermination()
var runtime = StoatFlowRuntime.fromConfig(
Main::buildTopology,
builder -> builder.streamsConfigOverrides(cfg -> {
cfg.defaultKeySerde(Serdes.String());
cfg.defaultValueSerde(Serdes.String());
})
);
runtime.start();
runtime.awaitTermination();
streamsConfigOverrides exposes the same StreamsConfig.Builder the YAML mapping populates — so anything you can set in YAML you can also set here, and your code wins. Reserve it for the non-serializable types and for values you'd rather compute at runtime than declare statically.
Class-name configuration in YAML
Some non-serializable types have a middle ground: if the class has a no-arg constructor, you can name it in YAML as a fully qualified class name and the runtime instantiates it for you. These are applied above the YAML defaults but below streamsConfigOverrides:
stoatflow:
default-key-serde: org.apache.kafka.common.serialization.Serdes$StringSerde
deserialization-exception-handler: io.stoatflow.core.exception.LogAndContinueDeserializationExceptionHandler
The supported class-name keys are default-key-serde, default-value-serde, deserialization-exception-handler, production-exception-handler, processing-exception-handler, and rocks-db-config-setter (ConfigMapper.kt). Anything requiring a constructor argument must go through streamsConfigOverrides instead.
Inspecting the merged result
The runtime exposes the fully merged, masked configuration at the /config HTTP endpoint — useful for confirming that an env-var override or overlay file actually landed where you expected. Sensitive values (passwords, secrets, tokens) are masked in the output:
curl -s localhost:8080/config
See the REST API for the full endpoint reference.
Next steps
- Configuration model — why config is split into
stoatflow(engine) andruntime(infrastructure), and how it composes. - Core config — the
stoatflow.*keys: lanes, barriers, state, changelog, watermarks. - Runtime config — the
runtime.*keys: HTTP server, metrics, health, endpoints. - Defaults and presets — what the out-of-the-box defaults are and the RocksDB presets.
- Configuration reference — every key in one table.
Serdes and Avro
Configure default and per-operator serdes, use the built-in Serdes, write custom serdes, serialize windowed keys, and wire up Avro with Confluent Schema Registry.
Engine configuration (:core)
The :core engine knobs every StoatFlow app sets — application id, brokers, default serdes, lanes, commit-barrier cadence, state dir, changelog, processing guarantee, and event-time coordination — with defaults from source.