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.

These are the :core engine settings — the stream-processing knobs that exist whether you embed stoatflow-core directly or run on the batteries-included stoatflow-runtime. On the runtime they live under the stoatflow: block of application.yaml; in plain :core code you set the same values on StreamsConfig (or its Builder). This page groups the ones you'll actually reach for, with their defaults.

This page covers the settings most apps touch. Many more :core knobs exist (dispatch tuning, restoration sizing, RocksDB presets, FK-join, caching, hot-standby HA). For every key with its type and bounds, see Configuration reference. For the layered merge model behind YAML overrides and Kafka client passthrough, see Configuration model and Kafka client config.

Identity and connection

Only application-id is required — it has no default, so the engine won't start without it. bootstrap-servers defaults to localhost:9092, which is fine for local development but you'll set it explicitly for any real broker.

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.application-idapplicationId— (required)Used for the consumer group and the transactional id. Pick a stable value; changing it starts a fresh consumer group.
stoatflow.bootstrap-serversbootstrapServerslocalhost:9092Comma-separated broker list. Override for any non-local broker.
stoatflow:
  application-id: order-processor
  bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092
StoatFlow is single-instance — exactly one process owns all partitions. Startup validates complete partition assignment and (optionally) checks for other live group members. See Lanes and parallelism and the validation.* block in the reference.

Default serdes

When an operator doesn't specify a serde via Consumed, Produced, Materialized, etc., these defaults apply. Both default to ByteArray — set them to the type your topology mostly uses.

YAML keyStreamsConfig fieldDefault
stoatflow.default-key-serdedefaultKeySerdeSerdes.ByteArray()
stoatflow.default-value-serdedefaultValueSerdeSerdes.ByteArray()

In YAML the values are fully qualified serde class names; in code you pass Serde instances:

val config = StreamsConfig.builder("order-processor", "localhost:9092")
    .defaultKeySerde(Serdes.String())
    .defaultValueSerde(Serdes.String())
    .build()
stoatflow:
  default-key-serde: org.apache.kafka.common.serialization.Serdes$StringSerde
  default-value-serde: org.apache.kafka.common.serialization.Serdes$StringSerde
On the runtime, the cleaner way to set typed default serdes is streamsConfigOverrides { ... } in code — see Your first app. Using Schema Registry serdes? Set stoatflow.schema-registry-url; it's propagated to default serdes automatically. More in Serdes.
Setting default-key-serde explicitly does one extra thing: it turns off last-resort boundary key-serde inheritance. StoatFlow cannot distinguish a deliberate Serdes.ByteArray() from an unconfigured default — they are the same value — so "did you configure one at all" is the signal it uses. If you genuinely key a sub-topology boundary with raw bytes, say so here and nothing will second-guess it.

Topology

How the compiler shapes and checks your topology. All of these are StoatFlow-only — Kafka Streams has no equivalent.

YAML keyStreamsConfig fieldDefault
stoatflow.topology.sub-topology-splitsubTopologySplitlazy
stoatflow.topology.processor-api-key-affinityprocessorApiKeyAffinityoff
stoatflow.topology.validation.<rule-id>validationSeverity(rule)per rule

sub-topology-split selects where sub-topology boundaries are opened. lazy (the default) opens one only where a downstream operator genuinely needs the re-keyed record's lane affinity — a grouped aggregation, any join, toTable() — plus every explicit repartition(). That is the Kafka Streams repartitionRequired model, so describe() lines up with a KS original. eager restores the pre-1.0.0 boundary at every key-changing node.

Changing it renumbers sub-topology ids, which show up in thread names, the sub_topology metric tag and the topology endpoints. See Lanes and parallelism — in particular the skew note, if you relied on a re-key to spread work across lanes.

processor-api-key-affinity selects whether a Processor API node's key affinity is presumed from the adapter's shape. off (the default) matches Kafka Streams, which never repartitions before process() / processValues() even with connected stores. presumed restores the pre-1.0.0 boundary. It is inert under sub-topology-split: eager, which splits at every key-changing node regardless.

off extends the lazy split's skew trade to Processor API nodes, and after a many-to-one re-key it gives up key affinity there — a real trade-off, spelled out in Key affinity after a re-key. StoatFlow refuses to compile the provable case (papi-key-affinity-presumed, below). It also puts the node in its upstream sub-topology, so it inherits that sub-topology's lane count rather than starting its own — re-check any per-sub-topology numberOfLanes override you set deliberately.

validation.<rule-id> turns individual topology checks off, or promotes them to build failures:

stoatflow:
  topology:
    sub-topology-split: lazy
    processor-api-key-affinity: off
    validation:
      unresolved-boundary-key-serde: error   # off | warn | error
      inherited-boundary-key-serde: off
      papi-key-affinity-presumed: error      # the default for this one
