"The Field Guide" — How to Read This Series
Larkspur's quarterly review ends with three headline numbers and two answers for each. Rohan, a staff backend engineer, is asked to "just fix the queries". This field guide maps data engineering in 2026, introduces the retailer and its incident, starts the companion lab, reproduces all three disagreements with SQL, and states the design question the series teaches you to answer.
Skim Card
One thing to remember: an analytics number you can’t trace back to its records and reconcile with an outside source is a rumour, however good the dashboard looks.
- Most wrong numbers come from correct SQL over records nobody can account for: duplicates, deletes, dates and definitions.
- Learn concepts before tools; each concept here comes with a Larkspur use case and one from another industry.
- Run every number yourself: the lab reproduces each claim, and each part ends with an interview-style Design Drill.
- The lab runs on a laptop: S3-compatible storage, an Iceberg REST catalog and PostgreSQL in about 0.9 GB of container memory.
- The incident is reproducible: UK revenue £29,999,232.43 vs £29,790,674.06, stock availability 97.2% vs 88.5%, conversion 6.21% vs 5.31%.
- Every defect behind those gaps is in a manifest with its count and effect; the revenue gap splits into five exact amounts.
Verified against: DuckDB 2.0.0, Apache Polaris 1.8.0, SeaweedFS 4.51, PostgreSQL 18.6, Python 3.13 · 28 October 2026
Jump to: The map · The incident · Run the lab · The question
Story Opening
The quarterly business review had a slide titled “Q3 at a glance”, and every number on it was argued with before the meeting ended.
UK net revenue was £30.00 million. Farah Siddiqui, the finance controller, had spent the previous week reconciling the payment provider’s settlement files in a spreadsheet, and they supported £29.79 million, through adjustments nobody could tie back to individual orders. “It’s only 0.7%,” someone said. “On a quarter, 0.7% is a restatement,” Farah said, and wrote it down.
The stores dashboard showed 97% stock availability. Three regional managers had photos of empty shelves on their phones.
The product team reported 6.2% online conversion. Marketing’s dashboard, built from the same clickstream, said 5.3%, and marketing had already asked for budget to fix the drop.
Rohan Desai built Larkspur’s order and payment services, knows Postgres, Kafka and idempotent consumers, and has never been paged for a dashboard. That afternoon the CTO’s request landed in Rohan’s inbox: “take a look at the queries, they’re probably just wrong.”
Rohan read them. They were mostly correct SQL. They ran against read replicas, nightly WHERE updated_at > yesterday extracts, a spreadsheet of exchange rates and two notebooks nobody would admit to owning. Each one answered a slightly different question, and none of them could say which records it had counted.
Rohan took the list to Ingrid Solberg, the principal data engineer who had just joined from adtech. Ingrid looked at it for a minute.
“These queries aren’t wrong. They’re unaccountable. Fixing them won’t help. You need a platform where every number can be traced back to the records it came from.” Then the line the whole team would hear many more times: “A number you can’t reconcile is just a rumour.”
Who this series is for
You’re a senior backend engineer. You’ve spent eight to fifteen years on services in Java and Spring, Go or Node. You know PostgreSQL’s isolation levels, Kafka’s consumer groups, REST, Kubernetes, and why retries need idempotency keys. You’ve never owned an analytics platform.
Maybe you’re preparing for a system design interview where the prompt is “design a reporting system”. Maybe you’re moving into a data platform team, or your product needs data features and the data team is three quarters behind. In each case the gap is the same: you’re an expert in distributed systems and a beginner in analytics, and many of your instincts point the wrong way.
The series treats you as both. It won’t explain what a transaction is. It will explain why a columnar file can skip most of a table without a B-tree index, why “just add an updated_at column” loses data, and why the answer to “how many sessions were there?” depends on a definition someone has to own.
Demand for these skills is real, though the figures vary with who counts. One analysis of about 110,000 data engineering job postings (huntr, July 2026) found Python in 74% and SQL in 67% of them. Treat that as direction, not precision.
What you’ll be able to do
| After | You can |
|---|---|
| Season 1 (Parts 0–14) | Explain warehouses, lakes and lakehouses and choose between them; model data so numbers don’t double-count; pick file formats, table formats, catalogs and partitioning for a given query pattern; size and defend a storage-and-compute design in an interview. |
| Season 2 (Parts 15–25) | Build ingestion, CDC, transformation, orchestration, testing and contracts that survive re-runs and late data; prove completeness with reconciliation against an outside source of truth; handle privacy, cost and observability; walk through a full batch platform design. |
| Season 3 (Parts 26–35) | Reason about event time, watermarks and exactly-once; build streaming pipelines on Kafka and Flink; serve consistent metrics; prepare data for ML and RAG; answer an end-to-end data system design question with a repeatable method. |
The thread through all three seasons is interview-grade design confidence: given a prompt with streaming sources, static sources, years of history and an accuracy requirement, you’ll know which questions to ask and which choices to defend.
Prerequisites
| You should know | You don’t need |
|---|---|
SQL joins and GROUP BY; a little Python | Spark, Airflow, dbt, or any data tool |
| Docker and Compose | A cloud account: everything runs locally |
| Kafka producers and consumers (Season 3) | Statistics or machine learning |
How each part is built
Every part follows the same order, so you can skip to what you need:
- Skim Card: the thesis and claims in under a minute, with the versions checked.
- Story Opening: a Larkspur problem where a backend instinct misleads.
- The Quick Map: Backend instinct → Data engineering reality → Why it matters.
- Concepts, with use cases: definition, what breaks without it, Larkspur, another industry.
- Under the Hood: the mechanism.
- Hands-On: runnable code with real output.
- The Landscape: open source, AWS, Google Cloud, Azure, Snowflake, Databricks.
- Tips & Gotchas and, in hands-on-heavy parts, Troubleshooting.
- Design Drill: an interview prompt from another industry, with a model answer outline.
- Further Reading and the Story Closing.
Parts 14, 25 and 35 are design reviews: full interview walkthroughs with sizing, a checklist and a second worked example.
Anything that isn’t generally available is labelled inline: (preview), (experimental), (incubating). Vendors rename products often; the posts use the names in force when each part was written and mention old names only as “formerly”.
The map of data engineering in 2026
Every data platform moves records from the systems that produce them to the people and programs that consume them. The stages have stable names even when the tools change:
Parquet files")] TF["Table format
Iceberg"] CAT["Catalog
Polaris"] end subgraph PROC["Processing"] SQLT["SQL transformations"] DIST["Distributed batch
Spark"] STR["Stream processing
Flink"] end subgraph SERVE["Serving"] WH["Query engines
and warehouses"] SEM["Semantic layer"] RT["Real-time OLAP"] FEAT["Features and vectors"] end subgraph USE["Consumers"] BI["Dashboards"] APPS["Applications"] ML["ML and AI"] end SRC --> ING --> STORE --> PROC --> SERVE --> USE PROC --> STORE X["Cross-cutting: orchestration · quality and contracts · governance and security · observability and lineage · cost"] -.- STORE
You already know the left edge (you build the sources) and part of the right edge (your services call APIs and caches). The middle is where your instincts mislead you: storage built for scanning instead of point lookups, transformations that must be re-runnable over years of history, and “correct” meaning “matches the outside world”, not “the transaction committed”.
Where each part lands:
| Stage | Parts |
|---|---|
| Foundations and mental models | 1 Three Houses for Data · 2 Think in Pipelines |
| Storage: files, tables, catalogs, layout | 3 Bytes on Disk · 7 The Table That Remembers · 8 Format Wars, Ceasefire · 9 Who Owns the Truth · 10 Where the Bytes Live |
| Modelling | 4 Grain Before Glory · 5 Other Shapes · 6 SQL You Will Actually Use |
| Processing engines | 11 You Might Not Need a Cluster · 12 Shuffle, Spill and Skew · 13 Engines at Scale |
| Ingestion | 15 Getting Data In · 16 Changes, Not Snapshots · 27 The Backbone |
| Transformation and orchestration | 17 Transformations as Code · 18 dbt at Scale · 19 Who Runs What, When |
| Quality, completeness, observability | 20 Trust, but Test · 21 Every Record Counts · 22 Know Before They Do |
| Governance and cost | 23 Locks on the Lake · 24 The Bill Always Comes |
| Streaming | 26 Time Is a Lie · 28 Streaming SQL · 29 Stateful Streams in Java · 30 The Streaming Lakehouse |
| Serving and AI | 31 Last Mile · 32 Feeding the Models · 33 Context Is the Product |
| Platform and organisation | 34 Platform, Not Pipelines |
| Design reviews | 14 Storage and Compute · 25 The Batch Platform · 35 The Whole Platform, and the Interview Playbook |
Larkspur
Larkspur is an omnichannel retailer and marketplace in India, the UK and Singapore: about 600 stores, an app and a website, and a marketplace of third-party sellers. It’s fictional, but it produces almost every kind of data a data engineer meets, which is why the series uses it.
| Source | Shape | Arrives as | Teaches |
|---|---|---|---|
| Store POS transactions | high-volume events | Kafka stream (Season 3); landed JSON files until then | streaming ingestion, dedup, late data from offline tills |
| App and web clickstream | very high volume, semi-structured JSON | Kafka stream; landed files until then | schema drift, sessionization, bots |
| Orders, payments, refunds | OLTP rows in PostgreSQL | CDC (Season 2); Parquet extracts until then | upserts, deletes, MERGE, reconciliation to the payment ledger |
| Product catalogue and prices | OLTP, prices change | daily extract | slowly changing dimensions, point-in-time joins |
| Promotions | reference data, edited after launch | admin export | as-of joins |
| Inventory | daily snapshot per store and SKU | warehouse system files | periodic snapshot facts, semi-additive measures |
| Supplier feeds | messy CSV and XLSX | files dropped in object storage | file ingestion, contracts with outsiders |
| Marketplace sellers | paginated, rate-limited REST API | API pulls (Season 2) | incremental extraction, idempotent loads |
| Delivery GPS and cold-chain sensors | IoT telemetry | MQTT to Kafka (Season 3) | event time, windows, out-of-order data |
| Support tickets and reviews | unstructured text | export (Season 3) | chunking, embeddings, RAG ingestion |
| FX rates | daily reference | public API, simulated | conversion as of the transaction date |
| PSP settlement files | daily CSV per country | files from the payment provider | the external source of truth for money |
Until Season 2 adds databases and APIs and Season 3 adds Kafka, a generator writes everything Season 1 needs as landed raw files, standing in for what ingestion will later produce. The same seed produces the same business events in every mode, so numbers carry over between seasons.
The people:
- Rohan Desai, staff backend engineer, moved to the new Data Platform team after the review. Your stand-in.
- Ingrid Solberg, principal data engineer, ex-adtech, has run petabyte lakes and been paged for “the dashboard is wrong” more often than anyone can count.
- Farah Siddiqui, finance controller, who asks the uncomfortable questions: “why does yesterday’s revenue keep changing?”
Three numbers, two answers each
The incident isn’t hand-waved. The companion lab generates four months of Larkspur data (1 June to 4 October 2026) with documented defects: things real systems do, injected deliberately, each with a count and an exact effect on the numbers. Here’s what the review was arguing about, computed from the raw files.
UK net revenue, Q3 2026. The board deck’s query and the correct one, both plain SQL over the same files, in pence:
-- sql/incident/uk_revenue_legacy.sql┌──────────────────────┐│ uk_net_revenue_minor ││ decimal(38,0) │├──────────────────────┤│ 2999923243 │└──────────────────────┘
-- sql/incident/uk_revenue_correct.sql┌──────────────────────┐│ uk_net_revenue_minor ││ int64 │├──────────────────────┤│ 2979067406 │└──────────────────────┘£29,999,232.43 against £29,790,674.06: the board deck is 0.70% above the figure Farah could support with the settlement files. Larkspur UK is cashless, so every UK payment passes through the payment service provider (PSP), and its daily files are the outside check. They don’t equal net revenue line for line: fees, chargebacks and settlement delays all differ legitimately. Separating those legitimate differences from real bugs, record by record, is the subject of Part 21. The bugs behind the £208,558.37:
| Defect | Removes | Explained in |
|---|---|---|
| A till that loses its connection re-sends buffered baskets with new event IDs | £118,076.98 | Parts 2, 6, 21 |
Cancelled online orders are hard-deleted; the nightly updated_at poller saw them created and never saw them go | £57,064.96 | Part 16 |
| Refunds are dated by the original purchase, so a July refund of a June sale vanishes from Q3 | £42,511.91 | Part 4 |
| Euro orders converted at “today’s” rate, not the rate on the day of the sale | −£8,797.38 | Part 5 |
| The quarter is cut at UTC midnight, not London midnight | −£298.10 | Part 5 |
The first three inflate the number and the last two pull it down slightly; together they’re exactly £208,558.37. Reconciliation means finding all five, not just the big one.
Stock availability, Q3 2026. The dashboard’s number, the same snapshots counted per day, and the share of store-SKU-days where the shelf had stock at closing:
-- sql/part00/stock_availability.sql┌───────────────┬──────────────────────┬────────────────────┐│ dashboard_pct │ per_day_snapshot_pct │ shelf_at_close_pct ││ double │ double │ double │├───────────────┼──────────────────────┼────────────────────┤│ 97.2 │ 91.1 │ 88.5 │└───────────────┴──────────────────────┴────────────────────┘The dashboard adds up on-hand quantities across each week and calls any week with stock “available”. Stock on hand is a balance, not a flow; adding balances across days answers no question anyone asked, and counting per day instead drops the figure from 97.2% to 91.1%. Then 216 of the 600 stores take their stock snapshot at 06:00, after the overnight delivery, so their snapshot shows stock that wasn’t on the shelf the evening before; correcting for that gives 88.5%. Part 4 covers the first problem, Part 5 the second. Banks meet the same trap with account balances: the average daily balance is a meaningful number, the sum of a month’s daily balances is not.
Online conversion, Q3 2026. Same events, two definitions of a session:
-- sql/part00/conversion.sql┌───────────┬──────────┬────────────────┐│ team │ sessions │ conversion_pct ││ varchar │ int64 │ double │├───────────┼──────────┼────────────────┤│ product │ 2161516 │ 6.21 ││ marketing │ 2525785 │ 5.31 │└───────────┴──────────┴────────────────┘The product team starts a new session after 30 minutes of inactivity and filters bots by user agent. Marketing counts one session per visitor per UTC day and keeps every hit, including 474,085 crawler requests that each arrive as a new visitor. Neither query has a SQL bug. They encode different definitions, including whether a bot is a visitor, and nobody owns the definition of a session. Streaming services fight the same battle over what counts as a “view”. Part 6 sessionizes properly, Part 20 writes the definition into a data contract, and Part 31 puts one metric definition in a semantic layer.
Notice the instincts behind each gap, because they’re yours too. The event ID looked like the key, so re-sent baskets counted twice; transit card readers that re-send taps after an outage cause the same double count. A delete leaves nothing behind for an updated_at poller to find. A stock count looked like something you can add up the way you add up sales. And “session” seemed too obvious to need a definition.
Every defect, with its count and effect, is listed in docs/injected-defects.md in the companion folder, and make test checks these queries against the generator’s manifest.
Running the companion lab
The code lives in the every-record-counts folder of the companion-workspaces repository, and each part is tagged every-record-counts/part-<N>.
| Tool | Version | Used for |
|---|---|---|
| Docker Desktop, or another engine with Compose v2 | recent, arm64 or amd64 | the services |
| uv | 0.13 or later | Python 3.13 and every Python package |
make | any | the targets |
Start the core profile: S3-compatible object storage (SeaweedFS), the Iceberg REST catalog (Apache Polaris) and PostgreSQL 18. From a clean machine it takes about 15 seconds after the images are pulled:
make up-coreIts output, with the Creating, Starting and Waiting progress lines left out:
docker compose --profile core up -d --wait seaweedfs postgres polaris Container every-record-counts-postgres-1 Healthy Container every-record-counts-polaris-bootstrap-1 Exited Container every-record-counts-seaweedfs-1 Healthy Container every-record-counts-polaris-bootstrap-1 Exited Container every-record-counts-postgres-1 Healthy Container every-record-counts-seaweedfs-1 Healthy Container every-record-counts-polaris-1 Healthydocker compose --profile core run --rm --no-deps polaris-setupcatalog larkspur createdcatalog_admin can manage content in larkspurThe bootstrap job writes Polaris’s root credentials into PostgreSQL and exits; the setup job creates the larkspur catalog. Neither stays running, which is why make waits only for the three long-running services.
Generate the raw zone and copy it into the larkspur-raw bucket:
make datamake upload[ 0.0s] reference data[ 0.1s] sales[ 6.1s] POS events and the UK revenue calibration[ 7.2s] inventory[ 10.0s] suppliers[ 10.0s] clickstream[ 27.2s] PSP settlements[ 27.6s] writing data/raw[ 57.5s] done: UK Q3 net revenue legacy £29,999,232.43 vs correct £29,790,674.06 (+0.700%)uploaded 3458 files, 623 MB to s3://larkspur-raw/raw/ in 6sThen reproduce the incident. scripts/duck.py opens DuckDB, points a set of views at the bucket and runs SQL files, printing each result (the outputs above, with the timing lines trimmed):
uv run python scripts/duck.py sql/incident/uk_revenue_legacy.sql sql/incident/uk_revenue_correct.sql \ sql/part00/stock_availability.sql sql/part00/conversion.sqlThe views it creates live in sql/raw/views.sql. Two lines in that file deserve a note now, because they catch everyone once:
SET TimeZone = 'UTC'; -- otherwise ::DATE on a TIMESTAMPTZ uses your laptop's zone
CREATE OR REPLACE VIEW raw_pos_events ASSELECT * FROM read_json(getvariable('raw_root') || '/pos/events/*/*.jsonl.gz', hive_partitioning = true);DuckDB’s session time zone defaults to your machine’s, so on a laptop in Bengaluru the same query would cut UTC days at 18:30. And read_json over a glob turns a folder of gzipped files into a table, with upload_date taken from the folder names. Part 3 explains why that convenience isn’t a table in the transactional sense.
make test runs the generator’s tests and the Part 0 checks (they skip themselves if the stack is down):
......... [100%]9 passed in 19.02sProfiles and memory. Services are grouped into Compose profiles, and each part says which ones to start. Every long-running service has a memory limit, and the JVM services size their heaps from it.
| Profile | Services | First used | Memory limits |
|---|---|---|---|
core | SeaweedFS, Polaris, PostgreSQL 18 | Part 0 | SeaweedFS 1.5 GB, Polaris and PostgreSQL 1 GB |
batch | Trino, Spark | Part 1 (Trino), Part 7 (Spark) | 3 GB, 2.5 GB |
stream | Kafka, schema registry, Kafka Connect, Flink | Part 16 | added in Season 2 |
orchestrate, serve, governance | Airflow, BI and OLAP, policy engine | Seasons 2–3 | added when needed |
The core profile peaked at 0.86 GB while the incident queries ran (DuckDB runs on your machine, outside Docker). The lab was measured on a MacBook Pro M3 Pro with a 9.8 GB Docker VM, and every part lists its measured peak.
Tip — Check how much memory your Docker VM has (
docker info --format '{{.MemTotal}}') before starting a heavy profile; other projects’ containers count against it. The lab’s budget assumes you stop what you don’t need.
Glossary seed
Short definitions now; each term links to (or names) the part that explains it properly. Later parts add to this list.
| Term | Meaning | Part |
|---|---|---|
| OLTP | Systems for many small reads and writes of current state: your services’ databases | 1 |
| OLAP | Systems for scanning and aggregating large amounts of history | 1 |
| Data warehouse | A managed analytical database: storage, compute and file format owned by one system | 1, 13 |
| Data lake | Files in cheap storage, readable by any engine, with no transactional tables | 1 |
| Lakehouse | A lake plus an open table format and catalog, giving warehouse features over open files | 1 |
| Columnar storage | Storing each column’s values together so a query reads only the columns it needs | 1, 3 |
| Parquet | The standard open columnar file format for analytics | 3 |
| Row group | A block of rows inside a Parquet file, stored column by column, with min/max statistics in the footer | 3 |
| Predicate pushdown | Skipping row groups (or files) whose statistics show they can’t match a filter | 3, 10 |
| Apache Arrow | The standard in-memory columnar format engines and languages exchange data in | 3, 11 |
| Conditional write | An object-storage write that succeeds only if the object is absent or unchanged; the primitive behind table commits | 3, 7 |
| Table format | Metadata that turns a set of files into a table with snapshots and atomic commits (Iceberg, Delta, Hudi) | 7, 8 |
| Catalog | The service that knows which metadata file is a table’s current version, and who may read it | 9 |
| Snapshot | One committed version of a table; time travel reads an older one | 7 |
| Manifest | An Iceberg metadata file listing data or delete files with their partition values and column statistics | 7 |
| Optimistic concurrency | Write first, then swap the table’s pointer only if nobody committed meanwhile; otherwise re-check and retry or fail | 7 |
| Copy-on-write / merge-on-read | Rewrite every file holding a changed row, or write the change plus a delete file and merge at read time | 7, 10 |
| Deletion vector | A per-data-file bitmap of deleted rows (Iceberg format v3, in a Puffin file) | 7 |
| Hidden partitioning | Partitions defined as transforms of real columns (such as days(event_ts)), pruned without a partition column in the query | 7, 10 |
| Tag / branch | A named snapshot kept for reproducibility / a named line of snapshots that can be written apart from main | 7, 20 |
| Transaction log (Delta) | Delta Lake’s metadata: numbered JSON commits in _delta_log/, folded into Parquet checkpoints | 8 |
| Data inlining | DuckLake storing small inserts and deletes as rows in its catalog database until flushed to Parquet | 8 |
| UniForm / metadata translation | Writing a second format’s metadata over the same data files so other engines can read them (read-only) | 8 |
| Iceberg REST catalog | The HTTP protocol (an OpenAPI spec) every engine uses to list, load and commit Iceberg tables; the server owns the logic | 9 |
| Principal / principal role / catalog role | Polaris’s access model: identities hold job-function roles, which hold per-catalog roles, which hold privileges | 9 |
| Credential vending | A catalog handing an engine short-lived storage keys scoped to one table, minted through STS | 9 |
| Partitioning | Splitting a table’s files by a value (such as day) so queries can skip whole groups | 10 |
| Clustering (sort order, Z-order) | Ordering rows within partitions so file and row-group statistics can skip data | 10 |
| Compaction | Rewriting many small files into few target-sized ones; old files go at snapshot expiry | 10 |
| Vectorised execution | Operators process batches of column values at a time, not one row at a time | 11 |
| Out-of-core processing | An operator spills state to disk at its memory limit instead of failing | 11, 12 |
| Shuffle | Redistributing rows between tasks by key so equal keys meet; disk, serialization, network | 12 |
| Data skew (salting) | One key holding far more rows than others; salting spreads it over sub-keys | 12 |
| Grain | What exactly one row of a table represents | 4 |
| Fact table | Measurements of business events at a declared grain | 4 |
| Dimension | Descriptive context for facts: product, store, date | 4 |
| Semi-additive measure | Can be summed across some dimensions but not time (stock on hand, account balance) | 4 |
| Slowly changing dimension (SCD) | A dimension that keeps history when attributes change | 4 |
| Star schema | A fact table joined to denormalised dimension tables, one key per dimension | 4 |
| Surrogate key | A warehouse-assigned integer key; one natural key can have several rows over time | 4 |
| Conformed dimension | A dimension with the same keys and values in every fact table that uses it | 4 |
| As-of join | Joining each row to the latest version at or before its timestamp | 4, 28 |
| Early-arriving fact | A fact whose dimension row doesn’t exist yet; it points at a placeholder row | 4 |
| Business date | The date an event belongs to in its local time zone, not in UTC | 5 |
| Data Vault | An insert-only integration model of hubs (business keys), links (relationships) and satellites (attribute history) | 5 |
| Wide table (one big table) | A pre-joined, denormalised table shaped for one set of questions, viable on columnar engines | 5 |
| Bitemporal data | Records with two times: when something was true (valid time) and when the system learned it (transaction time) | 5, 21 |
| Window function | A value per row computed from related rows (partition, order, frame) without collapsing them | 6 |
| Gaps and islands | Finding runs of consecutive values, such as days out of stock, with date - row_number() | 6 |
| Sessionization | Grouping a visitor’s events into visits by a rule, usually an inactivity gap | 6 |
| HyperLogLog | A small, mergeable sketch that estimates distinct counts within a few percent | 6 |
| High-water mark | The largest value an incremental load has read; the next run reads only above it | 6 |
| Idempotent pipeline | Re-running it for the same input gives the same output, never doubled data | 2 |
| Backfill | Re-processing past periods, usually after a fix or a new column | 2, 19 |
| Partition-scoped overwrite | A run deletes and rewrites exactly the partitions it owns, atomically, instead of appending | 2, 12 |
| Dedup key | The business identity of a record (a basket, not a message), used to count it once | 2, 21 |
| Lateness window | How long after a business date its data may still arrive; days inside it are provisional | 2, 18 |
| Five promises | Completeness, uniqueness, correctness, timeliness, consistency: how numbers fail and what each part protects | 2 |
| Change data capture (CDC) | Reading a database’s log to get every insert, update and delete | 16 |
| ETL / ELT | Transform before loading vs load raw, then transform inside the platform | 2 |
| Medallion (bronze, silver, gold) | Raw, cleaned and business-ready layers of a lakehouse | 2 |
| Data contract | An agreed, versioned schema and set of guarantees between a producer and its consumers | 20 |
| Reconciliation | Proving two systems agree, record by record, or explaining every difference | 21 |
| Lineage | Which sources and jobs produced a given table or number | 22 |
| Event time / processing time | When something happened vs when the system saw it | 26 |
| Watermark | A stream processor’s estimate that no older events are still coming | 26 |
| Semantic layer | One shared definition of each metric, served to every tool | 31 |
The question this series teaches you to answer
Interviews for data and platform roles keep returning to one kind of prompt. Here’s the version this series is built to answer. Part 35 works through it in full, and every Design Drill on the way is a smaller piece of it.
“A metropolitan transit authority wants a ridership platform. Card readers at station gates and on 4,000 buses report taps, the buses also send GPS pings every ten seconds, and 300 stations have footfall sensors. Planners combine all of that with the timetable and route network, which changes every quarter. They want ridership by route, stop and hour that counts every tap exactly once, kept for seven years, and operations wants yesterday’s figures by 6 a.m. Walk me through the design, and tell me how you’d prove nothing was lost or counted twice.”
A strong answer clarifies before it designs (how late can a bus’s taps arrive? what is a “trip” when a passenger changes buses? how does the timetable change mid-quarter?), estimates volumes, declares the grain of each table, picks storage, a table format and partitioning for how the data will be queried, and separates event time from arrival time. It removes taps that readers re-send after an outage using a declared key, not the message ID. It re-processes late taps inside a window that still meets the 6 a.m. deadline. And it proves completeness by reconciling against a source that didn’t come through the same pipeline, such as each reader’s own tap counter or the fare-clearing house’s daily totals. If that list reads like jargon today, the series is for you. By Part 14 you’ll be able to defend the storage half, by Part 25 the batch pipeline, and by Part 35 the whole design.
Story Closing
Rohan didn’t fix the queries. The rest of the week went on running each one, logging the files it read, and writing one paragraph per number explaining why the answers differed. By Friday the stock and conversion gaps had explanations. The revenue gap could only be narrowed down: duplicates, probably; deletes, maybe; something with refunds.
Ingrid read the paragraphs and nodded at the revenue one. “Good. You’ve found out that you can’t find out. That’s where the job starts.”
Ingrid drew three boxes on the whiteboard and labelled them warehouse, lake and lakehouse. “Before we argue about tools, you need to know why your Postgres instincts are going to fight you. Tomorrow you’ll ask the same question three ways and measure the difference.”
In Part 1, Rohan answers one revenue question with PostgreSQL, with DuckDB over a folder of Parquet files, and with Trino over an Iceberg table, and learns why analytical systems store data the way they do.
This is Part 0 of “Every Record Counts: Data Engineering for Backend Engineers” — Season 1: Foundations & the Lakehouse.