Fifty Thousand Retailers

A medium 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
medium
Seniority
L5

Problem

Demand planners on the commercial team can't separate promoted units from baseline, or tell how stale each row's source is. Sell-through arrives as daily distributor EDI files, weekly retailer portal exports from 50,000 partners, and streaming direct-to-consumer orders: design one pipeline that fixes both.

Retail data at CPG scale. Every SKU, every store.

Business requirements

Worked solution and explanation

Three clocks, one fact table

This is a freshness contract problem dressed up as a CPG data lake. Anyone can land three feeds in a warehouse. The skill being probed is whether each channel keeps its own clock all the way into the unified fact, so the commercial team can see how stale each slice is. The trap is one shared nightly schedule, which makes DTC a day late and hides the lag. A second trap is joining promotions as of today, which retags last month's sales. Silently filtering bad retailer rows is the third. Get those wrong and the channel totals quietly stop matching what retailers reported.

Step 1: Give each channel its own freshness tier

dtc_stream is a Flink job marked real-time. edi_loader and portal_loader are Spark batch jobs marked < 24h, because the weekly portal drops are polled daily and loaded the moment they land. The grader needs a streaming path and a batch path visible side by side. Merging the three into one job is the mistake.

Step 2: Stamp freshness and promo status in one canonical transform

canonical_transform_freshness_tag is the only place the three shapes become one schema. It writes a source_freshness column on every row. It joins promotion_calendar on sale_date BETWEEN valid_from AND valid_to, so promoted units get a promo_flag and baseline units stay separable. The tag lives in the data, not in a dashboard footnote.

Step 3: Validate retailer rows and route rejects aside

Duplicates and bad product codes hit validate_dedup_retailer_rows. Valid rows continue. Rejects go to triage_review_store with a rejection reason, so the data team reviews them and replays. A filter that drops them hides how often retailers send junk.

DTC streams in real time while EDI and portal files load in batch. All converge in `canonical_transform_freshness_tag`, which stamps source freshness and promo status. Rejected retailer rows land in `triage_review_store`.
  • dtc_orders (source, Kafka)
  • distributor_edi (source)
  • retailer_portals (source)
  • dtc_stream (transform, Flink); freshness: real-time
  • edi_loader (transform, Spark); freshness: < 24h
  • portal_loader (transform, Spark); freshness: < 24h
  • validate_dedup_retailer_rows (quality gate, Great Expectations)
  • triage_review_store (storage, PostgreSQL)
  • promotion_calendar (storage, PostgreSQL)
  • canonical_transform_freshness_tag (transform, dbt)
  • unified_sales_fact (storage, warehouse)
  • commercial_team (consumer, dashboard)
  • dtc_orders -> dtc_stream
  • distributor_edi -> edi_loader
  • retailer_portals -> portal_loader
  • edi_loader -> validate_dedup_retailer_rows
  • portal_loader -> validate_dedup_retailer_rows
  • validate_dedup_retailer_rows -> triage_review_store
  • validate_dedup_retailer_rows -> canonical_transform_freshness_tag
  • dtc_stream -> canonical_transform_freshness_tag
  • promotion_calendar -> canonical_transform_freshness_tag
  • canonical_transform_freshness_tag -> unified_sales_fact
  • unified_sales_fact -> commercial_team
  • Channels: dtc_orders, distributor_edi, retailer_portals
  • Per-channel ingest: dtc_stream, edi_loader, portal_loader
  • Validation and triage: validate_dedup_retailer_rows, triage_review_store
  • Canonical and warehouse: promotion_calendar, canonical_transform_freshness_tag, unified_sales_fact
  • Consumer: commercial_team
Silent filter
A WHERE clause drops duplicates and bad codes. The fact looks clean, and nobody knows a retailer is sending 4% junk until totals fail to reconcile.
Quarantine for triage
The gate sends rejects to triage_review_store with a reason. The good rows keep moving, the data team sees exactly what each retailer sent wrong, and fixes it at the source.

Common follow-up questions