🛸 Hitchhiker's Guide — Phase 05: Streaming Alternatives & CDC
Read this if: you know Flink (P04) and need the rest of the streaming world fast — the other engines, when to pick each, CDC, and the join patterns. Skim, read WARMUP, build the lab.
0. The 30-second mental model
Flink isn't the only stream processor — it's the heaviest. Kafka Streams is a library (no cluster), Spark Structured Streaming is micro-batch on the Spark engine, Akka Streams / FS2 are in-service back-pressured pipelines. They differ in ops and latency, not in the patterns: CDC (change stream → table), as-of joins, interval joins, sessionization. One sentence: pick the engine by operational fit; the patterns are the same everywhere.
1. Engine picker (one line each)
Flink → low-latency, large state, strongest exactly-once; a cluster to run
Kafka Streams → a LIBRARY in your app, state in RocksDB + changelog topics; no cluster
Spark Str. Str. → micro-batch (seconds), same API as batch Spark; free if you're on Spark
Akka Streams → in-service, typed, back-pressured graphs (Reactive Streams)
FS2 → in-service, pure/resource-safe streams on Cats Effect (P12)
2. KStream vs KTable (the duality, again)
KStream = events (append) KTable = latest-value-per-key (compacted log → table)
toTable() / toStream() interconvert state durability = changelog topic in Kafka
This is P02 compaction as a typed API; your apply_cdc is a KStream→KTable fold.
3. CDC in 5 bullets
- DB commit log (binlog/WAL) → change events (Debezium):
op=c/u/d, before/after, LSN. - No dual-write: you capture what the DB already committed.
- Snapshot + streaming: full table snapshot, then stream from that position (no gap).
- Apply idempotently by version/LSN: dup/out-of-order = no-op (P01).
- Tombstone version memory: keep the version after delete so a stale insert can't resurrect the row. Key the topic by PK for per-row ordering.
4. Join patterns (and their traps)
stream↔table (AS-OF) : value valid AT EVENT TIME, not latest → else TEMPORAL LEAKAGE
stream↔stream (INTERVAL): r.ts ∈ [l.ts+lower, l.ts+upper] → bound = bounded state
enrichment (external) : async I/O + cache; the store may lag the stream
5. Sessionization
new session when (ts - last) > gap (gap exclusive)
boundaries are DYNAMIC; a late event can BRIDGE two sessions into one (merge)
The shape for web sessions, trips, viewing sessions.
6. Beginner mistakes that mark you
- "Always Flink" — can't justify it; ignoring Kafka Streams / Spark SS fit.
- Enriching with the current dimension value → temporal leakage.
- Unbounded stream-stream joins → unbounded state.
- Blocking external lookups in an operator (no async I/O) → backpressure.
- CDC apply without LSN gating → duplicates/resurrected rows.
- Forgetting CDC needs PK-keyed topics for per-row ordering.
7. War stories
- "Old events all show users as 'pro'." → enriched with current tier; needed as-of join.
- "The CDC table has rows we deleted." → no tombstone version memory; a stale insert resurrected them.
- "Our enrichment service fell over under load." → synchronous external lookups; switch to async I/O + cache.
- "Kafka Streams app lost its state on redeploy." → no/short changelog retention; the changelog is the durability.
8. How this phase pays off later
- CDC apply → P09 lakehouse upserts, P11 Cassandra/DynamoDB sinks.
- As-of join → capstone fraud detector enrichment (P16).
- Akka/FS2 → built for real in Scala (P12).
- Engine picker → ADRs in P15.
Read WARMUP, build the lab, then P06: we cross from streaming into batch — Spark internals, the other half of the platform.