Process everything at 2 a.m., or process each event as it arrives. The choice looks like a latency decision and is really a complexity decision.
Prerequisites: Message Queues, Analytics Storage Time to read: ~24 minutes
You need daily revenue by region.
Batch: at 2 a.m., query yesterday’s orders, aggregate, write the result. Simple, correct, restartable, and the number is up to 26 hours stale.
Stream: every order updates a running total as it happens. The number is always current, and now you have to answer: what if events arrive out of order? What if one arrives three hours late? What if a worker crashes mid-aggregation? What if you need to recompute after a bug?
🚨 The decision is rarely about whether real-time is “better.” It’s about whether the value of fresh data exceeds the very real cost of stream processing’s complexity. Most of the time, for most questions, it doesn’t.
Run periodically over a bounded dataset.
Every night at 02:00:
read all of yesterday's orders (bounded — the set is known and complete)
aggregate
write results
✅ Simple. A finite input, a deterministic output. It’s a function. ✅ Restartable. Failed at 60%? Run it again. The input hasn’t changed. ✅ Easy to test. Feed known input, assert known output. ✅ Efficient. Bulk reads, large sequential I/O, high throughput per unit of compute. ✅ Easy to reason about correctness. You either processed the day’s data or you didn’t.
❌ Latency equals the interval. A daily job means up to 24 hours of staleness. ❌ Bursty resource usage. Idle for 23 hours, then saturating the cluster. ❌ Late-arriving data needs reprocessing of an already-completed window.
Tools: Spark, plain SQL in a warehouse, dbt, Airflow-orchestrated scripts, Hadoop MapReduce (historically).
Run continuously over an unbounded dataset.
Forever:
read the next event
update state
emit results
✅ Seconds of latency. ✅ Smooth resource usage — steady load rather than a nightly spike. ✅ Enables genuinely real-time products — fraud blocking, live dashboards, alerting, dynamic pricing.
❌ State management is hard. The running aggregate lives somewhere and must survive crashes. ❌ Out-of-order and late events are a permanent design concern, not an edge case. ❌ Reprocessing is harder. Found a bug? You need to replay history and reconcile. ❌ Debugging is harder. There’s no “the input file” to inspect. ❌ Always-on means always-on-call.
Tools: Flink, Kafka Streams, Spark Structured Streaming, ksqlDB, Materialize, Beam.
These four are what make streaming genuinely difficult, and knowing them separates people who’ve read about it from people who’ve operated it.
🚨 The single most important concept in stream processing.
If you aggregate by processing time, that click lands in the 15:00 bucket — wrong. Your hourly report is incorrect, and worse, it’s non-deterministic: reprocess the same data and you get different answers because the timings differ.
Always aggregate by event time. Which immediately creates the next problem.
You must group unbounded data into finite chunks to aggregate it.
| Window type | Shape | Use for |
|---|---|---|
| Tumbling | Fixed, non-overlapping: [0-5m)[5-10m) |
“Orders per 5 minutes” |
| Hopping / sliding | Fixed size, overlapping: 5-min window every 1 min | “Rolling 5-minute average” |
| Session | Gap-based: closes after N minutes of inactivity | “User session length” |
| Global | One window, forever | Running totals |
Session windows are the interesting one — the window boundary is defined by the data, not the clock. A user active at 10:00, 10:02, 10:04, then nothing until 11:30 has two sessions, and you only know the first ended when the gap elapsed.
A window can’t stay open forever, but events arrive late. When do you emit the result?
A watermark is the system’s assertion: “I believe I’ve now seen all events with event time ≤ T.”
Watermark at 15:05 means: the 14:55–15:00 window can be closed and emitted.
An event for 14:58 arriving at 15:07 is LATE.
⚖️ The fundamental trade-off, and it has no correct answer:
Aggressive watermark (allow 10 s lateness) → fast results, more data missed
Conservative watermark (allow 1 hour) → complete results, an hour of latency
Options for handling late events:
🚨 In mobile-heavy products, events can arrive hours or days late — a phone that was offline in a tunnel, on a plane, or simply had the app suspended. Any streaming design for mobile must state its lateness policy explicitly.
The consumer crashes mid-window. What happens to the partial aggregate?
Checkpointing is the answer: periodically snapshot the operator state and the input offsets together, atomically. On recovery, restore the state and rewind the input to the matching offset. Flink’s distributed snapshot algorithm (based on Chandy-Lamport) does exactly this.
🚨 This gives “exactly-once state semantics”, not exactly-once side effects. If your job sends an email or charges a card, replay after recovery will do it again. External effects still need idempotency. This is the same distinction as Kafka’s transactional guarantees, and overclaiming it is a common interview error.
| Batch | Streaming | |
|---|---|---|
| Data | Bounded | Unbounded |
| Latency | Minutes to hours | Milliseconds to seconds |
| State | In the job, discarded after | Persistent, checkpointed |
| Correctness | Straightforward | Watermarks, late data, retractions |
| Reprocessing | Just re-run it | Replay + reconcile |
| Testing | Easy (known input → known output) | Hard (timing matters) |
| Resource profile | Bursty | Steady |
| Failure | Restart the job | Restore from checkpoint |
| Operational burden | Low | High |
Two named patterns you should be able to discuss.
Run both. A speed layer (streaming) gives approximate real-time results; a batch layer gives accurate results later; a serving layer merges them.
flowchart LR
D[Data] --> B[Batch layer<br/>accurate, hours late]
D --> S[Speed layer<br/>approximate, seconds]
B --> V[Serving layer]
S --> V
V --> Q[Queries]
✅ Accurate and fast. ❌ 🚨 You maintain the same business logic twice, in two systems, in two languages. They drift. The bugs are the divergences, and they’re miserable to find. This is the well-known criticism.
Only streaming. To reprocess, replay the log from the beginning with new code.
flowchart LR
D[Data] --> K[[Kafka<br/>durable, replayable log]]
K --> S[Stream processor]
S --> V[Serving layer]
K -.replay from offset 0.-> S2[New version<br/>backfill]
✅ One codebase, one system. Reprocessing is “run a second instance from offset 0.” ❌ Requires retaining all history in the log (or in object storage), and replaying a year of events takes real time and compute.
🎙️ Kappa is generally the modern preference, and knowing why is a good signal: “I’d avoid Lambda — maintaining the same aggregation logic in both Spark and Flink means they inevitably diverge, and the discrepancies are extremely hard to debug. Kappa keeps one implementation and uses Kafka’s replay for backfills.”
Process small batches very frequently — every second, every 10 seconds. Spark Structured Streaming originally worked this way.
✅ Batch’s simplicity (bounded chunks, easy restart) with near-streaming latency. ✅ Better throughput than per-event processing, because of batching efficiency. ❌ Latency is bounded below by the batch interval.
🚨 This covers far more real requirements than people admit. When a stakeholder says “real-time,” they usually mean “not tomorrow.” A 30-second micro-batch satisfies most “real-time dashboard” requests at a fraction of the complexity.
Use batch when:
Use streaming when:
🎙️ The answer that scores well: “I’d start with a batch job every 15 minutes. That satisfies ‘near real-time’ for the dashboard, and it’s dramatically simpler to build, test, and reprocess when something is wrong. If the business genuinely needs sub-minute latency — for fraud blocking, say — I’d add streaming for that specific path rather than converting everything.”
🚨 Interrogate the requirement. “We need real-time” is one of the most commonly overstated requirements in engineering. Ask: what decision is made from this data, and how quickly must it be made? If the answer is “an analyst looks at it each morning,” a nightly job is correct.
| Decision | Gain | Cost |
|---|---|---|
| Batch | Simple, testable, restartable, efficient | Stale by up to the interval; bursty load |
| Streaming | Seconds of latency; enables real-time products | State management, watermarks, late data, always-on ops |
| Micro-batch | Most of streaming’s latency, most of batch’s simplicity | Floor on latency |
| Lambda | Accurate and fast | Two implementations that drift |
| Kappa | One codebase; replay for backfill | Must retain history; replays are slow |
| Aggressive watermark | Fast results | More late data dropped |
| Conservative watermark | Complete results | Higher latency |
1. Build a tumbling window by hand. Kafka + a small consumer that aggregates events into 1-minute buckets by event time. Then inject an event with a timestamp 5 minutes in the past and watch what your naive implementation does with it. That’s the late-data problem, discovered rather than read about.
2. Compare event time and processing time. Generate events with random delays between event time and arrival. Aggregate both ways. Compare the outputs. Then reprocess the same data with different delays and observe that the processing-time result changes while the event-time result doesn’t.
3. Try Kafka Streams or Flink with windowing:
stream.groupByKey()
.windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1)))
.count()
Change the grace period and observe how completeness and latency trade off.
4. Kill it mid-window. Start an aggregation, kill the process halfway, restart it. Without checkpointing, the partial state is lost. With checkpointing, it resumes. Doing this once makes the whole state-management topic concrete.