Two Years of Clicks, Cheap

A hard Pipeline Design interview practice problem on DataDriven. Design the pipeline on the canvas: sources, transforms, storage and checks, then get feedback on it.

Domain
Pipeline Design
Difficulty
hard
Seniority
L7

Problem

Our platform generates 600 million user interaction events per day through Kafka and we need a cost-efficient architecture to store and query this data for analytics with a two-year retention requirement. Query latency and storage cost are both first-class constraints. Design the end-to-end ingestion, storage, and query architecture.

Two years of clicks. Every query has to be affordable.

Business requirements

Worked solution and explanation

What this really is

This is a storage-tiering problem dressed up as an ingestion question. Getting events out of Kafka is the easy part. The real skill is giving each byte the engine its access pattern deserves: two years of history on cheap object storage, the last few minutes on a small fast store, and queries billed by the bytes they scan. The trap is one big warehouse with a two-year retention setting. It works on day one. Then the bill climbs every month, a heavy trend query stalls the product dashboards, and a retried purchase lands twice in the revenue finance reads.

The shape that fits

Dedup once on `event_id`, then fork: a streaming path into `session aggregates` for dashboards, and a batch path into a compacted `Iceberg` lake that feeds the nightly `daily rollup` and the scan-billed `query serving layer`.
  • raw event stream (source, Kafka)
  • dedup stream (transform, Flink)
  • session aggregates (storage, Redis); freshness: < 1min
  • product dashboards (consumer, Grafana)
  • lake writer (transform, Spark)
  • compacted event store (storage, Iceberg)
  • daily rollup (transform, dbt); freshness: < 24h
  • query serving layer (transform, SQL)
  • analyst reports (consumer, Metabase)
  • raw event stream -> dedup stream
  • dedup stream -> session aggregates
  • session aggregates -> product dashboards
  • dedup stream -> lake writer
  • lake writer -> compacted event store
  • compacted event store -> daily rollup
  • compacted event store -> query serving layer
  • daily rollup -> query serving layer
  • query serving layer -> analyst reports

Step 1: Put dedup upstream of both tiers

dedup stream keys its state on event_id, with a TTL that covers the client retry window. Place it before the fork and both tiers inherit clean data. Place it after the fork and you run two dedup implementations that will eventually disagree.

Step 2: Keep the hot tier small and fresh

session aggregates holds recent rollups in Redis, fed in real time. Dashboards never read the lake, so a two-year scan cannot slow the page that shows the last minute.

Step 3: Batch the lake writes and compact them

lake writer flushes every few minutes, not once per event. Iceberg compaction then merges the output into large Parquet files partitioned by event_date. Thousands of tiny files an hour is how a cheap lake turns into a slow one.

Step 4: Roll up nightly for the common questions

daily rollup builds per-day aggregates on a < 24h cadence, and finance reconciles against those. Most trend reports read the rollup. query serving layer falls through to raw partitions only when a question needs event-level detail.

One warehouse, two-year retention
Every event sits on warehouse storage and every query competes for the same compute. Dashboards and heavy scans share one queue. Cost tracks retention, so it only goes up.
Tiered lake plus hot store
History sits on object storage in compressed columnar files, recent data on a small hot store. Queries pay only for the partitions they prune to. Cost tracks access, and the dashboard path stays isolated.

Common follow-up questions