RuleDefaultFires when
unresolved-boundary-key-serdewarnA sub-topology boundary resolved no key serde for the key crossing it and you configured no default key serde, so the placeholder ByteArray default is used for lane hashing. With a default configured, there is nothing to report and the rule stays quiet at every severity
inherited-boundary-key-serdewarnThe boundary resolved only by inheriting a serde from before the re-key
papi-key-affinity-presumederrorA proven re-key (selectKey, map, groupBy, …) feeds a Processor API node connecting a writable store, with no boundary between them. Read-only stores (KIP-813) are excluded — they cannot be written, so there is no update to lose
papi-key-affinity-presumed-chainwarnThe same shape, but the feeder is itself a Processor API node. process() / addProcessor() declare a key change unconditionally, so the re-key may not exist at all — refusing the build would refuse topologies Kafka Streams runs happily

The two boundary-serde rules and the chain rule default to warn, so upgrading changes nothing about what you are told — only where. papi-key-affinity-presumed is the one that defaults to error, because the shape it names is silent data loss at runtime rather than a diagnostic. All severities are decided at topology compilation rather than at engine start, so TopologyTestDriver reaches them with no broker: at error the driver refuses to build, naming every offending site in one message rather than failing on the first; at warn the same line lands in your own test output. off silences the condition outright. An unknown rule id is rejected at startup rather than silently ignored.

Lanes (parallelism)

Lanes are the engine's virtual partitions — records with the same key always route to the same lane, preserving per-key order while running many keys in parallel. See Lanes and parallelism for the why.

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.lanes.countnumLanesmax(2, CPU cores)More lanes = more per-key parallelism, at the cost of more queues. Start at the default; I/O-bound workloads commonly go 4×–8× cores — see Tuning.
stoatflow.lanes.queue-capacitylaneQueueCapacity300Per-lane queue depth (backpressure). Minimum 32.
stoatflow.lanes.thread-typelaneThreadTypeVIRTUALVIRTUAL (Project Loom) or PLATFORM (OS threads). Keep VIRTUAL unless you've switched the RocksDB backend to JNI and pinning is a concern.
stoatflow:
  lanes:
    count: 32
    queue-capacity: 500

Commit-barrier cadence

The commit barrier is what makes StoatFlow exactly-once: when a barrier flows through the topology, state flushes and the Kafka transaction commits as one atomic unit. The cadence self-tunes at runtime within the bounds you set — interval-ms is the seed, and the engine adapts between min-interval-ms and max-interval-ms. You set the envelope, not the algorithm; how the engine schedules within it is an implementation concern (see Architecture).

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.commit-barrier.interval-msbarrierIntervalMs500Seed interval. The runtime adjusts from here.
stoatflow.commit-barrier.min-interval-msbarrierMinIntervalMs150Floor on barrier-to-barrier time.
stoatflow.commit-barrier.max-interval-msbarrierMaxIntervalMs5000Ceiling on barrier-to-barrier time.
stoatflow.commit-barrier.interval-factorbarrierIntervalFactor1.5Multiplier applied to recent commit duration when spacing barriers, clamped to [min, max] (>= 1.0). Rarely changed.
stoatflow.commit-barrier.timeout-msbarrierTimeoutMs60000Bounds the commit path. Must be greater than interval-ms. Also cascades to the Kafka producer's transaction.timeout.ms, max.block.ms, and delivery.timeout.ms.
stoatflow.commit-barrier.max-epoch-recordsbarrierMaxEpochRecordsunset (no hard cap)Optional hard cap on records committed per barrier. Leave unset unless you need a strict bound.
stoatflow:
  commit-barrier:
    interval-ms: 500
    min-interval-ms: 150
    max-interval-ms: 5000
    timeout-ms: 60000
Tighter latency vs. lower overhead. Lowering min-interval-ms and max-interval-ms commits more often (lower end-to-end latency, more transaction overhead). Raising them batches more work per commit (higher throughput, higher tail latency). The defaults are a balanced starting point — tune from observed commit duration. See Tuning.
A stalled commit is not silent: if a barrier can't complete within timeout-ms, the commit aborts and the process restarts cleanly. On restart, state recovers to the last committed barrier — no loss, no duplication under exactly-once. The barrier scheduling cadence and the commit protocol's internals are engine concerns; see Architecture for the boundary, and Exactly-once for the guarantee.

There are several finer self-tuning knobs under commit-barrier.* (e.g. commit-duration-ema-alpha, initial-max-epoch-records). They have sensible defaults and rarely need changing — the full list is in the reference.

Processing guarantee

Controls the commit protocol. Both modes use barriers for state-flush and offset-commit synchronization; the difference is the transaction boundary.

YAML keyStreamsConfig fieldDefault
stoatflow.processing-guaranteeprocessingGuaranteeEXACTLY_ONCE
ValueBehaviour
EXACTLY_ONCE (default)Kafka transactions commit output, changelog, and consumer offsets atomically. No duplicates or loss on crash recovery. Higher per-commit cost.
AT_LEAST_ONCENon-transactional producer with consumer.commitSync(). Lower commit latency. Output records may be duplicated on crash recovery; windowed aggregations may over-count.
stoatflow:
  processing-guarantee: EXACTLY_ONCE

See Exactly-once for the full semantics and the at-least-once recovery contract.

State and changelog

State stores are RocksDB-backed by default and durably backed by Kafka changelog topics, so state survives restarts. See State stores and State and thread safety.

