InterviewKata
System design · Technology guide T08 · Builds on Module 23

Stream and batch processing

Counting clicks per minute, building tonight's recommendations, flagging fraud within a second: all are data processing jobs, and the frameworks for them have names interviewers expect you to know. Spark, Flink, Kafka Streams and Beam overlap a lot, and every cloud runs them for you. This guide explains how each works, how they differ, and which to choose, with a chooser and code you can run.

A 2-minute conversation: Sam asks, Priya explains
0:00 / 2:24

1. Batch and stream

Module 23 teaches the concepts; this guide is about the frameworks.

BatchStream
InputA bounded dataset: yesterday's files, a table snapshotAn unbounded flow of events, usually from Kafka, Kinesis or Pub/Sub
ResultsOnce, when the job finishes (minutes to hours)Continuously, per window or per event (milliseconds to seconds)
Hard partSize: shuffles, skew, costTime: late and out-of-order events, state that must survive failures
Re-runningEasy: run the job againReplay the log from an offset, with the same code

2. How a stream processor handles time

Events carry the time they happened (event time), but arrive later and out of order. A watermark is the processor's estimate that no older events are still coming; windows fire when the watermark passes their end.

3. The products

Batch and streaming · open source

Apache Spark

Splits data into partitions processed in parallel by executors, planning SQL and DataFrame queries into stages separated by shuffles. Structured Streaming runs the same queries over a stream in small batches (micro-batches), with event-time windows and watermarks. Spark 4.0 shipped in 2025.

Best for: large batch jobs and SQL over data lakes, with streaming at second-level latency from the same code.

Streaming first · open source

Apache Flink

Processes events one at a time with keyed state, event-time windows and watermarks. Periodic checkpoints snapshot all state consistently, so a failed job restarts from the last checkpoint with exactly-once state. Flink 2.0 shipped in 2025.

Best for: low-latency stateful streaming (fraud, alerts, real-time aggregates).

Library · open source

Kafka Streams

A Java library, not a cluster: your service consumes Kafka topics, keeps state in local stores backed by Kafka changelog topics, and scales by adding instances to the consumer group.

Best for: stream processing inside services when the data is already in Kafka.

Model and managed runner · Google Cloud

Apache Beam and Dataflow

Beam is a programming model (pipelines of transforms, with windows, triggers and watermarks) that runs on several engines. Google Cloud Dataflow is the managed runner: it autoscales workers, rebalances work, and runs the same pipeline as batch or streaming.

Best for: managed batch and streaming on Google Cloud without running clusters.

Managed clusters · every cloud

Amazon EMR and Dataproc

Managed Spark (and Flink, Hive, Trino) clusters you size and start: Amazon EMR, including EMR Serverless, and Google Cloud Dataproc, including serverless Spark.

Best for: running open-source Spark jobs on a cloud with little change.

Managed Flink · AWS and others

Amazon Managed Service for Apache Flink

Runs Flink applications without managing the cluster; it was previously called Kinesis Data Analytics. Confluent Cloud and others also offer managed Flink next to Kafka.

Best for: Flink streaming on AWS.

Platform · every cloud

Databricks

A managed platform built around Spark and Delta Lake tables, with notebooks, jobs, streaming and SQL warehouses, on AWS, Azure and Google Cloud.

Best for: teams doing data engineering, analytics and ML on one lakehouse platform.

SQL on streams · Azure

Azure Stream Analytics

A managed service where you write SQL with time windows over Event Hubs or IoT Hub input and send results to storage, databases or dashboards.

Best for: simple streaming SQL on Azure without code.

4. Differences at a glance

OptionModelLatencyState and recoveryRuns as
SparkBatch; micro-batch streamingSeconds (streaming), minutes+ (batch)Checkpoints per micro-batchCluster (EMR, Dataproc, Databricks)
FlinkEvent at a timeMillisecondsDistributed snapshots, exactly-once stateCluster or managed service
Kafka StreamsEvent at a timeMillisecondsLocal stores plus changelog topicsA library in your service
Beam on DataflowUnified batch and streamingSecondsManaged by the runnerManaged, autoscaling
Azure Stream AnalyticsSQL over windowsSecondsManagedManaged

5. When to choose which, and why

  1. Is a result every hour or every night good enough? Use batch: Spark, or SQL in a warehouse (T09). It's simpler, cheaper and easy to re-run.
  2. Seconds matter, and the logic is simple (filter, enrich, per-minute counts)? A managed option: Dataflow, Stream Analytics, or Spark Structured Streaming.
  3. Milliseconds and complex state (fraud rules, sessions, joins of streams)? Flink.
  4. The data is already in Kafka and the processing belongs to one service? Kafka Streams inside that service.
  5. Always use event time for anything users will compare to the clock, and decide how late is too late.

Try it: choose for your requirements

6. Windows, watermarks and late events, in code

