02 · Data Sources & Ingestion Patterns¶
Before you can write a line of transform logic, you need data flowing in. This lesson covers the shapes data sources come in and the two fundamental ingestion patterns — batch and streaming — that every pipeline chooses between (or, often, mixes).
What actually ran
Code below uses the Python standard library (csv, time, json) —
no external services or network calls.
The three source shapes¶
Most sources you'll ingest from fall into one of these categories:
- Files — CSVs dropped in a folder, JSON exports, logs, Parquet files someone else's pipeline produced.
- APIs — REST/GraphQL endpoints returning JSON, usually paginated and sometimes rate-limited.
- Databases — an operational Postgres/MySQL table you need a copy of for analytics, without hammering the production system.
Each shape has a different "am I done reading?" signal: a file ends at EOF, an API tells you via pagination metadata (or you paginate until an empty page), a database query returns a fixed result set — or, for ongoing ingestion, you track a watermark (see Level 2's incremental loads lesson).
Batch: ingest on a schedule, in chunks¶
Batch ingestion reads a bounded chunk of data — "everything since last run," or "today's file" — on a schedule (hourly, nightly). It's the default choice for the majority of analytics pipelines because it's simple, easy to retry, and easy to reason about.
import csv, io
# Simulates "today's file" landing in a folder, one batch at a time
daily_files = {
"2026-08-27.csv": "order_id,amount\n1,50.00\n2,30.00\n",
"2026-08-28.csv": "order_id,amount\n3,20.00\n4,45.50\n",
}
def process_batch(filename, content):
rows = list(csv.DictReader(io.StringIO(content)))
total = sum(float(r["amount"]) for r in rows)
print(f"Batch {filename}: {len(rows)} rows, ${total:.2f} total")
return rows
all_rows = []
for filename, content in daily_files.items():
all_rows.extend(process_batch(filename, content))
print(f"Ingested {len(all_rows)} rows across {len(daily_files)} batches")
Batch 2026-08-27.csv: 2 rows, $80.00 total
Batch 2026-08-28.csv: 2 rows, $65.50 total
Ingested 4 rows across 2 batches
Each batch is a complete, self-contained unit of work — if batch
2026-08-28.csv fails halfway through, you re-run that batch, not the
whole history. This is why batch pipelines are usually organized by a
natural boundary (a day, an hour, a file) rather than one giant continuous
stream of rows.
Streaming: ingest as events arrive¶
Streaming ingestion processes each record (or small micro-batch of records) as soon as it's available, rather than waiting for a scheduled window. It trades simplicity for lower latency — useful when "we found out about this order 6 hours late" is a real business problem (fraud detection, live dashboards, alerting).
import time
# Simulates events arriving one at a time, as they would from a message
# queue (Kafka, Kinesis, Pub/Sub) — here just a Python generator standing
# in for that queue.
def event_stream():
events = [
{"order_id": 1, "amount": 50.00, "ts": "10:00:01"},
{"order_id": 2, "amount": 30.00, "ts": "10:00:04"},
{"order_id": 3, "amount": 20.00, "ts": "10:00:09"},
]
for e in events:
yield e
running_total = 0.0
for event in event_stream():
running_total += event["amount"]
print(f"[{event['ts']}] processed order {event['order_id']}, "
f"running total ${running_total:.2f}")
[10:00:01] processed order 1, running total $50.00
[10:00:04] processed order 2, running total $80.00
[10:00:09] processed order 3, running total $100.00
In a real streaming system, event_stream() would be a consumer reading
from Kafka/Kinesis/Pub-Sub, and processing would keep running indefinitely —
there's no "end of file." That has a real consequence: streaming pipelines
need a different mental model for correctness (what happens if you crash
between processing an event and acknowledging it?) which Level 3's streaming
lessons cover in depth.
Batch vs. streaming: the real tradeoff¶
| Batch | Streaming | |
|---|---|---|
| Latency | Minutes to a day | Seconds or less |
| Complexity | Lower — bounded, retryable units | Higher — unbounded, needs checkpointing |
| Cost model | Runs briefly, on a schedule | Runs continuously, needs always-on infra |
| Failure recovery | Re-run the failed batch | Resume from last checkpoint/offset |
| Typical tools | Cron, Airflow, plain scripts | Kafka, Kinesis, Flink, Spark Structured Streaming |
A large majority of real pipelines are batch, because most business questions ("how did we do yesterday?") don't need sub-minute latency, and batch is dramatically simpler to build, test, and debug. Reach for streaming only when the business requires low latency — not because it sounds more sophisticated.
Traps¶
- Streaming for its own sake. If nightly batch answers the business question, streaming adds operational complexity (always-on infrastructure, checkpointing, harder debugging) for no benefit.
- Batch windows that don't match a natural boundary. Splitting a batch arbitrarily (every 10,000 rows, regardless of source structure) makes failures harder to reason about than splitting by day/file/source unit.
- Ignoring what "done" means for a source. For an API, forgetting to check for an empty next-page token means you either loop forever or stop one page early — always confirm the source's actual end-of-data signal.
Cheat sheet¶
| Source | Typical ingestion pattern | "Am I done?" signal |
|---|---|---|
| Flat files | Batch | End of file / all files in folder processed |
| REST API | Batch (with pagination) | Empty page / no next token |
| Operational database | Batch (scheduled query) or CDC (Level 2) | Query completes / no new watermark rows |
| Message queue (Kafka, etc.) | Streaming | Never — runs continuously |
How It Actually Works¶
Underneath every ingestion pattern is the same mechanical question: how does the source system decide what to hand you, and how does that decision get made durable enough to survive a crash on either side?
- Full extraction issues a query (or file listing) that scans the entire source dataset
every run. Mechanically the source database walks its storage (a full table scan or an
index scan across all rows) and streams a result set back over a cursor; the ingestion
client pages through that cursor in fixed-size batches (e.g.
fetchmany(10000)) to avoid holding the whole result in memory. There is no bookkeeping between runs — correctness comes from re-deriving state from zero every time, which is simple but means cost scales with total source size, not with what changed. - Incremental/watermark extraction instead tracks a monotonically increasing column
(an
updated_attimestamp or auto-incrementid) as a watermark persisted by the ingestion tool itself, outside the source system. Each run issuesWHERE updated_at > :last_watermark, which the source's query planner can satisfy with an index range scan instead of a full scan — the mechanical win is turning an O(n) scan into an O(delta) scan. The correctness risk is entirely in when the watermark is advanced: if you advance it before confirming the batch landed downstream, a mid-batch failure silently drops rows written between the old and new watermark on retry. - Event/webhook-driven ingestion flips the pull model to push: the source system itself fires an HTTP callback or writes to a message broker (Kafka, SNS/SQS) the instant a change commits. The mechanical guarantee you actually get depends on the broker's delivery semantics — Kafka's at-least-once delivery with consumer offset commits means your ingestion code will occasionally see the same event twice after a consumer restart, so idempotent upserts (keyed by an event ID) are not optional, they are how correctness is achieved despite duplicate delivery, not despite the pattern.
- File-drop ingestion relies on the filesystem or object store's own change notification
(S3
ObjectCreatedevents, or a directory listener pollingmtime) to trigger a load; the mechanical hazard is partial writes — a listener firing on a file that a producer is still uploading will read truncated bytes, which is why production pipelines write to a temp key and atomically rename/move only after the upload completes.
Exercise¶
Extend the batch example so that it also tracks, per batch, the minimum
and maximum amount seen — print a one-line summary per batch showing
count, total, min, and max. Then write a short paragraph (3-4 sentences)
arguing whether the daily-order-totals pipeline in this lesson should stay
batch or move to streaming, using the tradeoff table above to justify your
answer.