The Dashboard and the Attribution Model

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
L6

Problem

Design one architecture on Azure Databricks that feeds both a real-time campaign performance dashboard and a daily attribution report from a stream of ad impression and conversion events peaking near 200,000 per second. Today separate streaming and batch pipelines produce the two, and their numbers have drifted apart.

Streaming and batch. One pipeline to rule them.

Business requirements

Worked solution and explanation

The trap

This is a consistency problem wearing a streaming-vs-batch costume. Anyone can draw a fast path for the dashboard and a slow path for the report. That is exactly the architecture that is drifting today. The real skill is making the daily report a closed-window read of the same rollups the dashboard reads, so the two views have nothing to disagree about. Keep two computations and every rule gets written twice: dedup, lateness, the attribution lookback. The invoice you bill against then stops matching the number the client watched all day.

Build it in four decisions

Step 1: Dedup once, upstream of both views

Drop duplicates on a stable event id in dedup_stream, before anything aggregates. Today the streaming side dedups approximately and the batch side dedups exactly, and that gap is where the dashboard overcounts. A duplicate that arrives late is still caught, because dedup keys on the id and not on arrival order.

Step 2: Window on event time, with a lateness allowance

event_time_attribution buckets by when the impression happened, not when it arrived. A mobile event that shows up three hours late updates its own hour through the upsert into the warehouse, and because both views read that bucket, both restate together.

Step 3: Give attribution the history it needs

Last-touch credit needs the impressions from before today. event_archive holds the deduped history, and event_time_attribution reads the lookback window from it for each conversion. A same-day-only lookup silently drops credit for yesterday's impression.

Step 4: Serve both views from one warehouse table

Rollups land in rollup_warehouse. The dashboard reads the open periods and the report reads yesterday's closed window. Same rows, two queries, no reconciliation job.

The reference design

One dedup, one event-time attribution job, one `rollup_warehouse`: the dashboard and `daily_report` are two reads of the same rows.
  • impression_events (source, Kafka)
  • conversion_events (source, Kafka)
  • dedup_stream (transform, Flink)
  • event_archive (storage, object storage)
  • event_time_attribution (transform, Flink); idempotency: upsert
  • rollup_warehouse (storage, Databricks)
  • dashboard (consumer, Grafana)
  • daily_report (consumer, reporting)
  • impression_events -> dedup_stream
  • conversion_events -> dedup_stream
  • dedup_stream -> event_archive
  • dedup_stream -> event_time_attribution
  • event_archive -> event_time_attribution
  • event_time_attribution -> rollup_warehouse
  • rollup_warehouse -> dashboard
  • rollup_warehouse -> daily_report
Two synchronized pipelines
Stream dedups approximately, batch exactly. Stream buckets by arrival, batch by event time. Each owns its own attribution window. Numbers diverge, and someone writes a reconciliation job.
One computation, two reads
dedup_stream and event_time_attribution run once. The report is a filter on rollup_warehouse, so any disagreement would have to come from a query, not a pipeline.

Common follow-up questions