How configuration works

How the StoatFlow runtime loads configuration — application.yaml on the classpath, overlay files, environment variables, system properties, and the layered precedence order.

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, overriding logback.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:

#SourceForm
1Environment variables, __ path separatorSTOATFLOW__LANES__COUNT=32
2Environment variables, legacy STOATFLOW_ prefixSTOATFLOW_STOATFLOW_LANES_COUNT=32
3Overlay files from STOATFLOW_CONFIG_FILES (later file wins)STOATFLOW_CONFIG_FILES=base.yaml,prod.yaml
4Bundled application.yml on the classpathsrc/main/resources/application.yml
5Bundled application.yaml on the classpathsrc/main/resources/application.yaml
6Default values in the config data classese.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.

System properties. A handful of settings — notably the license — are read as JVM system properties (-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).

Legacy 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()

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