Apache Spark
stateful processing
event processing
global ID assignment
data engineering

Build stateful chain for different events and assign global ID in spark

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Apache Spark can maintain per-key state across streaming events, but assigning a true global chain ID requires careful design. The practical pattern is to group all related events by a stable business key, keep chain state with mapGroupsWithState or flatMapGroupsWithState, and create the chain ID once when the first event for that key arrives.

First decide what the chain key really is

Before writing Spark code, define the key that identifies "the same chain". Examples might be:

  • a customer ID
  • a session ID
  • an order ID
  • a correlation ID shared across event types

If different event types belong to the same logical chain, they must be normalized onto the same grouping key. Without that, Spark cannot keep the state together.

Use Structured Streaming state, not ad hoc IDs

For modern Spark streaming jobs, flatMapGroupsWithState is usually the right API. It lets you read the previous state for a key, update it with new events, and emit enriched output rows.

Simplified Scala example:

scala
case class Event(chainKey: String, eventType: String, payload: String, eventTime: Long)
case class ChainState(chainId: String, lastEventType: String, count: Long)
case class Output(chainKey: String, chainId: String, eventType: String, count: Long)

Then the state update function can initialize the chain once:

scala
1import java.util.UUID
2import org.apache.spark.sql.streaming.{GroupState, GroupStateTimeout}
3
4def updateChain(key: String, events: Iterator[Event], state: GroupState[ChainState]): Iterator[Output] = {
5  val sortedEvents = events.toSeq.sortBy(_.eventTime)
6
7  val current = if (state.exists) {
8    state.get
9  } else {
10    ChainState(UUID.randomUUID().toString, lastEventType = "", count = 0L)
11  }
12
13  var working = current
14
15  val out = sortedEvents.map { event =>
16    working = working.copy(
17      lastEventType = event.eventType,
18      count = working.count + 1
19    )
20
21    Output(key, working.chainId, event.eventType, working.count)
22  }
23
24  state.update(working)
25  out.iterator
26}

This gives every event for the same chainKey the same chainId.

Wire it into the streaming query

scala
val result = events
  .groupByKey(_.chainKey)
  .flatMapGroupsWithState(OutputMode.Append(), GroupStateTimeout.NoTimeout())(updateChain)

The important part is that state is scoped by the grouping key. That is how Spark remembers the chain between micro-batches.

Why monotonically_increasing_id() is not the answer

It is common to reach for monotonically_increasing_id() when the problem statement says "global ID". That is usually wrong for stateful chains.

Reasons:

  • it is not stable across reprocessing
  • it is generated per row, not per logical chain
  • it does not naturally preserve a consistent ID for later related events

If you need one chain ID per business entity over time, you need state or an external mapping, not a one-shot row ID function.

When Spark state is not enough

If chains can merge, split, or be discovered across different keys later, simple per-key state may not be sufficient. In those cases, you may need:

  • an external key-to-chain-ID store
  • a deduplication or reconciliation step
  • a deterministic chain ID derived from business logic

Spark can hold state per key very well, but it is not magic. The data model still has to make the chain observable.

Operational concerns

Stateful streaming means you also need to think about:

  • checkpointing
  • state store size
  • late data and event-time ordering
  • timeout and cleanup of old chains

A correct stateful chain design is not just about the update function. It is also about how the state grows and expires in production.

Common Pitfalls

  • Using a row-level ID function when the requirement is a stable chain-level identifier.
  • Grouping different event types by different keys and then expecting one shared state object.
  • Ignoring event ordering when chain transitions depend on sequence.
  • Forgetting checkpointing, which breaks state recovery after restart.
  • Letting state grow forever because no timeout or cleanup policy exists.

Summary

  • Define a stable business key that groups all events belonging to the same chain.
  • Use flatMapGroupsWithState or mapGroupsWithState to persist chain state across batches.
  • Create the chain ID once when the state is first initialized for that key.
  • Do not use monotonically_increasing_id() as a substitute for persistent chain identity.
  • Plan for checkpointing, ordering, and state cleanup from the start.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design