Ten events, counted in one-minute windows by the time they happened. The batch job sees everything and gets 6 and 4. The stream processor fires the first window when the watermark passes 60 seconds, counting 4; a late event within the allowed lateness updates it to 5; one that arrives far too late is dropped. Counting by arrival time instead gives quite different numbers, which is why event time matters. The program was run in all three languages; each prints the output below it.

WINDOW = 60          # tumbling windows: [0,60), [60,120), ...
DELAY = 10           # watermark = latest event time seen - 10 s (how out of order events may be)
LATENESS = 30        # a fired window still accepts late events until the watermark passes its end + 30 s

# (event time, arrival time) in seconds, listed in arrival order. Phones go offline; events arrive late.
EVENTS = [(5, 6), (20, 22), (59, 61), (64, 65), (55, 66), (72, 73), (50, 80), (100, 101), (30, 110), (119, 120)]


def label(start):
    return f"[{start},{start + WINDOW})"


def batch(key):
    counts = {}
    for event in EVENTS:
        start = key(event) // WINDOW * WINDOW
        counts[start] = counts.get(start, 0) + 1
    return " ".join(f"{label(s)}={counts[s]}" for s in sorted(counts))


print("batch, event time, all data:", batch(lambda e: e[0]))

counts, fired, watermark = {}, set(), float("-inf")
for event_time, arrival in EVENTS:
    start = event_time // WINDOW * WINDOW
    if start in fired and watermark >= start + WINDOW + LATENESS:
        print(f"stream drops event at {event_time} (arrived {arrival}): later than allowed lateness")
        continue
    counts[start] = counts.get(start, 0) + 1
    if start in fired:
        print(f"stream updates {label(start)}={counts[start]} for late event at {event_time} (arrived {arrival})")
    watermark = max(watermark, event_time - DELAY)
    for s in sorted(counts):
        if s not in fired and watermark >= s + WINDOW:
            fired.add(s)
            print(f"stream emits {label(s)}={counts[s]} when the watermark reaches {watermark} (arrival {arrival})")
for s in sorted(counts):
    if s not in fired:
        print(f"stream emits {label(s)}={counts[s]} at end of input")

print("windows by arrival time instead:", batch(lambda e: e[1]))
const WINDOW = 60; // tumbling windows: [0,60), [60,120), ...
const DELAY = 10; // watermark = latest event time seen - 10 s (how out of order events may be)
const LATENESS = 30; // a fired window still accepts late events until the watermark passes its end + 30 s

// [event time, arrival time] in seconds, listed in arrival order. Phones go offline; events arrive late.
const EVENTS: [number, number][] = [[5, 6], [20, 22], [59, 61], [64, 65], [55, 66], [72, 73], [50, 80], [100, 101], [30, 110], [119, 120]];

const label = (start: number) => `[${start},${start + WINDOW})`;
const windowOf = (t: number) => Math.floor(t / WINDOW) * WINDOW;

function batch(key: (e: [number, number]) => number): string {
  const counts = new Map<number, number>();
  for (const e of EVENTS) counts.set(windowOf(key(e)), (counts.get(windowOf(key(e))) ?? 0) + 1);
  return [...counts.keys()].sort((a, b) => a - b).map((s) => `${label(s)}=${counts.get(s)}`).join(" ");
}

console.log("batch, event time, all data:", batch((e) => e[0]));

const counts = new Map<number, number>();
const fired = new Set<number>();
let watermark = -Infinity;
for (const [eventTime, arrival] of EVENTS) {
  const start = windowOf(eventTime);
  if (fired.has(start) && watermark >= start + WINDOW + LATENESS) {
    console.log(`stream drops event at ${eventTime} (arrived ${arrival}): later than allowed lateness`);
    continue;
  }
  counts.set(start, (counts.get(start) ?? 0) + 1);
  if (fired.has(start)) console.log(`stream updates ${label(start)}=${counts.get(start)} for late event at ${eventTime} (arrived ${arrival})`);
  watermark = Math.max(watermark, eventTime - DELAY);
  for (const s of [...counts.keys()].sort((a, b) => a - b)) {
    if (!fired.has(s) && watermark >= s + WINDOW) {
      fired.add(s);
      console.log(`stream emits ${label(s)}=${counts.get(s)} when the watermark reaches ${watermark} (arrival ${arrival})`);
    }
  }
}
for (const s of [...counts.keys()].sort((a, b) => a - b)) {
  if (!fired.has(s)) console.log(`stream emits ${label(s)}=${counts.get(s)} at end of input`);
}

console.log("windows by arrival time instead:", batch((e) => e[1]));
import java.util.*;
import java.util.function.ToIntFunction;

public class Main {
    static final int WINDOW = 60;   // tumbling windows: [0,60), [60,120), ...
    static final int DELAY = 10;    // watermark = latest event time seen - 10 s (how out of order events may be)
    static final int LATENESS = 30; // a fired window still accepts late events until the watermark passes its end + 30 s

