A data engineer builds the infrastructure that moves, transforms, and stores data so analysts and data scientists can use it. This roadmap shows you exactly what to learn, in what order, and realistic timelines to go from beginner to job-ready data engineer in 2025.
At a Glance
| Phase | Skills | Timeline |
|---|---|---|
| 1. Foundations | Linux, Git, Python basics | Months 1–2 |
| 2. SQL & Databases | Advanced SQL, data modeling, indexing | Months 2–3 |
| 3. Python for Data | pandas, PySpark, scripting ETL | Months 3–4 |
| 4. Data Warehousing | Snowflake, BigQuery, Redshift, star schema | Months 4–5 |
| 5. ETL / ELT Pipelines | batch processing, dbt, Airbyte | Months 5–6 |
| 6. Orchestration | Airflow, Prefect, Dagster | Months 6–7 |
| 7. Big Data & Spark | HDFS, Spark DataFrames, Spark SQL | Months 7–8 |
| 8. Streaming | Kafka, Flink, Spark Streaming | Months 8–10 |
| 9. Cloud Platforms | AWS/GCP/Azure data services | Months 10–12 |
| 10. DataOps | data quality, observability, CI/CD for pipelines | Months 12–14 |
Phase 1 — Foundations (Months 1–2)
Before touching data pipelines, get comfortable with the command line, version control, and the basics of Python.
Linux & Shell
Data engineers spend a lot of time in terminals — SSH-ing into servers, inspecting logs, running scripts. Learn:
- File system navigation (
ls,cd,find,grep) - File manipulation (
cp,mv,rm,cat,head,tail) - Process management (
ps,kill,top,nohup) - Text processing (
awk,sed,cut,sort,uniq,wc) - Shell scripting (
forloops, conditionals,cronfor scheduling)
Git
All pipeline code lives in Git. Learn:
git init / clone / add / commit / push / pull
git branch / checkout / merge / rebase
git log --oneline --graph
Use Conventional Commits (feat:, fix:, chore:) from day one.
Python Basics
You don't need to be a Python expert yet — just comfortable:
- Variables, types, control flow, functions
- List/dict comprehensions
- File I/O (
open,csv,json) - Exception handling (
try/except) - Virtual environments (
venv,pip, oruv)
Phase 2 — SQL & Databases (Months 2–3)
SQL is the most important skill for a data engineer. You need to be fluent, not just functional.
Core SQL
-- Aggregations + GROUP BY
SELECT customer_id, SUM(amount) AS total, COUNT(*) AS orders
FROM orders
WHERE created_at >= '2025-01-01'
GROUP BY customer_id
HAVING SUM(amount) > 1000
ORDER BY total DESC;
-- JOINs
SELECT o.id, c.name, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.id
LEFT JOIN refunds r ON o.id = r.order_id
WHERE r.id IS NULL; -- orders with no refund
Window Functions (essential for analytics)
-- Ranking + running total
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) AS running_total,
LAG(revenue, 1) OVER (ORDER BY date) AS prev_day,
ROUND(100.0 * (revenue - LAG(revenue, 1) OVER (ORDER BY date))
/ LAG(revenue, 1) OVER (ORDER BY date), 2) AS day_over_day_pct
FROM daily_revenue;
Data Modeling
| Model | Description | Use Case |
|---|---|---|
| Star Schema | fact table + dimension tables | data warehouses, BI |
| Snowflake Schema | normalized dimensions | complex hierarchies |
| One Big Table (OBT) | denormalized wide table | analytics, Spark |
| Data Vault | hub/link/satellite | auditable, historical |
Database Internals to Understand
- Indexes (B-tree, hash, composite) — know when to add them
- Query execution plans (
EXPLAIN ANALYZEin PostgreSQL) - ACID transactions and isolation levels
- N+1 problem and how to avoid it
Phase 3 — Python for Data Engineering (Months 3–4)
Now go deeper with Python focused on data tasks.
pandas for Data Manipulation
import pandas as pd
df = pd.read_csv("orders.csv", parse_dates=["created_at"])
# Clean
df = df.dropna(subset=["customer_id"])
df["amount"] = pd.to_numeric(df["amount"], errors="coerce").fillna(0)
# Transform
monthly = (
df.groupby(df["created_at"].dt.to_period("M"))
.agg(revenue=("amount", "sum"), orders=("id", "count"))
.reset_index()
)
print(monthly)
Writing ETL Scripts
A simple extract-transform-load script pattern:
import psycopg2, requests, json
def extract():
resp = requests.get("https://api.example.com/orders", timeout=30)
resp.raise_for_status()
return resp.json()
def transform(records):
return [
{
"order_id": r["id"],
"customer_id": r["customer"]["id"],
"amount": float(r["total"]),
}
for r in records
if r.get("status") == "completed"
]
def load(rows, conn_str):
with psycopg2.connect(conn_str) as conn, conn.cursor() as cur:
cur.executemany(
"INSERT INTO orders (order_id, customer_id, amount) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING",
[(r["order_id"], r["customer_id"], r["amount"]) for r in rows],
)
if __name__ == "__main__":
raw = extract()
clean = transform(raw)
load(clean, "postgresql://user:pass@localhost/db")
Key Libraries
| Library | Purpose |
|---|---|
pandas |
DataFrame manipulation |
polars |
Rust-backed fast DataFrames |
psycopg2 / sqlalchemy |
PostgreSQL connectivity |
boto3 |
AWS SDK for Python |
google-cloud-bigquery |
BigQuery client |
pyspark |
Spark DataFrames in Python |
great_expectations |
data quality checks |
duckdb |
embedded OLAP SQL engine |
Phase 4 — Data Warehousing (Months 4–5)
Data warehouses store analytical data at scale, separate from operational databases.
Core Concepts
- OLTP vs OLAP: transactional (row-oriented, many small writes) vs analytical (column-oriented, large scans)
- Columnar storage: Parquet, ORC — compress well, fast for aggregations
- Partitioning: slice tables by date/region to prune scans
- Clustering / Z-ordering: co-locate related rows for faster filtering
Major Data Warehouses
| Platform | Cloud | Strengths |
|---|---|---|
| Snowflake | AWS/GCP/Azure | easy scale, time travel, data sharing |
| BigQuery | GCP | serverless, pay-per-query, ML integration |
| Redshift | AWS | Postgres-compatible, RA3 nodes |
| Databricks Lakehouse | AWS/GCP/Azure | Spark + Delta Lake, unified platform |
| Azure Synapse | Azure | integrated with Azure ecosystem |
| DuckDB | local / embedded | zero-config, fast, great for dev |
Star Schema Example
-- Dimension table
CREATE TABLE dim_customer (
customer_sk SERIAL PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT,
country TEXT,
valid_from DATE,
valid_to DATE
);
-- Fact table
CREATE TABLE fact_orders (
order_id TEXT PRIMARY KEY,
customer_sk INT REFERENCES dim_customer(customer_sk),
date_sk INT, -- FK to dim_date
product_sk INT, -- FK to dim_product
quantity INT,
amount NUMERIC(12,2)
);
Phase 5 — ETL / ELT with dbt (Months 5–6)
Modern data engineering favors ELT (extract → load raw → transform in warehouse) over traditional ETL.
dbt (Data Build Tool)
dbt lets you write SQL SELECT statements; it handles materialisation, dependency resolution, and documentation.
-- models/staging/stg_orders.sql
{{ config(materialized='view') }}
SELECT
id AS order_id,
customer_id,
CAST(total AS NUMERIC) AS amount,
status,
created_at::DATE AS order_date
FROM {{ source('raw', 'orders') }}
WHERE status != 'cancelled'
-- models/marts/fct_monthly_revenue.sql
{{ config(materialized='table', partition_by={'field': 'month', 'data_type': 'date'}) }}
SELECT
DATE_TRUNC('month', order_date) AS month,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM {{ ref('stg_orders') }}
GROUP BY 1
dbt Core Concepts
| Concept | Description |
|---|---|
| Sources | raw tables in warehouse |
| Staging models | 1-to-1 clean + rename |
| Intermediate models | joins, business logic |
| Mart models | final analytics-ready tables |
| Seeds | CSV lookup tables checked into Git |
| Tests | not_null, unique, accepted_values, custom |
| Snapshots | SCD Type 2 history tracking |
Ingestion Tools
| Tool | Type | Use Case |
|---|---|---|
| Airbyte | open-source EL | 300+ connectors |
| Fivetran | managed EL | enterprise, low maintenance |
| Stitch | managed EL | simpler, cheaper |
| Kafka Connect | streaming EL | real-time sources |
| custom scripts | Python/SQL | APIs, legacy sources |
Phase 6 — Orchestration (Months 6–7)
Orchestration tools schedule and monitor data pipelines, handle retries, and manage dependencies between tasks.
Apache Airflow
Airflow uses DAGs (Directed Acyclic Graphs) written in Python.
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.postgres.operators.postgres import PostgresOperator
from datetime import datetime, timedelta
default_args = {"retries": 2, "retry_delay": timedelta(minutes=5)}
with DAG(
"daily_orders_pipeline",
schedule_interval="0 6 * * *", # 06:00 UTC daily
start_date=datetime(2025, 1, 1),
catchup=False,
default_args=default_args,
) as dag:
extract = PythonOperator(
task_id="extract_orders",
python_callable=extract_orders_from_api,
)
load = PostgresOperator(
task_id="load_to_staging",
sql="sql/load_staging_orders.sql",
postgres_conn_id="warehouse",
)
transform = BashOperator(
task_id="dbt_run",
bash_command="dbt run --select marts.fct_monthly_revenue",
)
extract >> load >> transform
Orchestration Comparison
| Tool | Language | Strengths | Best For |
|---|---|---|---|
| Airflow | Python | mature, large ecosystem | complex DAGs, enterprise |
| Prefect | Python | modern API, dynamic workflows | developer-friendly |
| Dagster | Python | asset-centric, strong typing | data asset management |
| dbt Cloud | SQL | built-in scheduling for dbt | dbt-first shops |
| Mage | Python | UI-first, Jupyter-like | quick start, small teams |
Phase 7 — Big Data & Apache Spark (Months 7–8)
When data exceeds a single machine (typically 10GB+), you need distributed computing.
Spark Concepts
- RDD → low-level distributed collection (avoid in new code)
- DataFrame → structured, SQL-like API (use this)
- Lazy evaluation → transformations build a plan; actions execute it
- Partitions → units of parallelism; aim for 100–200MB per partition
PySpark Example
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.appName("OrdersETL").getOrCreate()
df = spark.read.parquet("s3://data-lake/raw/orders/")
result = (
df
.filter(F.col("status") == "completed")
.withColumn("month", F.date_trunc("month", F.col("created_at")))
.groupBy("month")
.agg(
F.sum("amount").alias("revenue"),
F.count("*").alias("orders"),
)
.orderBy("month")
)
result.write.mode("overwrite").parquet("s3://data-lake/processed/monthly_revenue/")
Spark Performance Tips
| Tip | Why |
|---|---|
Avoid collect() on large DataFrames |
OOM on driver |
Use filter / select early |
reduce shuffle data |
broadcast() small dimension tables |
avoid shuffle joins |
Persist intermediate DFs (cache()) |
avoid recomputation |
Tune spark.sql.shuffle.partitions |
default 200 is often wrong |
| Use Delta Lake / Iceberg | ACID, schema evolution, time travel |
Phase 8 — Streaming Data (Months 8–10)
Batch processing is great for nightly jobs, but many use cases need near-real-time data.
Apache Kafka
Kafka is the industry-standard event streaming platform.
| Concept | Description |
|---|---|
| Topic | named stream of events |
| Partition | parallel lane within a topic |
| Producer | app that writes events |
| Consumer | app that reads events |
| Consumer Group | multiple consumers sharing work |
| Offset | position of a message in a partition |
| Broker | Kafka server node |
| Replication | copies of partitions across brokers |
from confluent_kafka import Producer, Consumer
# Producer
p = Producer({"bootstrap.servers": "localhost:9092"})
p.produce("orders", key="order-123", value='{"amount": 99.99}')
p.flush()
# Consumer
c = Consumer({
"bootstrap.servers": "localhost:9092",
"group.id": "order-processor",
"auto.offset.reset": "earliest",
})
c.subscribe(["orders"])
while True:
msg = c.poll(1.0)
if msg and not msg.error():
print(f"Received: {msg.value().decode()}")
Streaming Frameworks
| Framework | Language | Strengths |
|---|---|---|
| Spark Structured Streaming | Python/Scala | unified batch+streaming, SQL API |
| Apache Flink | Python/Java/Scala | true stream, low latency, stateful |
| Kafka Streams | Java | lightweight, no cluster needed |
| AWS Kinesis Data Analytics | SQL | managed Flink on AWS |
Phase 9 — Cloud Data Platforms (Months 10–12)
Most data engineering happens in the cloud. Pick one cloud to go deep on first.
AWS Data Stack
| Service | Role |
|---|---|
| S3 | data lake storage (Parquet, Delta) |
| Glue | serverless Spark ETL + Data Catalog |
| Redshift | columnar data warehouse |
| Kinesis | managed event streaming |
| EMR | managed Hadoop/Spark cluster |
| Athena | serverless SQL on S3 |
| Lake Formation | data lake governance |
| Step Functions | pipeline orchestration |
GCP Data Stack
| Service | Role |
|---|---|
| Cloud Storage (GCS) | data lake |
| BigQuery | serverless data warehouse |
| Dataflow | managed Apache Beam (batch + streaming) |
| Pub/Sub | managed event messaging |
| Dataproc | managed Spark/Hadoop |
| Looker | BI and data exploration |
Azure Data Stack
| Service | Role |
|---|---|
| Azure Data Lake Storage (ADLS) | data lake |
| Azure Synapse Analytics | warehouse + Spark |
| Azure Data Factory | ETL orchestration |
| Event Hubs | managed Kafka-compatible streaming |
| HDInsight | managed Hadoop/Spark |
| Azure Databricks | Spark + Delta Lake |
File Formats to Know
| Format | Type | Strengths |
|---|---|---|
| Parquet | columnar | fast reads, great compression |
| ORC | columnar | Hive-optimised, good compression |
| Avro | row | schema evolution, Kafka serialisation |
| Delta Lake | columnar + ACID | Spark-native, time travel, upserts |
| Apache Iceberg | columnar + ACID | multi-engine, hidden partitioning |
Phase 10 — DataOps (Months 12–14)
DataOps applies DevOps principles to data pipelines: testing, CI/CD, monitoring, and data quality.
Data Quality with Great Expectations
import great_expectations as ge
df = ge.read_csv("orders.csv")
df.expect_column_values_to_not_be_null("order_id")
df.expect_column_values_to_be_unique("order_id")
df.expect_column_values_to_be_between("amount", min_value=0, max_value=100_000)
df.expect_column_values_to_be_in_set("status", ["completed", "pending", "refunded"])
results = df.validate()
print(results["success"]) # True/False
dbt Tests
# models/staging/schema.yml
models:
- name: stg_orders
columns:
- name: order_id
tests: [not_null, unique]
- name: amount
tests:
- not_null
- dbt_utils.accepted_range:
min_value: 0
- name: status
tests:
- accepted_values:
values: [completed, pending, refunded]
CI/CD for Data Pipelines
# .github/workflows/dbt-ci.yml
name: dbt CI
on: [pull_request]
jobs:
dbt-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with: {python-version: "3.12"}
- run: pip install dbt-bigquery
- run: dbt deps
- run: dbt compile
- run: dbt test --select state:modified+
env:
DBT_ENV_PROFILES_DIR: .
Observability
| Tool | Purpose |
|---|---|
| Monte Carlo | automated data observability |
| Soda Core | open-source data quality checks |
| dbt artifacts | lineage, test results |
| Airflow metrics | DAG duration, failure rates |
| Prometheus + Grafana | Spark / Kafka metrics |
| OpenLineage | cross-tool data lineage |
Full Technology Map
┌─────────────────────────────────────────────────────────────────┐
│ DATA ENGINEER TECH MAP │
├──────────────┬──────────────────────────────────────────────────┤
│ Foundations │ Linux · Git · Python · SQL │
├──────────────┼──────────────────────────────────────────────────┤
│ Storage │ PostgreSQL · S3/GCS/ADLS · Delta/Iceberg/Parquet │
├──────────────┼──────────────────────────────────────────────────┤
│ Warehousing │ Snowflake · BigQuery · Redshift · Databricks │
├──────────────┼──────────────────────────────────────────────────┤
│ Ingestion │ Airbyte · Fivetran · Kafka Connect · Custom │
├──────────────┼──────────────────────────────────────────────────┤
│ Transform │ dbt · PySpark · pandas · polars · SQL │
├──────────────┼──────────────────────────────────────────────────┤
│ Orchestration│ Airflow · Prefect · Dagster · dbt Cloud │
├──────────────┼──────────────────────────────────────────────────┤
│ Batch │ Spark · Glue · Dataflow · EMR │
├──────────────┼──────────────────────────────────────────────────┤
│ Streaming │ Kafka · Flink · Spark Streaming · Kinesis │
├──────────────┼──────────────────────────────────────────────────┤
│ DataOps │ Great Expectations · dbt tests · OpenLineage │
├──────────────┼──────────────────────────────────────────────────┤
│ Cloud │ AWS (Glue/Redshift/EMR) · GCP · Azure │
└──────────────┴──────────────────────────────────────────────────┘
Realistic Timeline
| Month | Focus | Milestone |
|---|---|---|
| 1 | Linux, Git, Python basics | Automate a file processing task |
| 2 | Advanced SQL, data modeling | Write complex window function queries |
| 3 | Python ETL scripting | Build a CLI ETL script with error handling |
| 4 | Data warehouse setup | Load data into Snowflake or BigQuery |
| 5 | dbt basics | Transform raw → staging → mart with tests |
| 6 | Airflow DAGs | Schedule a multi-step pipeline |
| 7 | Spark DataFrames | Process 10GB CSV with PySpark |
| 8 | Kafka basics | Produce/consume events, build a streaming pipeline |
| 9 | Cloud data services | Deploy a full pipeline on AWS or GCP |
| 10 | Data quality + CI/CD | Add Great Expectations + GitHub Actions |
| 11–12 | Portfolio + specialise | 3 portfolio projects, pick a track |
| 13–14 | Job search | Leetcode SQL, system design, interviews |
Portfolio Project Ideas
| Project | Skills Demonstrated | Complexity |
|---|---|---|
| GitHub Archive pipeline | Python, Kafka, Spark, BigQuery | Medium |
| E-commerce dbt project | SQL, dbt models, tests, docs | Low |
| Real-time crypto dashboard | Kafka, Flink/Spark Streaming, Grafana | High |
| Wikipedia pageview ETL | Airflow, Spark, S3, Parquet | Medium |
| Spotify listening history | API ingestion, Snowflake, dbt, Looker | Low–Medium |
| NYC Taxi data lakehouse | Delta Lake, Databricks, PySpark, dbt | Medium–High |
Data Engineer Roles & Salary
| Role | Experience | US Salary | EU Salary |
|---|---|---|---|
| Junior Data Engineer | 0–2 years | $90–120k | €45–65k |
| Data Engineer | 2–4 years | $120–160k | €65–90k |
| Senior Data Engineer | 4–7 years | $160–200k | €90–120k |
| Staff Data Engineer | 7+ years | $200–250k | €110–145k |
| Data Engineering Manager | 5+ years | $180–240k | €100–140k |
| Data Architect | 8+ years | $180–250k | €110–150k |
Common Mistakes
| Mistake | Better Approach |
|---|---|
| Building pipelines before understanding the data | Profile raw data first (df.describe(), SELECT COUNT(*)) |
| Not handling schema drift | Use schema enforcement (dbt contract, Avro, Iceberg) |
| One giant DAG for everything | Small, modular DAGs with clear boundaries |
| Writing transformations outside the warehouse | Push computation to the warehouse (ELT over ETL) |
| Ignoring data quality until prod breaks | Add dbt tests + Great Expectations from day one |
| Reprocessing all history when logic changes | Use incremental models and Delta/Iceberg time travel |
| Storing secrets in code | Use environment variables, AWS Secrets Manager, Vault |
| Not monitoring pipeline SLAs | Set up Airflow SLA misses + Slack alerts |
Data Engineer vs Related Roles
| Dimension | Data Engineer | Data Scientist | Analytics Engineer | Data Analyst | ML Engineer |
|---|---|---|---|---|---|
| Primary output | pipelines, data products | models, insights | dbt models, metrics | dashboards, reports | ML systems |
| Main language | Python, SQL, Scala | Python, R | SQL, dbt | SQL, Excel, Python | Python, C++ |
| Typical tools | Spark, Airflow, Kafka | scikit-learn, pandas | dbt, Looker | Tableau, Power BI | PyTorch, Kubernetes |
| Infrastructure | designs data systems | uses data systems | bridges both | consumes clean data | deploys models |
| US salary range | $120–200k | $130–200k | $110–170k | $80–130k | $150–230k |
Frequently Asked Questions
Do I need a CS degree to become a data engineer?
No. Many data engineers come from software development, data analysis, or self-taught backgrounds. What matters is demonstrable skill in SQL, Python, and building pipelines. A portfolio of working projects is more persuasive than a degree.
Should I learn Scala or Python for Spark?
Start with Python (PySpark). It has a larger community, easier learning curve, and most new Spark jobs use PySpark. Learn Scala only if your employer or a specific job requires it — it offers better performance but isn't necessary to get hired.
Spark or dbt first?
dbt first. Most data engineering roles start in the warehouse layer (Snowflake/BigQuery + dbt). Spark is needed once data scale exceeds what the warehouse handles well, which may be months or years into a role.
How important is real-time/streaming?
Valuable but not required for most roles. The majority of data engineering work is batch. Get a solid batch foundation, then add streaming. Kafka is by far the most common streaming technology to know.
Which cloud should I learn first?
AWS is the largest market share and most job postings. GCP is best if you plan to use BigQuery (easiest warehouse to start with). Azure is dominant in enterprise/Microsoft shops. Pick based on the jobs you're targeting, or learn BigQuery on GCP first for its low-cost serverless model.
What SQL questions get asked in data engineering interviews?
Expect window functions (ROW_NUMBER, RANK, LAG/LEAD, running totals), complex JOINs, de-duplication, finding the Nth highest value, and writing incremental queries. Practice on real datasets — not just toy examples.