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.
- Sam Spark, Flink, Kafka Streams, Beam. Which is which?
- Priya They overlap a lot. All of them process data in parallel; they differ in whether they think in batches or in events, how they handle time, and where they run.
- Sam Batch or stream?
- Priya Batch processes a bounded dataset and gives one result when the job ends; the hard part is size. Streaming processes an unbounded flow and gives results continuously; the hard part is time, because events arrive late and out of order, and state that must survive failures.
- Sam How does a stream processor handle time?
- Priya Each event goes to the window for when it happened, its event time. A watermark trails the latest event time and says older events are probably all in. When it passes a window's end, the window fires. A late event within the allowed lateness updates the result; one that's later still is dropped or sent to a side output.
- Sam What are the options?
- Priya Spark, for large batch jobs and SQL, with Structured Streaming in micro-batches. Flink, event at a time, with exactly-once state from checkpoints. Kafka Streams, a library inside your service. Beam, a programming model, run managed by Google Cloud Dataflow. Managed clusters like EMR and Dataproc. Amazon's managed Flink service, formerly Kinesis Data Analytics. Databricks. And Azure Stream Analytics for streaming SQL.
- Sam How do I choose?
- Priya If hourly or nightly is fine, use batch. Seconds and simple logic: a managed option like Dataflow. Milliseconds and complex state: Flink. Data already in Kafka, owned by one service: Kafka Streams. And always window by event time.
- Priya Your turn. A tap happens at 10:00:59 but arrives at 10:01:20. Which minute counts it?
- Priya The 10:00 minute. Event time is when it happened, not when it arrived.
- Sam And the code?
- Priya Ten events in one-minute windows: the batch answer, the streaming answer with a watermark, a correction for a late event, a dropped event, and what goes wrong if you count by arrival time.
- Sam Common mistakes?
- Priya Windowing by arrival time. Streaming what could be a nightly job. Assuming exactly-once covers outside systems. Unbounded state. And ignoring skew.
1. Batch and stream
Module 23 teaches the concepts; this guide is about the frameworks.
| Batch | Stream | |
|---|---|---|
| Input | A bounded dataset: yesterday's files, a table snapshot | An unbounded flow of events, usually from Kafka, Kinesis or Pub/Sub |
| Results | Once, when the job finishes (minutes to hours) | Continuously, per window or per event (milliseconds to seconds) |
| Hard part | Size: shuffles, skew, cost | Time: late and out-of-order events, state that must survive failures |
| Re-running | Easy: run the job again | Replay 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
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.
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).
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.
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.
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.
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.
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.
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
| Option | Model | Latency | State and recovery | Runs as |
|---|---|---|---|---|
| Spark | Batch; micro-batch streaming | Seconds (streaming), minutes+ (batch) | Checkpoints per micro-batch | Cluster (EMR, Dataproc, Databricks) |
| Flink | Event at a time | Milliseconds | Distributed snapshots, exactly-once state | Cluster or managed service |
| Kafka Streams | Event at a time | Milliseconds | Local stores plus changelog topics | A library in your service |
| Beam on Dataflow | Unified batch and streaming | Seconds | Managed by the runner | Managed, autoscaling |
| Azure Stream Analytics | SQL over windows | Seconds | Managed | Managed |
5. When to choose which, and why
- 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.
- Seconds matter, and the logic is simple (filter, enrich, per-minute counts)? A managed option: Dataflow, Stream Analytics, or Spark Structured Streaming.
- Milliseconds and complex state (fraud rules, sessions, joins of streams)? Flink.
- The data is already in Kafka and the processing belongs to one service? Kafka Streams inside that service.
- 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
- "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
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?
What does a watermark tell a stream processor?
Results are needed once a night. Which approach fits best?
A team's service already consumes a Kafka topic and needs per-user counts. A light option?
10. Sources and further reading
Reviewed on 3 November 2026. Framework versions move quickly; check release notes before relying on version-specific features.
| Source | Used for |
|---|---|
| Flink: timely stream processing | Event time and watermarks |
| Spark Structured Streaming guide | Micro-batches, windows and watermarks |
| Apache Beam programming guide | Windows, triggers and the unified model |
| Kafka Streams | Library model, state stores and changelogs |
| Module 23: Batch and stream processing | Concepts this guide builds on |