The Design Session
A repeatable framework for driving the 60-minute whiteboard, then the whole thing walked end-to-end on a real scenario — orders and payments analytics with late events and a hard reconciliation bar — showing exactly how to climb L1 to L4 out loud.
The six-step framework
Everything in chapters 02–08 is raw material. This chapter is the assembly: a single loop you run on any pipeline prompt so you're never staring at a blank board. It's the macro-version of the five-beat answer structure from Chapter 01, stretched across a full hour.
| # | Step | Budget | What you actually do |
|---|---|---|---|
| 1 | Clarify requirements | ~5 min | Ask before you draw: volume, freshness, correctness bar, consumers, sources, budget. Two or three sharp questions, not twenty. |
| 2 | Define success metrics & SLAs | ~2 min | Write the freshness SLA, completeness target, and correctness bar on the board. These become the yardstick for every later decision. |
| 3 | Sketch high-level architecture | ~10 min | Sources → ingest → landing → transform → marts → serve, with orchestration and observability underneath. Breadth first; get the whole system drawn. |
| 4 | Deep dives on the three levers | ~25 min | Late-arriving data, reconciliation/validation, and failure modes. This is most of the score — go deep with real mechanisms. |
| 5 | State trade-offs explicitly | ~10 min | Latency vs cost vs correctness vs complexity. Make the call; don't just enumerate options. |
| 6 | Evolution & what breaks next | ~5 min | How it scales to 10×, what you'd cut for v1, the next bottleneck. Close by inviting their questions. |
Steps 1–3 are where you demonstrate L1–L2: a clear, correct, idempotent system. Step 4 is where you climb to L3–L4: late data, reconciliation, contracts, failure domains. If you run steps 1–3 crisply and fast, you buy time for the deep dives that carry the grade. Rushing the deep dives to polish the architecture is the classic inversion — spend your minutes where the levels are.
The scenario
Here's the prompt we'll walk the entire framework on — a near-verbatim version of what this loop tends to ask:
"We run a marketplace. We need an analytics pipeline that powers orders and revenue reporting for finance and ops. Order data lives in our app's Postgres database; money movement — captures, refunds, payouts — comes from a third-party payments provider (think Stripe) via API and webhooks. Some events arrive late. Finance needs the numbers to reconcile — reported revenue has to tie out to what the payment processor says we actually collected. Design it."
Three things in that prompt are load-bearing, and you should name them back to the interviewer so they know you heard them: (1) two sources with different semantics — a database you control and a provider you don't; (2) late-arriving events, called out explicitly; (3) a hard reconciliation requirement against an external source of truth. That third point is the tell: this is a financial pipeline, so correctness dominates latency. Anchor every later trade-off to that.
The instinct under pressure is to sketch Kafka → Spark → Snowflake in the first ninety seconds. Resist it. You don't yet know the volume, the freshness need, or whether "reconcile" means "within a cent, daily" or "roughly, monthly." Drawing before clarifying is the single most common way candidates signal L1 — see Chapter 01.
Steps 1–2 · Clarify & set SLAs
Open with a tight volley of clarifying questions. Below are the ones that actually change the design, each with a plausible answer the interviewer might give — internalize the shape so you can improvise when the numbers differ.
| What you ask | Why it moves the design | Plausible answer |
|---|---|---|
| How fresh do finance and ops need these numbers? | Separates batch from streaming; sets the SLA. | "Ops wants intraday-ish; finance closes the day and wants T+1 by 6am." |
| What's the volume — orders/day, payment events/day? | Decides whether full-refresh is even viable, and partition strategy. | "~2M orders/day, ~5M payment events/day. Growing 2× a year." |
| How late do events actually arrive, and how out-of-order? | Sizes the lookback window and watermark. | "Webhooks usually seconds; retries and disputes can land days later. Refunds weeks later." |
| What exactly must reconcile, to what tolerance? | This is the crux — defines the control totals and break policy. | "Daily gross revenue and net payout must tie to the processor's settlement report, to the cent." |
| Who consumes it, and can they tolerate restatements? | Decides whether numbers can move after publish, and contract strictness. | "BI dashboards + the finance close. Restatements are OK if labeled and auditable." |
| Any hard constraints — warehouse, compliance, budget? | Grounds tool choices; avoids designing something they can't run. | "We're on Snowflake with dbt + Airflow. Money data is SOX-relevant." |
Notice you asked six questions, not twenty, and each one has a design consequence. Now write the success metrics on the board — this is Step 2, and it takes thirty seconds but frames the entire rest of the hour:
SUCCESS METRICS (write these on the board, top-right)
Freshness : orders + payments marts ready by 06:00 local, T+1 (SLA)
Complete. : >= 99.9% of a day's events captured before daily close (SLO)
Correctness: daily gross revenue & net payout tie to processor
settlement to within $0.00 (hard break if not) (SLA)
Restatement: allowed, but must be versioned + auditable
Scale bar : 2M orders/day, 5M pay-events/day, ~2x YoY
"Finance wants T+1 by 6am and exact reconciliation; ops wants intraday but can live with hourly. Nothing here needs sub-second latency, and streaming would make the reconciliation harder, not easier. So I'll design a batch/micro-batch pipeline — hourly for ops freshness, with a definitive daily close for finance — and I'll justify that against the SLAs rather than reaching for streaming by reflex." That single sentence demonstrates trade-off reasoning before you've drawn a box.
Step 3 · Sketch the architecture
Now draw — breadth first, left to right, one row for the happy path, with orchestration and observability as cross-cutting rails underneath. Reproduce this on the board roughly as-is. The medallion layering (landing → staging → marts) is straight from Chapter 02; the ingestion choices from Chapter 05.
SOURCES INGEST / CDC LANDING (bronze) TRANSFORM (dbt) MARTS (gold) SERVE
immutable, raw staging -> intermediate
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐ ┌──────────────┐
│ App Postgres │──①─▶│ log-based CDC │───▶│ raw.orders │───▶│ stg_orders │─┐ │ fct_orders │──▶ │ BI / dashbds │
│ (orders) │ ⚡ │ (Debezium) │ │ (append, by _lsn)│ │ (typed, dedup) │ │ │ │ │ (ops, hourly)│
└───────────────┘ └──────────────────┘ └──────────────────┘ └───────────────────┘ ├─▶│ fct_payments │──▶ │ Finance close│
│ │ │ │ (T+1, exact) │
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────────┐ │ │ fct_revenue │ └──────────────┘
│ Payments API │──②─▶│ webhook sink + │───▶│ raw.pay_events │───▶│ stg_payments │─┘ │ (recon'd) │──▶ ┌──────────────┐
│ + webhooks │ ⚡ │ poll reconciler │ │ (append, dedup │ │ int_settlements │ │ dim_customer │ │ recon_report │
│ (Stripe) │ │ (idempotent key) │ │ by event_id) │ │ (SCD2 refunds) │ │ (SCD2) │ │ (audit) │
└───────────────┘ └──────────────────┘ └──────────────────┘ └───────────────────┘ └──────────────┘ └──────────────┘
┌───────────────┐ ▲ WAP: write to _staging, AUDIT (recon+tests), then PUBLISH swap
│ Processor │──③── daily settlement file (S&T truth) ───────────────────┘
│ settlement │
└───────────────┘
══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
ORCHESTRATION │ Airflow: data-aware DAGs, hourly ops run + daily close run, retries+backoff, sensors on landing, backfill-safe
OBSERVABILITY │ freshness + volume + schema-drift monitors, recon dashboard, lineage, alerting -> block on hard breaks
══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
⚡ = most likely failure points (external API, dedup/merge boundary) ①②③ = the three feeds; ③ is the reconciliation source of truth
Walk it in about ninety seconds: two operational feeds (orders via CDC, payment events via webhook) land immutably in bronze, dbt shapes them through staging and intermediate models into finance-grade marts, and a third feed — the processor's daily settlement file — is the independent source of truth you reconcile against. The Write-Audit-Publish gate sits right before the marts go live. Then stop and hand back control:
"That's the whole system end-to-end. There's real depth in three places the prompt flagged — how I absorb late payment events, how I make finance's numbers reconcile, and what happens when the payments API or a run fails. Where do you want to dig first?" You've now demonstrated L1–L2 breadth and handed the interviewer the deep-dive menu you're prepared for.
A few deliberate choices worth defending if asked. CDC for orders (Ch. 05) over a query-based updated_at pull, because at this volume and correctness bar you want completeness and ordering, and you need deletes. Immutable append-only landing keyed by event_id/_lsn, so re-runs never corrupt raw and every restatement is reproducible. dbt on the warehouse (ELT), because the transforms are set-based SQL and the warehouse is where reconciliation SQL naturally lives.
Deep dive · Late-arriving data
The interviewer pulls the first thread: "refunds and disputes land days or weeks after the original order — how do you handle that?" This is Chapter 04 made concrete. Answer in three moves, narrating the level climb as you go.
1 · Event time, not processing time. Every fact is stamped with the processor's event_time (when the money moved) and an ingested_at (when we saw it). All marts aggregate by event date, never by load date — otherwise a refund processed today silently corrupts today's revenue instead of correcting the original day's.
2 · Lookback window + idempotent merge. Each incremental run doesn't just process "since the last watermark". It re-scans a lookback window of arrivals (say 7 days, enough to survive a stalled run) and merges by natural key. Late events landing inside the window get folded into their correct event-date partition; the merge is idempotent so re-processing is a no-op when nothing changed.
-- dbt incremental model: absorbs late-arriving payment events by
-- re-scanning a lookback window and merging on the natural key.
{{ config(
materialized = 'incremental',
incremental_strategy = 'merge',
unique_key = 'payment_event_id',
on_schema_change = 'append_new_columns'
) }}
with src as (
select
payment_event_id,
order_id,
event_type, -- capture | refund | dispute | payout
amount_cents,
currency,
event_time, -- processor's authoritative timestamp
date(event_time) as event_date,
ingested_at
from {{ ref('stg_payments') }}
{% if is_incremental() %}
-- window on ARRIVAL (ingested_at), not event_time, so a refund that lands
-- today with an old event_time is still re-read and merged into its own
-- event_date. 7d of arrivals survives a stalled run; disputes use SCD2 below.
where ingested_at >= (
select dateadd('day', -7, max(ingested_at)) from {{ this }}
)
{% endif %}
)
select * from src
-- MERGE on payment_event_id => re-runs and late arrivals are idempotent:
-- a re-seen event overwrites in place, a new late event inserts into its event_date.
3 · SCD2 for anything that gets corrected. A dispute can flip an order's revenue-recognized status weeks later. Rather than mutate history in place, the customer and settlement dimensions are slowly-changing Type 2 — effective-dated rows so you can answer "what did we believe on the day of close?" (as-of) and "what's true now?" (current). That's what makes restatements auditable instead of destructive.
If you advance the watermark with a strict > on event_time, an event sharing the exact boundary timestamp gets dropped forever. Use >= plus the merge dedup on payment_event_id, and accept re-reading a sliver of overlap — idempotency makes that free. Calling out the off-by-one before you're asked is a clean L2→L3 signal (Ch. 05).
"The L1 answer is 'reprocess yesterday.' L2 is a lookback window plus an idempotent merge so re-runs are safe. L3 — what I'd actually build — adds SCD2 effective-dating for corrections and an explicit completeness SLA: we hold the finance close until ≥99.9% of the day's events are in. L4, if disputes months out matter, is bitemporal modeling — valid-time vs system-time — so a restatement is a first-class, queryable event, not a silent overwrite." You just walked all four rungs in four sentences.
Deep dive · Reconciliation
This is the heart of the prompt, and the single highest-leverage deep dive. The interviewer asks: "Finance says the number has to tie out. How do you guarantee that?" Everything here is Chapter 06 applied to money.
The core idea: you have two independent representations of the same truth — your event-derived fct_payments, and the processor's daily settlement report (feed ③). Reconciliation is the daily proof that they agree, per day, per currency, to a tolerance. You compute control totals on both sides and diff them.
-- Source-vs-target control-total reconciliation, by settlement day + currency.
-- SOURCE OF TRUTH = processor settlement file; TARGET = our warehouse marts.
with target as ( -- what our pipeline says we collected
select
event_date as settle_date,
currency,
count(*) as target_rows,
sum(amount_cents) as target_amount_cents
from {{ ref('fct_payments') }}
where event_type in ('capture', 'refund')
group by 1, 2
),
source as ( -- what the processor says we collected
select
settlement_date as settle_date,
currency,
count(*) as source_rows,
sum(net_amount_cents) as source_amount_cents
from {{ ref('stg_settlement') }}
group by 1, 2
),
recon as (
select
coalesce(s.settle_date, t.settle_date) as settle_date,
coalesce(s.currency, t.currency) as currency,
s.source_rows, t.target_rows,
s.source_amount_cents, t.target_amount_cents,
coalesce(t.target_amount_cents, 0)
- coalesce(s.source_amount_cents, 0) as variance_cents,
coalesce(t.target_rows, 0)
- coalesce(s.source_rows, 0) as row_variance
from source s
full outer join target t
on s.settle_date = t.settle_date and s.currency = t.currency
)
select
*,
case
when abs(variance_cents) = 0 and row_variance = 0 then 'OK'
when abs(variance_cents) <= 100 then 'WARN' -- <= $1.00 rounding tolerance
else 'BREAK' -- hard: block publish
end as recon_status
from recon
order by settle_date desc, currency;
Three design points to state as you show this:
- Tolerances are explicit and tiered. Zero-cent variance is
OK; sub-dollar (FX rounding, fees) is a loggedWARN; anything larger is aBREAK. Finance agreed the tolerance up front — you don't invent it silently. - A
full outer join, deliberately. It surfaces rows present on either side alone — a settlement with no matching events (we missed an ingest) or events with no settlement (a capture the processor hasn't settled yet). Those asymmetries are exactly the breaks you must catch. - The recon runs inside Write-Audit-Publish. The marts are built into a staging schema, the recon query is the audit, and promotion to the live schema only happens if there are zero
BREAKrows. A bad day never reaches finance.
# Airflow: Write-Audit-Publish for the daily finance close.
# The circuit breaker: publish is downstream of a recon gate that can halt the DAG.
from airflow.decorators import dag, task
from airflow.exceptions import AirflowFailException
import pendulum
@dag(schedule="0 5 * * *", start_date=pendulum.datetime(2026, 1, 1),
catchup=False, tags=["finance", "wap"])
def daily_close():
@task
def write(): # build marts into an isolated staging schema
run_dbt("build --select marts.finance+ --target close_staging")
@task
def audit(_): # run recon + dbt tests against staging
breaks = query_scalar(
"select count(*) from close_staging.recon_report "
"where recon_status = 'BREAK'"
)
if breaks > 0:
# circuit breaker: do NOT publish, page the on-call, keep last-good live
raise AirflowFailException(f"RECON BREAK: {breaks} day/currency rows fail to tie out")
@task
def publish(_): # atomic swap staging -> live; nothing partial goes live
run_sql("alter table analytics.fct_payments swap with close_staging.fct_payments")
publish(audit(write()))
daily_close()
When they ask "and when it breaks?" — "The recon report is grained by day and currency, so the break already tells me where. I drill from the failing day into the row-level diff — events we have that settlement doesn't (usually a late webhook, absorbed by tomorrow's lookback) versus settlements with no event (a missed ingest, which I backfill). The audit table is retained, so the whole investigation is reproducible for the SOX trail."
"L1 is a couple of not_null/unique dbt tests. L2 is daily row-count and sum recon against source with a freshness check, blocking on the critical ones. L3 — my target here — is WAP plus tolerances plus a data contract with finance on what 'tie out' means, and quarantine for the rows that don't. L4 is org-wide contracts, anomaly detection on the variance trend so a slow drift pages before it becomes a break, and automated break triage." Same four-rung arc, now on the correctness lever.
Deep dive · Failure modes
The third lever. The interviewer points at a ⚡ on the board — "the payments API is down for two hours during your run. Then what?" This is Chapter 07. Handle it by failure domain, not as one blob.
| Failure | Blast radius | Design response |
|---|---|---|
| Payments API / webhook outage | Payment marts go stale; orders unaffected. | Webhooks are at-least-once; we dedup on event_id. A poll-based reconciler backfills any events missed during the outage, so webhooks are an optimization, not the only path. Degrade gracefully: publish orders on time, hold payments with a stale-data flag. |
| A dbt run fails mid-transform | Could leave marts half-built. | WAP means half-built work lives in staging and never swaps in. Re-run is safe because every model is idempotent; the atomic swap is all-or-nothing. |
| Duplicate / poison webhook | Double-counted revenue. | Idempotency key = event_id, dedup at landing. Malformed events route to a dead-letter table and alert, rather than failing the whole batch. |
| CDC lag / connector stall | Orders fall behind. | Freshness monitor on max(_lsn_ts); sensor won't fire downstream until landing is current, so we never build marts on a partial day. |
| Bad upstream deploy (schema drift) | Silent corruption. | Contract tests + on_schema_change; schema-drift monitor blocks promotion. Alert-and-block on money data, warn on cosmetic fields. |
The strongest single point: "Because webhooks are best-effort at-least-once, I never trust them as the sole path. The daily settlement reconciliation is also my completeness backstop — if a webhook was permanently lost during the outage, the recon BREAK catches it and the poller backfills it. The correctness mechanism and the failure-recovery mechanism are the same mechanism." That closes the loop between two deep dives and reads as genuinely senior.
Steps 5–6 · Trade-offs & evolution
Step back and make the trade-offs explicit — the interviewer wants to hear you choose, with reasons anchored to the SLAs you wrote in Step 2.
| Axis | The choice I made | What it costs / buys |
|---|---|---|
| Latency vs correctness | Batch/micro-batch, not streaming. | Costs intraday sub-minute freshness (nobody asked for it); buys a far simpler, provably-reconcilable pipeline. For a finance loop, correctness wins. |
| Completeness vs freshness | Hold the close until ≥99.9% captured. | Costs a later close; buys numbers finance can sign. Ops gets an earlier, explicitly-provisional hourly view. |
| Cost vs simplicity | Lookback re-scan every run. | Costs re-computing a 7-day window daily; buys dead-simple late-data correctness. Cheap at this volume; revisit if it stops being cheap. |
| CDC vs query-based pull | Log-based CDC for orders. | Costs a connector to operate; buys completeness, ordering, and deletes — non-negotiable for financial completeness. |
Then Step 6 — evolution and what breaks next, which is your on-ramp to L4/L5 (Ch. 08):
- At 10× volume, the daily 7-day lookback re-scan gets expensive. First lever: partition pruning on
event_dateso the merge only touches the lookback partitions. Next: dbt's microbatch strategy to parallelize by day. - When a second consumer appears (data science wants raw events), the landing tables become a shared surface — that's when I'd formalize a data contract and move to an Iceberg/Delta lakehouse for schema evolution and time-travel, so a restatement is a snapshot, not a rebuild.
- When ops genuinely needs sub-minute, add a streaming path for the operational view only (kappa-style), while the reconciled daily batch stays the finance source of truth. Don't make finance pay the complexity tax for an ops requirement.
- What breaks next: the single Airflow instance and the manual break triage. The L5 move is a platform play — self-serve contracts, automated triage, org-wide freshness SLOs — but I'd name that as a direction, not build it for one pipeline.
Trace the arc you just walked: a correct idempotent batch design (L1–L2) → lookback + SCD2 + WAP + tolerances + explicit SLAs (L3) → CDC, contracts, failure-domain reasoning, bitemporal and lakehouse evolution (L4) → a light platform-strategy close (L5). You never claimed a level; you demonstrated each rung by building on the last. That climb — narrated, not asserted — is the entire thing the interview measures.
A second prompt to rehearse
Run the same six steps on a different shape of problem so the framework is muscle memory, not a memorized script. This one inverts the priorities — latency now matters more than exact reconciliation.
"We have a consumer app emitting clickstream and in-app events from mobile and web. Product wants near-real-time analytics — funnels, feature adoption, an anomaly alert if signups drop — with dashboards no more than a couple of minutes stale. Mobile clients are often offline and batch-upload events later. Design it."
How you'd attack it — the outline, not the full walk (that's your rehearsal):
- Clarify & SLAs. Freshness target (couple of minutes ⇒ streaming or micro-batch is justified here). Volume and peak/burst. Correctness bar — is "approximately right, fast" acceptable? For product analytics, usually yes, which flips the central trade-off versus the finance scenario.
- Architecture. Clients → Kafka (partition by user/session key for ordering) → stream processor (Flink or Spark Structured Streaming) → serving store for dashboards, with a parallel raw sink to the lake for replayable batch correction. Draw the streaming row on top, the batch-correction row beneath — a lambda/kappa decision to name explicitly (Ch. 08).
- Late data — the marquee deep dive here (Ch. 04). Offline mobile ⇒ event-time processing with watermarks and bounded allowed lateness; windowing (session windows for funnels) that emit on watermark and correct on late arrivals; the explicit completeness-vs-latency trade-off. This is where the level climb lives in this scenario.
- Reconciliation/validation (Ch. 06). Lighter than finance, but not zero: volume/freshness anomaly monitors (the "signups dropped" alert is literally a DQ control chart), schema-on-write contracts for the event payloads, and a nightly batch recount from the lake as the corrective source of truth for the fast-but-approximate streaming numbers.
- Failure modes (Ch. 07). Consumer lag & back-pressure, checkpoint/restart for exactly-once-ish state, poison events to a dead-letter, and the dedup key for at-least-once delivery from clients that retry uploads.
- Trade-offs & evolution. Streaming buys latency at the cost of operational complexity and harder correctness — the mirror image of scenario one. State that contrast out loud; it proves you're matching the design to requirements rather than pattern-matching to a favorite stack.
Same six steps, opposite conclusion. In the finance scenario correctness dominated and you chose batch; here latency dominates and you chose streaming — and in both you named why against the SLAs. Interviewers are calibrating exactly that: can you run a repeatable process and still reach the design the requirements actually call for? Drill both until the framework is automatic, then move to Chapter 10 to pressure-test the individual mechanisms, and reread Chapter 11 the morning of.
✓ Check yourself
Answer from memory before opening each one.
-
The prompt describes an app database, a payments provider, late events and a requirement that revenue tie out to the processor. Which of those drive the design, and what do they decide about batch versus streaming?
Show answer
Two sources with different semantics, explicit late data, and a hard reconciliation against an external source of truth. Together they make this a financial pipeline where correctness beats latency. Nothing needs sub-second freshness and streaming would make reconciliation harder, so choose batch or micro-batch, hourly for ops and a daily close for finance. See clarify and set SLAs.
-
A refund for a capture made three weeks ago arrives today. The incremental model re-reads a seven-day lookback. Does the filter go on event time or ingestion time, and which partition does the refund end up in?
Show answer
On ingestion time, because the refund arrives today with an old event time and an event-time filter would never re-read it. The merge on the payment event id then writes it into its own event-date partition, so the original day is corrected and today's revenue is untouched. See late-arriving data.
-
The recon query joins source and target with a full outer join and tiers its result into OK, WARN and BREAK. What does the join type catch that an inner join would hide, and who sets the tiers?
Show answer
It surfaces days or currencies present on one side only: a settlement with no matching events means a missed ingest, and events with no settlement may be unsettled captures. An inner join would drop both silently. Finance agrees the tolerance up front, with zero for exact ties and a small rounding band logged as a warning. See reconciliation.
-
The payments API is down for two hours during the run. What happens to orders marts and payments marts, and how do events missed in that window get back?
Show answer
Orders publish on time and payments hold with a stale flag, because failure domains are separate. A poll-based reconciler fetches events the webhooks missed, and the daily settlement reconciliation is the backstop: a permanently lost event shows up as a break and gets backfilled. Webhooks are at-least-once and deduplicated on event id. See failure modes.
-
Ops now wants dashboards under a minute stale, while finance still needs exact daily numbers. Do you move the whole pipeline to streaming? Argue it.
Show answer
No. Add a streaming path for the operational view only and keep the reconciled batch as the finance source of truth, so finance does not pay the complexity tax for an ops requirement. The deciding factors are the freshness SLA each consumer has and how much reconciliation difficulty each can accept. See trade-offs and evolution.
-
Take the clickstream prompt with offline mobile clients. Which decisions from the payments design flip, and which carry over unchanged?
Show answer
Latency now dominates and approximate numbers are acceptable, so streaming with event-time watermarks and bounded allowed lateness is justified. What carries over is the dedup key for at-least-once uploads, a dead-letter path, freshness and volume monitors, and a nightly batch recount from the lake as the corrective source. See a second prompt to rehearse.
Takeaway
- Run the same six steps on any prompt: clarify, set SLAs, sketch breadth first, deep-dive late data, reconciliation and failure, then state trade-offs and evolution.
- Name the load-bearing parts of the prompt back to the interviewer, and choose batch or streaming against the SLAs rather than by reflex.
- Late data is absorbed by a lookback on arrival time and an idempotent merge on a natural key, with each row landing in its event date.
- Reconcile against an independent source of truth by day and currency, with tiers agreed in advance, inside a write-audit-publish gate.
- Handle failure by domain, and let the reconciliation backstop double as the recovery mechanism for events lost in an outage.
- Add a streaming path only for the consumer that needs it, and keep the reconciled batch as the finance source of truth.
- The same process can reach the opposite design when requirements differ, as the clickstream prompt shows.
Next, test the individual mechanisms with questions at each level, with answers climbing from foundational to staff. → Practice Questions
Further reading
- Stripe: Receive Stripe events in your webhook endpoint — Stripe docs. At-least-once delivery, retries and duplicate events, which is why the pipeline dedups on event id.
- Stripe: Payout reconciliation report — Stripe docs. How a processor's own report ties payouts to the transactions behind them.
- Stripe: Idempotent requests — Stripe API reference. A production idempotency-key design.
- dbt: About incremental strategy — dbt Labs docs. Merge, insert_overwrite and microbatch, which the model in this chapter relies on.
- Iceberg: Branching and Tagging — Apache Iceberg docs. Branches as the staging area in write-audit-publish.