Lambda to Kappa Worked Example

Concepts covered: Kappa Architecture

The workload

A podcast platform serves listens per episode to creators and its recommendation ranking, from 3,000 listen events per second. Under Lambda, a nightly recompute of episode_listens_batch starts at 02:00 over a file archive of every listen and lands roughly 07:00; a speed layer keeps episode_listens_rt since midnight; consumers read both through a merge query. The codebases drifted twice in a year, and the layer holding the stricter rule set which way totals moved at 07:00. A crawler rule added to batch only made totals drop at the landing, then climb too fast all day. A completed-listen rule raised from 30 to 60 seconds played, in the speed layer only, made totals jump at the landing, then grow too slowly.

Before: 2 jobs, 2 tables and a merge that every consumer pays for on every read.
  • listen_events (source, event feed)
  • listen_archive (storage, files, full history)
  • nightly_recompute (transform, batch, 5 h)
  • episode_listens_batch (storage, through 00:00)
  • speed_counter (transform, stream job)
  • episode_listens_rt (storage, since 00:00)
  • merge_query (custom, read-time join)
  • creator_dashboard (consumer, totals per episode)
  • ranking (consumer, recommendations)
  • listen_events -> listen_archive
  • listen_archive -> nightly_recompute
  • nightly_recompute -> episode_listens_batch
  • listen_events -> speed_counter
  • speed_counter -> episode_listens_rt
  • episode_listens_batch -> merge_query
  • episode_listens_rt -> merge_query
  • merge_query -> creator_dashboard
  • merge_query -> ranking
After: 1 job, 1 table, and an archive the pipeline keeps but no longer reads.
  • listen_events (source, event feed)
  • listen_log (queue, 30-day retention)
  • listen_counter_v3 (transform, stream job)
  • episode_listens_v3 (storage, replaced by episode_id)
  • listen_archive (storage, files, audit copy)
  • creator_dashboard (consumer, totals per episode)
  • ranking (consumer, recommendations)
  • listen_events -> listen_log
  • listen_log -> listen_counter_v3
  • listen_log -> listen_archive
  • listen_counter_v3 -> episode_listens_v3
  • episode_listens_v3 -> creator_dashboard
  • episode_listens_v3 -> ranking

One definition of a completed listen

The code diff, by intent

SELECT
    COALESCE(o.episode_id, n.episode_id) AS episode_id,
    o.listens AS old_listens,
    n.listens AS new_listens
FROM episode_listens_v2 o
FULL OUTER JOIN episode_listens_v3 n
    ON n.episode_id = o.episode_id
WHERE o.listens IS NULL
    OR n.listens IS NULL
    OR o.listens <> n.listens
ORDER BY ABS(COALESCE(o.listens, 0) - COALESCE(n.listens, 0)) DESC
LIMIT 100

The log becomes the master dataset

The archive stops being the master dataset and becomes an audit copy; the log takes that role for 30 days. At 3,000 events per second and about 400 bytes each, 30 days with 3 replicas fills about 9.3TB of broker disk, which the team weighs against the 5-hour nightly job's compute and accepts. A replay runs at 120,000 events per second, and while it runs the output exists twice, so the serving store needs room for 2 copies of episode_listens, about 40GB each. A correction older than 30 days reads the archive through the same transform, across section 1's handoff seam.

The team wants to correct 60 days of history after a tagging bug, but the log retains 30 days. Which plan keeps the single-codebase property?

New checks replace the nightly overwrite

stepwhenwhat happenswhat proves it worked
1Mon 06:00Start listen_counter_v3 at the oldest retained offset, writing episode_listens_v3Job is reading; v3 row count rising
2Tue 00:30v3 reaches the live offset and keeps paceGap between log head and v3 under 5 seconds
3Tue 00:30 to Thu 02:00v3 and the Lambda pair both serve; the diff query runs hourlyDiff lists only episodes with listens in the last 24 hours, differing under 0.1%
4Thu 09:00Dashboard and ranking switch to episode_listens_v3No read of the batch or rt tables for 24 hours
5Fri 09:00Stop nightly_recompute and speed_counter; drop their tablesArchive still written; retention alert set at 25 days

Before step 5 the team writes down what gets harder. With no nightly overwrite, nothing erases drift in the stream job's counts, so a once-free correctness check must be built: an hourly query comparing sampled episodes against raw event counts in the log. A bad deploy now reaches every consumer at once, so every change ships as a new vN replay beside the running version. Retention is now a correctness boundary too: an alert fires when the oldest event a planned correction needs reaches 25 days old, 5 days before the log deletes it, leaving time to replay or read the archive.

Two weeks after cutover, a creator reports an episode's total fell 3% overnight. Under the new design, which cause is possible, and which old-design cause is ruled out?

About this lesson

This section is part of the Batch vs Streaming: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.