Skip to main content

Chapter 1: The Data Engineer's Mindset

You write a script that pulls yesterday's orders from an API and loads them into a database.

Twenty lines of Python. You run it once. The rows are there. It works. You move on.

Here is what happens next:

Nothing in the original twenty lines was wrong. The query was correct. The logic was correct. It produced the right answer, once.

That gap, between code that works and a system you can trust unattended, is what this chapter is about, and it matters more than it used to. AI agents are starting to query, transform, and act on data with no human reading the output first. A person trusting bad data eventually notices something feels off. An agent doesn't. It just acts on it. Part 5 of this book covers agentic workflows in depth, but the mindset that keeps an agent from confidently acting on garbage starts here, in chapter one.

The Data Engineer's Mindset

What Does a Data Engineer Do? A closer look at the ETL process ↗

1.1 The Shift You Are Making

Most people arrive at data engineering already trained to ask one question: does this produce the correct output, right now, for the input in front of me? Data engineering asks a different one: does this keep producing the correct output, automatically, for inputs you have not seen yet, after you've gone home?

That single change in what you're optimizing for reshapes how you approach almost everything you build.

DimensionTutorial / analyst mindsetData engineer mindset
CorrectnessCorrect for the data I tested withCorrect for nulls, duplicates, late arrivals, schema changes
ExecutionRan once, looked rightRuns unattended, on a schedule, for months
FailureSomething to fix and move pastSomething the system should expect and recover from
ChangeDone when the answer is rightDone when someone else can change the source and it still works, or fails loudly
OwnershipJob ends at handoffJob includes who gets paged when it breaks

A tutorial teaches you how code runs once. Data engineering is what happens the second, the hundredth, and the millionth time it runs, and what "running" actually looks like once it's live is data moving through extract, transform, and load stages on its own schedule, with nobody babysitting it.

That shift in mindset shows up in practice as five specific wrong assumptions almost everyone starts with. Recognizing them now is faster than learning them the hard way.


1.2 Five Wrong Assumptions New Data Engineers Bring In

Each of these feels true right up until something is running in production, unattended, for real users.

AssumptionWhy it feels trueWhy it breaks
"If my query is correct, my pipeline is correct"The query ran and the numbers matchedCorrectness is one moment in time. Pipelines run repeatedly against data that changes shape, arrives late, or arrives twice
"Data engineering is mostly writing ETL scripts"That's the visible, codeable partMost of the job is orchestration, failure handling, monitoring, access control, and cost
"If it works, I'm done"The task said load the data, and the data is loadedProduction code gets read, rerun, and modified by other people, including future you, who won't remember today's assumptions
"I can just rerun it if something breaks"Rerunning a script feels harmlessA script that already partially succeeded can duplicate rows or double-count revenue on rerun, unless it was built to be safely rerunnable
"The data will keep looking like this"It's looked the same in every testSource systems change fields without warning, and someone eventually sends a CSV with a column you were never told about

The five wrong assumptions, connected

If you've already lived one of these five, good news: the fix for all of them is not more cleverness in the code. It's a different set of questions you ask before you write it.


1.3 Five Mental Models to Start Building Now

These resurface constantly through this book, in SQL, in Python, in Airflow, in every cloud platform covered later. Start noticing them in whatever code you write this week.

Reliability beats correctness-once. A pipeline that's 95% correct but tells you clearly when it fails beats one that's 100% correct until the day it silently isn't. Ask yourself: how would I know if this broke?

Think in contracts, not just queries. Every table you produce has consumers, whether that's a dashboard, another pipeline, or a person making a decision. A contract means the column won't silently disappear or change type. Ask: who depends on this, and what have I implicitly promised them?

Everything fails eventually, so design for it. Networks time out. APIs return malformed data. Files show up late, twice, or not at all. Ask: if this fails halfway through, what state does it leave things in, and can I safely retry it?

You own what happens after you ship it. Writing the code is the easy 20%. Watching it, alerting on it, fixing it at 2am is the other 80%. Ask: if this breaks and I'm not around, what would the next person need to see to fix it fast?

Data has a lifecycle, not just a destination. Where a value came from, what transformed it, and who can see it are not afterthoughts you bolt on later. Ask: if someone asked me to prove where this number came from, could I?

Here's how the five connect:

The five mental models, connected

None of this is abstract. Here's the same twenty lines of code, written with and without these questions in mind.


1.4 The Same Task, Two Ways

The ordering script from the start of this chapter, written twice. Same goal, same output, very different assumptions baked in.

Works once:

import requests
import sqlite3

response = requests.get("https://api.example.com/orders?date=yesterday")
orders = response.json()

conn = sqlite3.connect("sales.db")
for order in orders:
conn.execute(
"INSERT INTO orders VALUES (?, ?, ?)",
(order["id"], order["customer"], order["amount"]),
)
conn.commit()

Built to be trusted:

import requests
import sqlite3
import logging

logging.basicConfig(level=logging.INFO)

def fetch_orders(date: str) -> list[dict]:
response = requests.get(f"https://api.example.com/orders?date={date}", timeout=10)
response.raise_for_status() # fail loudly instead of continuing on a bad response
return response.json()

def load_orders(orders: list[dict], date: str) -> None:
conn = sqlite3.connect("sales.db")
try:
conn.execute("DELETE FROM orders WHERE order_date = ?", (date,)) # idempotent rerun
for order in orders:
if order.get("amount") is None:
logging.warning("Skipping order %s: missing amount", order.get("id"))
continue
conn.execute(
"INSERT INTO orders (id, customer, amount, order_date) VALUES (?, ?, ?, ?)",
(order["id"], order["customer"], order["amount"], date),
)
conn.commit()
logging.info("Loaded %d orders for %s", len(orders), date)
except Exception:
conn.rollback()
raise
finally:
conn.close()

Same twenty-ish lines. No fancier library, no different language. Just five questions asked before writing each one: how would I know if this failed, what have I promised the caller, what happens on a bad response, is it safe to rerun, what would the next person need to see in the logs.

That's the entire mindset shift.


Summary

Writing code that's right once is the easy part. Data engineering is building systems that keep being right, automatically, after you've stopped watching. The five wrong assumptions in section 1.2 are what cause 3am pages. The five mental models in section 1.3 are the fix, and they cost nothing extra to start practicing today.

What's Next

Chapter 2 maps the modern data stack, source systems, ingestion, storage, transformation, orchestration, serving, and observability, so you can see where every tool in this book fits before learning them one at a time.