State directory and memory

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.state.dirstateDir${java.io.tmpdir}/stoatflowPut this on fast local storage (SSD). In Kubernetes, mount a PV — the default temp dir is wiped on pod restart, forcing full restoration.
stoatflow.state.uncommitted-max-bytesstateStoreUncommittedMaxBytes268435456 (256 MiB)Global cap on buffered uncommitted state; exceeding it triggers an early barrier.
stoatflow.state.segment-checkpoint-recordssegmentCheckpointRecords100000Periodic checkpoint cadence (record count) for segmented window/session stores, which run WAL-off and would otherwise be durable only on close(). Bounds how much changelog an ungraceful restart — or a hot-standby promotion — re-applies. Whichever-first with segment-checkpoint-ms.
stoatflow.state.segment-checkpoint-mssegmentCheckpointMs30000 (30 s)The same checkpoint cadence by wall-clock time; whichever-first with segment-checkpoint-records.
stoatflow.rocks-db.presetrocksDbConfigDEFAULT (256 MiB)DEFAULT / LOW_MEMORY (64 MiB) / HIGH_PERFORMANCE (1 GiB) total RocksDB memory.
stoatflow.rocks-db.backendrocksDbBackendTypeAUTOAUTO picks FFM (Foreign Function & Memory — no virtual-thread pinning) on the JVM and JNI under a native image; set FFM or JNI to force one.
stoatflow.state.format-downgradestateFormatDowngraderefuseWhat to do when a store's on-disk format is newer than the topology asks for — today, record headers turned off over a store that carries them. refuse fails startup with an actionable message and touches nothing; wipe-and-restore acknowledges the downgrade and rebuilds that store from its changelog.
stoatflow:
  state:
    dir: /var/lib/stoatflow/state
    uncommitted-max-bytes: 268435456
  rocks-db:
    preset: DEFAULT

Changelog topics

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.changelog.enabledchangelogEnabledtrueWhen on, state mutations are written to changelog topics inside the same transaction.
stoatflow.changelog.create-topics-if-not-existcreateChangelogTopicsIfNotExisttrueAuto-creates compacted changelog topics on startup.
stoatflow.changelog.replication-factorchangelogReplicationFactor-1 (broker default)Set explicitly (e.g. 3) for production durability.
stoatflow.changelog.num-partitionschangelogNumPartitions1One partition is usually sufficient for single-instance.
stoatflow.state.restoration-enabledstateRestorationEnabledtrueRestore state from changelog on startup when local state is missing.
stoatflow:
  changelog:
    enabled: true
    replication-factor: 3
For production, set changelog.replication-factor to at least 3 and put state.dir on a persistent volume — together they keep restoration fast (delta, not full) across pod restarts. See Kubernetes and the Production checklist.

Event-time and watermarks

Watermarks track event-time progress for windowing, late-record handling, and event-time timers. The engine auto-detects whether your topology needs event-time tracking and bypasses it on the hot path when it doesn't. See Event time and watermarks.

YAML keyStreamsConfig fieldDefaultNotes
stoatflow.watermark.max-out-of-orderness-msmaxOutOfOrderness10000 (10 s)How far out of order events may arrive before being treated as late.
stoatflow.watermark.idleness-timeout-mswatermarkIdlenessTimeout300000 (5 min)Partitions idle this long are excluded from the global watermark. 0 disables.
stoatflow.watermark.auto-interval-msautoWatermarkIntervalMs200Periodic watermark emission interval.
stoatflow.processing.timestamp-coordination-modetimestampCoordinationModeAUTOAUTO (derive from topology), ENABLED (force on), DISABLED (force off, benchmarking).
stoatflow.processing.max-task-idle-msmaxTaskIdleMs0Cross-partition timestamp-ordering wait. -1 disables (max throughput); 0 waits only while the broker has data; >0 adds extra wait. Mirrors KS max.task.idle.ms.
stoatflow:
  watermark:
    max-out-of-orderness-ms: 10000
    idleness-timeout-ms: 300000
  processing:
    timestamp-coordination-mode: AUTO
timestamp-coordination-mode: AUTO is correct for almost all topologies — override it only for debugging or benchmarking. Per-source watermark behaviour is configured on Consumed with a WatermarkStrategy; these settings are the engine-wide defaults.

Putting it together

A representative production stoatflow: block:

stoatflow:
  application-id: order-processor
  bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092

  default-key-serde: org.apache.kafka.common.serialization.Serdes$StringSerde
  default-value-serde: org.apache.kafka.common.serialization.Serdes$StringSerde

  processing-guarantee: EXACTLY_ONCE

  lanes:
    count: 32
    queue-capacity: 500

  commit-barrier:
    interval-ms: 500
    min-interval-ms: 150
    max-interval-ms: 5000
    timeout-ms: 60000

  state:
    dir: /var/lib/stoatflow/state
    uncommitted-max-bytes: 268435456

  changelog:
    enabled: true
    replication-factor: 3

  watermark:
    max-out-of-orderness-ms: 10000
    idleness-timeout-ms: 300000

Next steps