Chapter 2 — Key Takeaways (The Data Engineering Lifecycle)
The page to have open when something breaks and you do not yet know where.
The model
┌───────────┐ ┌────────┐ ┌───────────┐ ┌──────────┐
│ GENERATE │───▶│ INGEST │───▶│ TRANSFORM │─────────▶│ SERVE │
└───────────┘ └────────┘ └───────────┘ └──────────┘
│ │ │ │
╔════▼════════════════▼══════════════▼═════════════════════▼═══════╗
║ S T O R E ║
╚══════════════════════════════════════════════════════════════════╝
┌──────────────────────────────────────────────────────────────────┐
│ security │ data mgmt │ DataOps │ architecture │ orchestration │ SWE │
└──────────────────────────────────────────────────────────────────┘
Store is a substrate, not a step. Every stage reads and writes it.
| Stage | The question it owns |
|---|---|
| Generate | What does the source produce, and what does it guarantee? |
| Ingest | How does it get here, reliably and repeatably? |
| Store | Where, in what format, laid out how, for how long? |
| Transform | What does it mean, and what shape does it need? |
| Serve | How do consumers get at it, in what form? |
The diagnostic — use this first
Locate the stage before choosing a tool. Four hypotheses, four queries, ~12 minutes:
| # | Stage | Check |
|---|---|---|
| 1 | Generate | Is the source producing? (Kafka offsets / source row counts) |
| 2 | Ingest | Did it land? (COUNT(*) FROM bronze.x WHERE date = ...) |
| 3 | Transform | Where did rows disappear? (count at every layer) |
| 4 | Serve | Does the data exist but the consumer can't see it? |
The count-at-every-layer query is the highest-value five minutes in any investigation. Write it before you need it; put it in the runbook.
SELECT 'bronze' AS layer, COUNT(*) FROM bronze.x WHERE d = :dt
UNION ALL SELECT 'silver', COUNT(*) FROM silver.x WHERE d = :dt
UNION ALL SELECT 'gold', COUNT(*) FROM gold.x WHERE d = :dt;
Also: notice what is not broken. It eliminates half the tree for free.
The six questions about every source system
- What is the system of record for this fact when two systems disagree?
- What is the update pattern — append-only, mutable, or hard deletes? (hard deletes break naive ingestion completely)
- Is there a reliable change timestamp? Four ways
updated_atlies: app-maintained (misses direct SQL), assigned at transaction start, second granularity, not set on every code path. - Who can change the schema, and will you be told?
- What load can it take, and when?
- What does the data actually mean? ← nobody asks this one.
The watermark bug
t=100.0 txn A begins, updated_at = 100.0
t=100.5 txn B commits, updated_at = 100.5
t=100.6 extract reads B, sets watermark = 100.5
t=101.0 txn A commits with updated_at = 100.0 → permanently invisible
Fixes, in ascending correctness: overlap the window (turns loss into duplicates, which idempotent writes already handle) · use a commit-ordered monotonic column · use CDC (Ch. 14).
Ingestion decisions
| Decision | Options | Default |
|---|---|---|
| Cadence | batch / micro-batch / streaming | batch until measured otherwise |
| Direction | push / pull | pull for control, push for latency |
| Scope | full / incremental | full until it stops fitting the window |
| Guarantee | at-most / at-least / exactly-once | at-least-once + idempotent writes |
| Landing shape | raw / parsed | raw |
Why at-least-once + idempotent writes: it moves the hard problem from the transport layer, where
it is a distributed systems problem, to the write layer, where a DELETE solves it.
The five transformation layers
| Layer | What it does | Fails how |
|---|---|---|
| Structural | parse, cast, flatten, rename | loudly — a cast throws |
| Cleaning | dedupe, nulls, drop test rows | quietly — write down why a row was dropped |
| Conforming | make sources agree (identity stitching) | silently and expensively |
| Modeling | facts, dimensions, grain, keys | grain errors |
| Aggregating | rollups and metrics | drift between copies |
The grain trap
-- one row per order line, joined to promotions... 2 promotions = 2 rows = doubled revenue
SELECT oi.*, p.promotion_code
FROM fct_order_item oi LEFT JOIN promotions p ON p.order_id = oi.order_id;
Valid SQL. Correct values. Wrong grain. Three defenses, use all three:
- Declare the grain in the docs, in words, at the top.
- Test it —
uniqueon the declared key, in CI and production. - Assert row counts across the join.
SELECT DISTINCT is not a fix. It masks the symptom and silently collapses legitimately distinct rows.
Store — four decisions you own
System (Part II) · Format (Ch. 11) · Layout ← made by default, paid on every read · Lifecycle policy (Ch. 33, Ch. 31)
Small-files arithmetic, Kestrel clickstream at 30-second commits:
$$\frac{86{,}400}{30} \times 12 = 34{,}560\ \text{files/day} = 12.6\text{M/year at 27 KB each}$$
Full-year scan ≈ $5.05 in GET requests alone — and the latency hurts long before the bill does.
Compact to 256 MB files: ~1,332 files, same bytes.
Serve — five patterns
BI/analytics · machine learning (point-in-time correctness) · reverse ETL (inverts your risk — bugs face customers) · data as a product · self-serve.
The boundary fails in both directions, with the same symptom (dashboards disagreeing):
| Drawn | Result |
|---|---|
| Too far upstream | 41 tables, 7 definitions of revenue, 60% of capacity on maintenance |
| Too far downstream | The definition lives in 19 BI queries maintained by 5 people |
Workable: modeled star schema at the boundary + a few measured aggregates + exactly one place each metric is defined.
The six undercurrents and how each fails
| Undercurrent | Characteristic failure |
|---|---|
| Security | Superuser extraction account, still there four years later |
| Data management | Nobody can find anything; deletion requests unfulfillable |
| DataOps | Deploys from a laptop; failures found by stakeholders |
| Data architecture | Accretion — eleven systems, no decisions, all load-bearing |
| Orchestration | Cron with sleeps, hoping upstream finished |
| Software engineering | 2,000-line SQL nobody can change safely |
⚠️ Data management fails against a statutory clock. GDPR erasure: one month to respond, extendable by two for complex requests. Finding every copy is a design decision made in Part II.
Fail loudly, not plausibly
| Cost | |
|---|---|
| Loud (blank dashboard) | Hours of embarrassment |
| Plausible (default value fills in) | A month of decisions made on wrong numbers |
Choose loud. The exception is a genuinely availability-critical surface — and in analytics, you almost never are.