    // {event time, arrival time} in seconds, listed in arrival order. Phones go offline; events arrive late.
    static final int[][] EVENTS = {{5, 6}, {20, 22}, {59, 61}, {64, 65}, {55, 66}, {72, 73}, {50, 80}, {100, 101}, {30, 110}, {119, 120}};

    static String label(int start) { return "[" + start + "," + (start + WINDOW) + ")"; }

    static String batch(ToIntFunction<int[]> key) {
        TreeMap<Integer, Integer> counts = new TreeMap<>();
        for (int[] e : EVENTS) counts.merge(key.applyAsInt(e) / WINDOW * WINDOW, 1, Integer::sum);
        StringJoiner j = new StringJoiner(" ");
        counts.forEach((s, c) -> j.add(label(s) + "=" + c));
        return j.toString();
    }

    public static void main(String[] args) {
        System.out.println("batch, event time, all data: " + batch(e -> e[0]));

        TreeMap<Integer, Integer> counts = new TreeMap<>();
        Set<Integer> fired = new HashSet<>();
        long watermark = Long.MIN_VALUE;
        for (int[] e : EVENTS) {
            int eventTime = e[0], arrival = e[1], start = eventTime / WINDOW * WINDOW;
            if (fired.contains(start) && watermark >= start + WINDOW + LATENESS) {
                System.out.printf("stream drops event at %d (arrived %d): later than allowed lateness%n", eventTime, arrival);
                continue;
            }
            counts.merge(start, 1, Integer::sum);
            if (fired.contains(start)) System.out.printf("stream updates %s=%d for late event at %d (arrived %d)%n", label(start), counts.get(start), eventTime, arrival);
            watermark = Math.max(watermark, eventTime - DELAY);
            for (int s : counts.keySet()) {
                if (!fired.contains(s) && watermark >= s + WINDOW) {
                    fired.add(s);
                    System.out.printf("stream emits %s=%d when the watermark reaches %d (arrival %d)%n", label(s), counts.get(s), watermark, arrival);
                }
            }
        }
        for (int s : counts.keySet()) if (!fired.contains(s)) System.out.printf("stream emits %s=%d at end of input%n", label(s), counts.get(s));

        System.out.println("windows by arrival time instead: " + batch(e -> e[1]));
    }
}
batch, event time, all data: [0,60)=6 [60,120)=4
stream emits [0,60)=4 when the watermark reaches 62 (arrival 73)
stream updates [0,60)=5 for late event at 50 (arrived 80)
stream drops event at 30 (arrived 110): later than allowed lateness
stream emits [60,120)=4 at end of input
windows by arrival time instead: [0,60)=2 [60,120)=7 [120,180)=1

7. Say it in the interview

Two-sentence answers
  • "Batch or streaming?" Batch unless someone acts on the result within minutes; streaming when they do, such as fraud checks or live dashboards. Many systems run both: streaming for fresh approximate numbers, batch to correct them.
  • "How do you handle late events?" Window by event time, with a watermark that allows for normal delay and an allowed lateness that updates results; anything later goes to a side output for the batch job to reconcile.
  • "Flink or Spark?" Flink for low-latency stateful streaming; Spark for large batch and SQL, with Structured Streaming when seconds are fine. On Google Cloud, Beam on Dataflow covers both without clusters.

8. Common mistakes

Windowing by arrival timeOffline phones and retries put events in the wrong window. Use event time.
Streaming what could be a nightly jobStreaming is harder to run, test and fix. Choose it only when freshness pays for it.
Exactly-once everywhereFlink's exactly-once covers its own state; writes to outside systems still need idempotent or transactional sinks.
Unbounded stateKeys that never expire (session IDs, users) grow state forever. Set state time-to-live.
Ignoring skewOne hot key sends most of the data to one worker; salt the key or pre-aggregate.

9. Check yourself

A phone records a tap at 10:00:59 but its upload reaches the server at 10:01:20. Which window should count it in a per-minute event-time count?

Event time is when it happened, not when it arrived.

What does a watermark tell a stream processor?

Windows that end before the watermark can be emitted.

Results are needed once a night. Which approach fits best?

Batch is simpler, cheaper and easy to re-run.

A team's service already consumes a Kafka topic and needs per-user counts. A light option?

It's a library: state lives in local stores backed by changelog topics.

10. Sources and further reading

Reviewed on 3 November 2026. Framework versions move quickly; check release notes before relying on version-specific features.

SourceUsed for
Flink: timely stream processingEvent time and watermarks
Spark Structured Streaming guideMicro-batches, windows and watermarks
Apache Beam programming guideWindows, triggers and the unified model
Kafka StreamsLibrary model, state stores and changelogs
Module 23: Batch and stream processingConcepts this guide builds on