How Data Moves: Intermediate
What you will be able to do
The high-water mark pattern keeps 1 value from the last successful run, the largest updated_at that run saw, in a small control table. The next run asks the source for every row past that value. The mark is written in the same transaction that commits the loaded rows, so a run that fails halfway leaves it untouched and the retry reads the same rows again instead of skipping them.
| Run | Rows read | Max updated_at seen | Mark stored after run | Outcome |
|---|---|---|---|---|
| 1 (full load) | 2,000,000,000 | 2026-10-18 02:00 | 02:00 | baseline established |
| 2 | 1,340,000 | 2026-10-18 03:00 | 03:00 | committed |
| 3 | 1,180,000 | 2026-10-18 04:00 | 03:00 (unchanged) | failed before commit |
| 4 | 2,510,000 | 2026-10-18 05:00 | 05:00 | committed, rereads run 3's rows |
A monotonic id works as the mark too, but only for tables that are append-only. An order_id mark picks up new orders and misses every status change to an existing one, because an update never produces a larger id. The interviewer is testing whether you ask how the source table changes before you choose the column to watch.
The 4 breaks the interviewer expects
- Store the largest value actually loaded, in the same transaction as the load
- Subtract a lookback wider than the longest source transaction
- Deduplicate the overlap on the business key, newest version wins
- Ask how deletes reach the source before choosing the pattern
- Record the run's wall-clock end time, or any clock other than the source's, as the mark
- Advance the mark before the load commits
- Use an auto-increment id as the mark on a table that gets updated in place
- Assume the source's clocks agree with each other
Commit before processing, or after
| Offset | Event | Processed before crash | Committed before crash | Replayed after restart |
|---|---|---|---|---|
| 0 | play A | yes | yes | no |
| 1 | play B | yes | yes | no |
| 2 | play C | yes | no | yes, a duplicate |
| 3 | play D | no | no | yes |
| 4 | play E | no | no | yes |
| Guarantee | Commit timing | After the crash above | Typical use |
|---|---|---|---|
| At-most-once | before processing | play C lost, D and E processed once | metrics where a gap is cheaper than a repeat |
| At-least-once | after processing | play C processed twice | most pipelines, paired with deduplication |
| Exactly-once processing | atomically with the write, or idempotent sink | play C visible once in the sink | billing, inventory, ledgers |
Exactly-once is a property of the whole path, and the phrase interviewers want is 'exactly-once processing, built from at-least-once delivery plus a sink that tolerates repeats.' One way to get that tolerance is for the sink to accept the offset and the output in 1 transaction, so a crash rolls back both together. The other is an idempotent write, meaning repeating it leaves the same single row, usually because the sink keys on event_id and the second copy overwrites or is rejected. Either way, the log alone cannot deliver it; the sink has to participate, and saying so is the difference between hand-waving and an answer.
The numbers behind deduplication
- Say which side of processing the commit happens on before naming the guarantee
- Mint the event id on the producer so retries carry the same key
- Name the dedup window and what a duplicate older than it does
- Key related events together when their order matters
- Claim exactly-once without describing the sink's part in it
- Treat the log's ordering as global
- Commit offsets before the write has been acknowledged
- Forget the producer retry as a duplicate source
Skipping columns, files and row groups
| Mechanism | What it skips | What it needs from the writer |
|---|---|---|
| Partition pruning | whole directories | the partition column in the WHERE clause |
| Column projection | every column the query does not name | nothing, the layout provides it |
| Row group statistics | groups whose min and max exclude the filter value | data sorted or clustered on the filter column |
| Step | Bytes read |
|---|---|
| Full scan | 1 TB |
| Prune to 1 of 365 daily partitions | about 2.7 GB |
| Project 3 of 50 columns | about 165 MB |
| Statistics on sorted user_id, 1 of about 21 row groups matches | about 8 MB |
The last step assumes row groups of about 128 MB, about 21 per partition; the reader opens 1 group and still fetches only 3 of its 50 column chunks.
Row group statistics only help when values are clustered. If user_id is scattered randomly, every group's minimum sits near 1 and its maximum near the highest id, no group can be ruled out, and the statistics are noise. Sorting or bucketing on the filter column at write time is what turns statistics into skipping, and mentioning the write side separates the answer from the screening version.
When the answer is a row format
- Appending 1 record at a time from a stream
- Fetching 1 whole record by key
- Landing raw payloads before any parsing
- Schemas that change weekly
- Scans over millions of rows and a few columns
- Wide tables with 50 or more columns
- Partitions that are written once and read many times
- Output of a compaction job
Once a complete copy takes longer than 4 days, the design has to avoid complete copies. Only 2 directions survive: an incremental filter such as updated_since, which shrinks each run to the records that changed, and a bulk export endpoint if the vendor offers one, which trades API calls for a file drop. The incremental path carries every hazard from the batch section, including clock drift between the vendor's servers and the need for an overlap window, and saying so is the connection the interviewer hopes to hear.
Offset versus cursor pagination
| Property | Offset pagination | Cursor pagination |
|---|---|---|
| Request shape | offset=3600&limit=100 | cursor=opaque token from the last page |
| Inserts and deletes during the pull | positions shift: inserts repeat records, deletes skip them | position is anchored to a record, so nothing shifts |
| Resume after a crash | store the page number | store the last cursor |
| Jump to an arbitrary page | yes | no, pages are sequential |
| Server cost on deep pages | grows with the offset | constant |
Webhooks and the reconciliation poll
The 2 shapes a fix can take
| Two paths (batch plus speed layer) | One path replayed (log as source of truth) | |
|---|---|---|
| Code | 2 implementations of the same logic | 1 implementation |
| Correcting history | rerun the batch job over the affected days | reset the stream job to an earlier offset and reprocess |
| What it demands | a raw store the batch job can read, and reconciliation between the 2 outputs | log retention covering the oldest day you might need to redo |
| Failure mode | the 2 paths drift and disagree | retention expired, or replay slower than new data arrives |
| Where it fits | heavy batch logic that would be costly to express as a stream | logic that is already a stream and a log you can afford to keep |
The stream job writes its own table continuously; at 02:00 the batch job writes yesterday from the raw store, and the view hands that day to batch.
The seam
The serving view is where the paths touch, and the interviewer checks the filters. If both branches could return the same date, totals double for that day. A view that hard-codes current_date as the boundary has the opposite bug: between midnight and the 02:00 batch, yesterday belongs to neither branch and drops off the dashboard, and a batch job 3 hours late stretches the hole past 5 hours. Keying the boundary on the latest date the batch table holds lets the stream serve every day batch has not closed, and a day changes owner the moment batch writes it. That answers 'what does the dashboard show while you fix it': the stream's number until batch replaces it, never a blank day and never a doubled one.
> A food delivery marketplace pays restaurants every Monday for the previous week's orders. Order events stream from the app into a live operations dashboard, a nightly batch job rebuilds each day's order totals from the raw store, payment-processor fees arrive from a vendor API limited to 1,000 requests per hour, and the whole lake is Parquet partitioned by order date. On a Thursday, finance reports that 212 restaurants were paid for orders that appear twice.
The five follow-up questions where interviewers find out whether you understand the pipeline or only ran it.
- Category
- Pipeline Architecture
- Difficulty
- intermediate
- Duration
- 25 minutes
- Challenges
- 3 hands-on challenges
Topics covered: Batch Mechanics, Stream Guarantees, File Format Depth, API Patterns, Hybrid Architectures
Lesson Sections
- Batch Mechanics
The question arrives dressed as a capacity complaint: 'The orders table holds 2 billion rows and the nightly full reload no longer finishes before the dashboards refresh. What do you change?' Every candidate can describe reading only new rows. The score comes from the failure cases, so the interviewer repeats the pattern back to you and then waits for the part where you explain what it misses. Run 3 shows the behaviour the interviewer checks for. It read 1.18 million rows and failed before commi
- Stream Guarantees
This one comes out of your own answer. You describe a streaming pipeline, mention exactly-once in passing, and the interviewer says: 'Walk me through the consumer crashing after it has processed 3 events but only committed 2.' The rubric separates candidates who know the 3 guarantee names from candidates who can point at the line where the guarantee is decided. Commit before processing, or after A consumer reads from 1 partition of the log at an offset, processes the event, and at some point tel
- File Format Depth
'Why Parquet?' is a screening question, and the screening answer, 'it is columnar and compressed', earns 1 point of a possible 4. The interviewer wants the mechanisms that let a query read less, because they decide whether a dashboard loads or times out, and because a candidate who knows them also knows when the format is the wrong choice. Skipping columns, files and row groups A Parquet file is split into row groups, each holding a slice of rows, and within a row group every column is stored as
- API Patterns
The advanced probe hands you a vendor API and a figure: 'We need all 10 million records, the API allows 1,000 requests per hour and returns 100 records per page. Design the ingestion.' Candidates who start describing a loop have missed the question. The interviewer wants the arithmetic first, because the arithmetic rules out the design most people begin with. Offset versus cursor pagination Offset pagination fails in a way tutorials rarely show. Suppose the feed is sorted newest first and page 3
- Hybrid Architectures
The hybrid question is always a follow-up. You propose a streaming job for the live dashboard, the interviewer nods, and then: 'Your stream processor shipped a bug on Tuesday and has been emitting wrong totals for 3 days. How do you fix history, and what does the dashboard show while you do?' The grading is about the seam: which path owns which data, and who wins when the 2 disagree. The 2 shapes a fix can take Both shapes answer the Tuesday bug, with different bills. In the 2-path shape the sea