This episode dissects how Flink stores state: keyed state with ValueState, ListState, MapState, and AggregatingState, as well as operator state. You'll configure checkpointing with exactly-once guarantees, choose a state backend, and understand restart strategies for fault tolerance.

Episode 5 introduced windowing that stores temporary aggregations. Episode 6 brings that to the surface: state is one of Flink's biggest differentiators compared to other streaming engines. Almost all useful analytics — totals per user, pattern detection, recommendations — require the ability to remember what has already happened.
We'll dissect the types of state, how to store and access it via ValueState, ListState, MapState, and AggregatingState, then configure checkpointing to guarantee exactly-once. Finally, restart strategies make jobs resilient: if a TaskManager dies, the job recovers on its own.
An operator is considered stateful if it stores data between incoming elements. The simplest example: a sum operator inside a window, or an operator that computes a running total per user. Without state, both are impossible — every event is processed without any memory.
Flink distinguishes two families of state:
keyBy operator. For example, the total transactions per userId.keyed state → per key, accessed from a keyed context
operator state → per subtask, shared among related operatorsValueState stores a single value per key. Suitable for tracking the latest value or a simple accumulator:
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
ValueStateDescriptor<Long> desc =
new ValueStateDescriptor<Long>("total", Long.class);
ValueState<Long> total = getRuntimeContext().getState(desc);
Long lama = total.value();
total.update((lama == null ? 0L : lama) + event.getAmount());total.value() retrieves the current value, and total.update overwrites it. This read-write-update pattern is the main idiom of keyed state.
ListState stores a collection of elements; MapState stores key-value pairs. Both are used when state must hold more than one entity:
ListStateDescriptor<String> riwayatDesc =
new ListStateDescriptor<String>("riwayat", String.class);
ListState<String> riwayat = getRuntimeContext().getListState(riwayatDesc);
riwayat.add(event.getId());MapStateDescriptor<String, Double> hargaDesc =
new MapStateDescriptor<String, Double>(
"harga", String.class, Double.class);
MapState<String, Double> harga = getRuntimeContext().getMapState(hargaDesc);
harga.put("apel", 15000.0);
double apel = harga.get("apel");getListState and getMapState follow the same pattern: declare a descriptor, then get the handle from the runtime context. Choose ListState when order matters, MapState when fast lookup by sub-key is needed.
AggregatingState merges all values into a single aggregate result without storing the original elements — efficient for sums, averages, or counts:
AggregatingStateDescriptor<Order, Long, Long> aggDesc =
new AggregatingStateDescriptor<Order, Long, Long>(
"total-agg",
new AggregateFunction<Order, Long, Long>() {
public Long createAccumulator() { return 0L; }
public Long add(Order value, Long acc) { return acc + value.getAmount(); }
public Long getResult(Long acc) { return acc; }
public Long merge(Long a, Long b) { return a + b; }
},
Long.class);
AggregatingState<Order, Long> agg =
getRuntimeContext().getAggregatingState(aggDesc);The AggregateFunction inside the descriptor defines how to initialize, add, get the result, and merge accumulators — this function determines the aggregation logic.
A checkpoint is a periodic snapshot of all state plus the source consumption position. To make it automatic, enable an interval in config.yaml:
execution.checkpointing.interval: 5min
execution.checkpointing.mode: exactly-once
execution.checkpointing.min-pause: 1min
execution.checkpointing.timeout: 10min
execution.checkpointing.tolerable-failed-checkpoints: 2execution.checkpointing.mode chooses between exactly-once and at-least-once. min-pause guarantees checkpoints don't stack on top of each other, and tolerable-failed-checkpoints tolerates minor failures before the job is considered failed.
Flink's checkpointing is based on distributed snapshots (an adapted Chandy-Lamport algorithm). When a process dies, Flink restores state to the last checkpoint and replays events from the stored source position. Because events are reprocessed, the exactly-once guarantee is achieved as long as the sink also supports two-phase commits (like the Kafka sink) or is idempotent.
State is stored in a state backend:
state.backend.type: rocksdb
state.backend.incremental: true
state.backend.local-recovery: trueThe hashmap backend stores state in heap memory — fast but limited. The rocksdb backend stores it on local disk with an in-memory cache — capable of holding huge state. Incremental checkpoints make snapshots lighter because they only send changes since the last checkpoint.
The restart strategy determines how a job recovers after failure. In config.yaml:
restart-strategy.type: fixed-delay
restart-strategy.fixed-delay.attempts: 3
restart-strategy.fixed-delay.delay: 10sThere's also exponential-delay mode, which delays restarts progressively longer on each attempt — suitable for production so downstream systems aren't flooded with retries. The combination of restart strategy and checkpointing is what makes Flink self-healing.
Monitor checkpoint status from the REST API or the dashboard:
curl -s http://localhost:8081/jobs/overview
curl -s http://localhost:8081/jobs | python3 -m json.toolThe curl -s http://localhost:8081/jobs command shows the job list as JSON. In the dashboard, the Checkpoints tab shows snapshot history: state size, duration, and SUCCESS or FAILED status.
Episode 6 equipped you with the foundation of reliability: understanding keyed state and operator state, mastering ValueState, ListState, MapState, and AggregatingState, configuring checkpointing with exactly-once mode, choosing the right state backend, and setting up a restart strategy so jobs recover on their own.
The key takeaways:
keyBy; operator state is attached to a subtask.In the next episode, episode 7, we'll discuss error handling and debugging stream jobs — handling exceptions in operators and sources, monitoring job status and logs, using savepoints for recovery, and debugging with a local cluster and flink run -d. These skills are what separate engineers who write code from those who operate systems.