Chapter 93 — Data Engineer Roadmap
📖 Definition
A data engineer builds the pipelines and models that every analyst, dashboard, and AI feature depends on. You own movement and shape of data: ingestion, transformation, storage, freshness, and trust.
🔍 Market Reality
- Steadiest demand of the six roadmaps, and the most transferable across industries.
- Every company with AI ambitions discovers its data layer first. Half of "AI projects" are data projects wearing a costume.
- Strong path in from SQL analyst or backend work. Low maths requirement.
- Titles: data engineer, analytics engineer (dbt-heavy, closer to the business), platform data engineer (infra-heavy).
✅ Before Stage 1
| Prerequisite | Why it is needed |
|---|---|
SQL beyond SELECT |
The core language of the job |
| Python scripting | Glue, orchestration, and tests |
| Linux command line | Everything runs on a remote box |
| Git | Pipelines are code and get reviewed |
🗺️ The Roadmap
Stage 1 — SQL and data modelling (4-6 weeks)
Goal: model data so the next question is cheap to answer.
| Learn | Build |
|---|---|
| Joins, CTEs, window functions | A star schema over a public dataset, with the ten business questions it answers in one query each |
| Indexes and query plans | |
| Normalisation vs denormalisation | |
| Star schema: facts and dimensions | |
| Slowly changing dimensions |
Chapters 38 and 41 cover the storage-side theory.
Stage 2 — Pipelines (6-8 weeks)
Goal: move data on a schedule without babysitting it.
| Learn | Build |
|---|---|
| ETL vs ELT | A scheduled pipeline with dbt tests, a backfill command, and a freshness alert that actually fires |
| Orchestration: Airflow, Dagster, or Prefect | |
| Idempotent tasks and safe reruns | |
| Incremental loads and backfills | |
| dbt models and tests | |
| Alerting on failure and on staleness |
A pipeline that fails loudly is fine. A pipeline that silently stops updating is what gets people fired.
Stage 3 — Storage and scale (6-8 weeks)
Goal: handle data too big for one machine, without a surprise cloud bill.
| Learn | Build |
|---|---|
| Warehouses: BigQuery, Snowflake, Redshift | The same query on unpartitioned and partitioned data, with the cost and runtime difference measured |
| Columnar formats: Parquet | |
| Lakehouse tables: Iceberg or Delta | |
| Partitioning and clustering | |
| Spark fundamentals | |
| Query cost control |
Stage 4 — Streaming (4-6 weeks)
Goal: some questions cannot wait for tomorrow's batch.
| Learn | Build |
|---|---|
| Kafka: topics, partitions, consumer groups | A CDC stream from Postgres to the warehouse with a measured end-to-end lag figure |
| Delivery semantics and duplicates | |
| Change data capture with Debezium | |
| Windowing and late-arriving data | |
| Flink or Spark Streaming basics |
Stage 5 — Reliability and governance (4-6 weeks)
Goal: being trusted is the job. Wrong data is worse than no data.
| Learn | Build |
|---|---|
| Data contracts between producers and consumers | A data contract plus quality checks that block a bad upstream change before it reaches a dashboard |
| Quality checks and anomaly detection | |
| Lineage and impact analysis | |
| PII classification and access control | |
| On-call runbooks and incident write-ups |
💻 Code Example — Idempotent Incremental Load (Stage 2)
The pattern behind most production dbt models: process only what changed, and make a rerun a no-op instead of a duplicate.
-- models/marts/fct_orders.sql
{{ config(
materialized = 'incremental',
unique_key = 'order_id',
incremental_strategy = 'merge',
partition_by = { 'field': 'ordered_at', 'data_type': 'timestamp', 'granularity': 'day' }
) }}
with source as (
select *
from {{ ref('stg_orders') }}
{% if is_incremental() %}
-- A lookback window, not "> max(ordered_at)": late-arriving rows would
-- otherwise be skipped forever.
where updated_at >= (select coalesce(max(updated_at), '1900-01-01') from {{ this }})
- interval '3' day
{% endif %}
)
select
order_id,
customer_id,
ordered_at,
updated_at,
sum(line_amount) as order_amount
from source
group by 1, 2, 3, 4# models/marts/schema.yml — the tests are the deliverable, not an extra
models:
- name: fct_orders
columns:
- name: order_id
tests: [unique, not_null]
- name: customer_id
tests:
- not_null
- relationships: { to: ref('dim_customers'), field: customer_id }
tests:
- dbt_utils.recency:
datepart: hour
field: ordered_at
interval: 26 # freshness SLA: alert if no new order in 26 hoursThree interview-grade decisions are visible: merge on a unique key for idempotency, a lookback window for late data, and a freshness test so silence is treated as failure.
🧰 Tools on the CV
SQL and Python · dbt · Airflow or Dagster · one warehouse (BigQuery or Snowflake) · Kafka · Spark · Terraform basics
📁 Portfolio That Gets Replies
- One pipeline running on a schedule that you did not have to fix by hand.
- Tests and a freshness SLA someone else could rely on.
- A cost or runtime optimisation with numbers ("partitioning cut the daily scan from 1.2 TB to 40 GB").
🌍 Real-World Uses
- Reporting layer — raw events into a warehouse, modelled into facts and dimensions the business queries directly.
- AI feature plumbing — documents, embeddings, and metadata kept fresh for a RAG index.
- Operational CDC — replicating a production database into analytics without touching production load.
- Reverse ETL — pushing modelled segments back into CRM and marketing tools.
🎯 Likely Interview Questions
- An upstream team renamed a column overnight. How does your pipeline behave, and who finds out first? — Schema test fails, the model does not publish, an alert fires to you before a dashboard shows wrong numbers. A data contract prevents the rename from shipping silently.
- ETL or ELT, and why? — ELT for warehouses: land raw, transform in-warehouse with version-controlled SQL, keep the raw layer replayable. ETL when the target cannot transform or when PII must be dropped before landing.
- A dashboard shows yesterday's numbers. Walk me through the debug. — Freshness check first, then the orchestrator run history, then the source's own lag, then the incremental filter — that last one is the usual culprit.
- How do you make a task safe to rerun? — Deterministic output for the same input window: merge on a unique key or delete-and-insert the partition. Never blind
INSERT. - Same query, ten times the cost this month. Why? — Partition or cluster pruning stopped working: a wildcard filter, a function on the partition column, or
SELECT *over a widened table. - How do you handle late-arriving data? — A lookback window on the incremental filter plus an event-time watermark, and idempotent merges so reprocessing is free.
← Forward Deployed Engineer Roadmap | Index | Next: MLOps / AI Platform Roadmap →