Data Engineering
The role in one breath: an analyst answers questions, while a data engineer makes the data show up, clean itself and refresh on its own — reliably, with nobody babysitting it. It's about reproducibility, real open-source tooling, and architectures that survive a restart. The turning point from analyst to engineer is exactly when 'works on my laptop' becomes 'works for everyone, on a schedule.'
In practice you build the conveyor raw → clean → mart: pull data from APIs, databases and files, land it in a warehouse, reshape it with tested and documented SQL models, put the whole thing on a schedule with dependencies, retries and alerts, then package the stack into containers so it spins up anywhere with one command.
Environment: WSL2 + venv/uv + Docker/compose. Python for data: pandas/polars, requests/httpx, tenacity. Loading: dlt (plus Airbyte, Kafka/Redpanda for streaming). Transforms: dbt Core (plus SQLMesh). Storage: DuckDB, Postgres, ClickHouse, BigQuery/Snowflake. Formats & lake: Parquet, Iceberg on MinIO/S3. Orchestration: Prefect, Airflow, Dagster, Kestra. BI: Metabase, Superset, Lightdash, Grafana, a Cube semantic layer, and Streamlit.
It clicks for people who've outgrown 'just answer questions off ready-made data' and want to build the pipe itself. For those who like order, automation and engineering, and get a kick when something just runs on its own. A natural next step for an analyst leveling up into an engineer, or a backender drifting toward data.
The analyst works with data that's already prepared — questions, dashboards, SQL, stats — and doesn't own the plumbing. The data engineer (that's you) builds and keeps that plumbing: reliable delivery and refresh, so the analyst always has something solid to work on. The ML engineer takes the prepared data and trains models and features on it — your pipelines are the ground the whole ML thing stands on.
Script + environment
a couple of eveningsOne .py that comes up for anyone from a clean environment. This is the foundation of reproducibility — everything else grows from here.
- You spin up an isolated environment (venv/uv) and lock dependencies with pinned versions
- You keep code in the Linux filesystem inside WSL, not in /mnt/c, and understand why it's faster
- You hide secrets in .env and keep .env, .venv and data out of git via .gitignore
- You use git with intent: meaningful commits, task branches, a readable README
- The project runs on someone else's machine without any 'oh, just tweak this by hand' steps
From API to storage
1–2 weeksLearn to pull data yourself and land it in a database instead of a CSV on your desktop.
- You walk the whole pagination (page / offset / cursor), not just the first page
- You authenticate by token and keep the key out of code, in the environment
- You add retries with exponential backoff and respect rate limits — 429/5xx don't kill the run
- You write results into DuckDB/Postgres and can eyeball the tables in DBeaver
- A rerun doesn't create duplicates — you upsert/merge on a key
Transformations in dbt
2–3 weeksTurn raw tables into clean marts — as code, with tests, docs and lineage.
- You build staging → marts layers and wire models via ref() and source()
- You cover key columns with tests (unique, not_null) — and they pass green
- You build at least one incremental model with a correct is_incremental() filter
- You generate docs and read lineage from them — where each number came from
- You deliberately pick a materialization (view / table / incremental) per case
Grown-up ingest with dlt
2–3 weeksReplace hand-rolled scripts with a tool that manages schema and incrementals for you.
- You define sources as dlt resources and load them in a few lines of Python
- You set write_disposition (append / replace / merge) to fit the task
- You run cursor-based incrementals without hand-wrangling the schema
- You flatten nested JSON into plain tables automatically
- You swap the destination (say, DuckDB ↔ Postgres) with almost no code changes
Orchestration and scheduling
3–4 weeksWire the steps into one process, put it on a schedule, add retries and alerts.
- You express the pipeline as a flow/DAG with explicit dependencies between steps
- You put it on a schedule and can backfill missed dates
- You attach retries to flaky steps instead of 'I'll just rerun it by hand'
- You catch failures and get an alert — you don't hear about breakage from users
- You read logs and run history in the UI and see exactly what broke where
Containers and production
finish · production stackPackage the whole stack so it comes up anywhere with a single command.
Finish: docker compose up — and the whole data stack comes up from scratch.
- You write a Dockerfile for your pipeline and build a working image
- You bring up a multi-service setup (database + orchestrator + BI) via compose with depends_on
- The entire stack starts with one command from a clean machine
- Secrets and configs live in the environment — nothing is baked into the image
- You have data tests and basic observability: logs, statuses, alerts
- The project is polished enough to show an employer without wincing