The Register Never Sleeps

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

We run a 600-store retail chain feeding a Snowflake warehouse, but the POS terminals only bulk-upload each day's sales to S3 after close, so operations is blind to intraday numbers and learns that a store went dark mid-afternoon only when the nightly totals come in low. Build a pipeline that lands each sale in the warehouse within minutes and raises an alert when a store stops sending during business hours. Voids and same-day price corrections arrive as new events that reference the original transaction, and each has to update that original in place instead of adding a second row.

Every swipe lands in the warehouse. The table has to stay current without breaking.

Business requirements

Worked solution and explanation

What this really is

This is a change-data problem wearing a retail costume: a void isn't a new sale, it's a restatement of an old one, and a silent register isn't missing data, it's the signal. Swapping the end-of-day batch for continuous ingestion is the easy half everyone reaches for. The trap is treating the void as just another row and trusting someone to notice a dark store. Get the first wrong and fact_sales carries both 'completed' and 'voided' for one transaction id, so totals diverge from the register tape. Get the second wrong and ops learns a store died at noon only when nightly numbers come in low.


Walk the requirements

Step 1: Land each sale within minutes

An S3 event notification triggers snowpipe_ingest to load each POS file into staging_table on arrival, and ops_dashboard reads the warehouse. No end-of-day wait. When a store reconnects after an outage and dumps a backlog, Snowpipe absorbs it one file at a time.

Step 2: Resolve voids against the original by id

staging_table dedups on transaction id first (Snowpipe is at-least-once). Then fact_sales_merge runs a MERGE: a new sale inserts; a void or price correction, which arrives as a new event referencing the original id, updates that row's status and net amount instead of appending. Appending the void is the version where duplicates pile up and totals drift.

Step 3: Alert on silence during business hours

heartbeat_monitor tracks per-store arrival and fires to ops_alerts when a store goes quiet past its expected tolerance, with store id and last-seen time. Catching the gap at end of day is the failure this replaces.


The shape that fits

Snowpipe loads S3 files into `staging_table`, which dedups by transaction id; `MERGE` into `fact_sales` resolves voids and corrections in place; `heartbeat_monitor` alerts ops to silent stores during business hours.
  • pos_terminals (source, API); freshness: real-time
  • s3_staging (storage, S3)
  • snowpipe_ingest (transform, SQL); freshness: < 1min; idempotency: dedup
  • staging_table (storage, Snowflake); idempotency: dedup
  • fact_sales_merge (transform, dbt); freshness: < 1min; idempotency: upsert
  • fact_sales (storage, Snowflake); freshness: < 1min
  • heartbeat_monitor (quality gate, custom); on error: alert; alerting: Store silent past expected business-hours tolerance
  • ops_dashboard (consumer, Tableau); freshness: < 15min
  • ops_alerts (consumer, PagerDuty); freshness: < 1min
  • pos_terminals -> s3_staging
  • s3_staging -> snowpipe_ingest
  • snowpipe_ingest -> staging_table
  • staging_table -> fact_sales_merge
  • fact_sales_merge -> fact_sales
  • fact_sales -> heartbeat_monitor
  • fact_sales -> ops_dashboard
  • heartbeat_monitor -> ops_alerts
  • Land files: pos_terminals, s3_staging
  • Snowpipe + merge: snowpipe_ingest, staging_table, fact_sales_merge, fact_sales
  • Monitor + consumers: heartbeat_monitor, ops_dashboard, ops_alerts

Common follow-up questions