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

AfterYou 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 knowYou don’t need
SQL joins and GROUP BY; a little PythonSpark, Airflow, dbt, or any data tool
Docker and ComposeA 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:

  1. Skim Card: the thesis and claims in under a minute, with the versions checked.
  2. Story Opening: a Larkspur problem where a backend instinct misleads.
  3. The Quick Map: Backend instinct → Data engineering reality → Why it matters.
  4. Concepts, with use cases: definition, what breaks without it, Larkspur, another industry.
  5. Under the Hood: the mechanism.
  6. Hands-On: runnable code with real output.
  7. The Landscape: open source, AWS, Google Cloud, Azure, Snowflake, Databricks.
  8. Tips & Gotchas and, in hands-on-heavy parts, Troubleshooting.
  9. Design Drill: an interview prompt from another industry, with a model answer outline.
  10. 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:

graph LR subgraph SRC["Sources"] OLTP[("OLTP databases")] EV["Event streams"] FILES["Files and partner feeds"] API["SaaS and partner APIs"] end subgraph ING["Ingestion"] BATCH["Batch extract"] CDC["Change data capture"] STREAM["Stream ingest"] end subgraph STORE["Storage"] OBJ[("Object storage
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:

StageParts
Foundations and mental models1 Three Houses for Data · 2 Think in Pipelines
Storage: files, tables, catalogs, layout3 Bytes on Disk · 7 The Table That Remembers · 8 Format Wars, Ceasefire · 9 Who Owns the Truth · 10 Where the Bytes Live
Modelling4 Grain Before Glory · 5 Other Shapes · 6 SQL You Will Actually Use
Processing engines11 You Might Not Need a Cluster · 12 Shuffle, Spill and Skew · 13 Engines at Scale
Ingestion15 Getting Data In · 16 Changes, Not Snapshots · 27 The Backbone
Transformation and orchestration17 Transformations as Code · 18 dbt at Scale · 19 Who Runs What, When
Quality, completeness, observability20 Trust, but Test · 21 Every Record Counts · 22 Know Before They Do
Governance and cost23 Locks on the Lake · 24 The Bill Always Comes
Streaming26 Time Is a Lie · 28 Streaming SQL · 29 Stateful Streams in Java · 30 The Streaming Lakehouse
Serving and AI31 Last Mile · 32 Feeding the Models · 33 Context Is the Product
Platform and organisation34 Platform, Not Pipelines
Design reviews14 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.

SourceShapeArrives asTeaches
Store POS transactionshigh-volume eventsKafka stream (Season 3); landed JSON files until thenstreaming ingestion, dedup, late data from offline tills
App and web clickstreamvery high volume, semi-structured JSONKafka stream; landed files until thenschema drift, sessionization, bots
Orders, payments, refundsOLTP rows in PostgreSQLCDC (Season 2); Parquet extracts until thenupserts, deletes, MERGE, reconciliation to the payment ledger
Product catalogue and pricesOLTP, prices changedaily extractslowly changing dimensions, point-in-time joins
Promotionsreference data, edited after launchadmin exportas-of joins
Inventorydaily snapshot per store and SKUwarehouse system filesperiodic snapshot facts, semi-additive measures
Supplier feedsmessy CSV and XLSXfiles dropped in object storagefile ingestion, contracts with outsiders
Marketplace sellerspaginated, rate-limited REST APIAPI pulls (Season 2)incremental extraction, idempotent loads
Delivery GPS and cold-chain sensorsIoT telemetryMQTT to Kafka (Season 3)event time, windows, out-of-order data
Support tickets and reviewsunstructured textexport (Season 3)chunking, embeddings, RAG ingestion
FX ratesdaily referencepublic API, simulatedconversion as of the transaction date
PSP settlement filesdaily CSV per countryfiles from the payment providerthe 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:

DefectRemovesExplained in
A till that loses its connection re-sends buffered baskets with new event IDs£118,076.98Parts 2, 6, 21
Cancelled online orders are hard-deleted; the nightly updated_at poller saw them created and never saw them go£57,064.96Part 16
Refunds are dated by the original purchase, so a July refund of a June sale vanishes from Q3£42,511.91Part 4
Euro orders converted at “today’s” rate, not the rate on the day of the sale−£8,797.38Part 5
The quarter is cut at UTC midnight, not London midnight−£298.10Part 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>.

ToolVersionUsed for
Docker Desktop, or another engine with Compose v2recent, arm64 or amd64the services
uv0.13 or laterPython 3.13 and every Python package
makeanythe 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:

Terminal window
make up-core

Its 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 Healthy
docker compose --profile core run --rm --no-deps polaris-setup
catalog larkspur created
catalog_admin can manage content in larkspur

The 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:

Terminal window
make data
make 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 6s

Then 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):

Terminal window
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.sql

The 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 AS
SELECT * 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.02s

Profiles 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.

ProfileServicesFirst usedMemory limits
coreSeaweedFS, Polaris, PostgreSQL 18Part 0SeaweedFS 1.5 GB, Polaris and PostgreSQL 1 GB
batchTrino, SparkPart 1 (Trino), Part 7 (Spark)3 GB, 2.5 GB
streamKafka, schema registry, Kafka Connect, FlinkPart 16added in Season 2
orchestrate, serve, governanceAirflow, BI and OLAP, policy engineSeasons 2–3added 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.

TermMeaningPart
OLTPSystems for many small reads and writes of current state: your services’ databases1
OLAPSystems for scanning and aggregating large amounts of history1
Data warehouseA managed analytical database: storage, compute and file format owned by one system1, 13
Data lakeFiles in cheap storage, readable by any engine, with no transactional tables1
LakehouseA lake plus an open table format and catalog, giving warehouse features over open files1
Columnar storageStoring each column’s values together so a query reads only the columns it needs1, 3
ParquetThe standard open columnar file format for analytics3
Row groupA block of rows inside a Parquet file, stored column by column, with min/max statistics in the footer3
Predicate pushdownSkipping row groups (or files) whose statistics show they can’t match a filter3, 10
Apache ArrowThe standard in-memory columnar format engines and languages exchange data in3, 11
Conditional writeAn object-storage write that succeeds only if the object is absent or unchanged; the primitive behind table commits3, 7
Table formatMetadata that turns a set of files into a table with snapshots and atomic commits (Iceberg, Delta, Hudi)7, 8
CatalogThe service that knows which metadata file is a table’s current version, and who may read it9
SnapshotOne committed version of a table; time travel reads an older one7
ManifestAn Iceberg metadata file listing data or delete files with their partition values and column statistics7
Optimistic concurrencyWrite first, then swap the table’s pointer only if nobody committed meanwhile; otherwise re-check and retry or fail7
Copy-on-write / merge-on-readRewrite every file holding a changed row, or write the change plus a delete file and merge at read time7, 10
Deletion vectorA per-data-file bitmap of deleted rows (Iceberg format v3, in a Puffin file)7
Hidden partitioningPartitions defined as transforms of real columns (such as days(event_ts)), pruned without a partition column in the query7, 10
Tag / branchA named snapshot kept for reproducibility / a named line of snapshots that can be written apart from main7, 20
Transaction log (Delta)Delta Lake’s metadata: numbered JSON commits in _delta_log/, folded into Parquet checkpoints8
Data inliningDuckLake storing small inserts and deletes as rows in its catalog database until flushed to Parquet8
UniForm / metadata translationWriting a second format’s metadata over the same data files so other engines can read them (read-only)8
Iceberg REST catalogThe HTTP protocol (an OpenAPI spec) every engine uses to list, load and commit Iceberg tables; the server owns the logic9
Principal / principal role / catalog rolePolaris’s access model: identities hold job-function roles, which hold per-catalog roles, which hold privileges9
Credential vendingA catalog handing an engine short-lived storage keys scoped to one table, minted through STS9
PartitioningSplitting a table’s files by a value (such as day) so queries can skip whole groups10
Clustering (sort order, Z-order)Ordering rows within partitions so file and row-group statistics can skip data10
CompactionRewriting many small files into few target-sized ones; old files go at snapshot expiry10
Vectorised executionOperators process batches of column values at a time, not one row at a time11
Out-of-core processingAn operator spills state to disk at its memory limit instead of failing11, 12
ShuffleRedistributing rows between tasks by key so equal keys meet; disk, serialization, network12
Data skew (salting)One key holding far more rows than others; salting spreads it over sub-keys12
GrainWhat exactly one row of a table represents4
Fact tableMeasurements of business events at a declared grain4
DimensionDescriptive context for facts: product, store, date4
Semi-additive measureCan 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 change4
Star schemaA fact table joined to denormalised dimension tables, one key per dimension4
Surrogate keyA warehouse-assigned integer key; one natural key can have several rows over time4
Conformed dimensionA dimension with the same keys and values in every fact table that uses it4
As-of joinJoining each row to the latest version at or before its timestamp4, 28
Early-arriving factA fact whose dimension row doesn’t exist yet; it points at a placeholder row4
Business dateThe date an event belongs to in its local time zone, not in UTC5
Data VaultAn 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 engines5
Bitemporal dataRecords with two times: when something was true (valid time) and when the system learned it (transaction time)5, 21
Window functionA value per row computed from related rows (partition, order, frame) without collapsing them6
Gaps and islandsFinding runs of consecutive values, such as days out of stock, with date - row_number()6
SessionizationGrouping a visitor’s events into visits by a rule, usually an inactivity gap6
HyperLogLogA small, mergeable sketch that estimates distinct counts within a few percent6
High-water markThe largest value an incremental load has read; the next run reads only above it6
Idempotent pipelineRe-running it for the same input gives the same output, never doubled data2
BackfillRe-processing past periods, usually after a fix or a new column2, 19
Partition-scoped overwriteA run deletes and rewrites exactly the partitions it owns, atomically, instead of appending2, 12
Dedup keyThe business identity of a record (a basket, not a message), used to count it once2, 21
Lateness windowHow long after a business date its data may still arrive; days inside it are provisional2, 18
Five promisesCompleteness, uniqueness, correctness, timeliness, consistency: how numbers fail and what each part protects2
Change data capture (CDC)Reading a database’s log to get every insert, update and delete16
ETL / ELTTransform before loading vs load raw, then transform inside the platform2
Medallion (bronze, silver, gold)Raw, cleaned and business-ready layers of a lakehouse2
Data contractAn agreed, versioned schema and set of guarantees between a producer and its consumers20
ReconciliationProving two systems agree, record by record, or explaining every difference21
LineageWhich sources and jobs produced a given table or number22
Event time / processing timeWhen something happened vs when the system saw it26
WatermarkA stream processor’s estimate that no older events are still coming26
Semantic layerOne shared definition of each metric, served to every tool31

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.