AdvancedPipeline Architecture · 35 min · 12 challenges

Pipeline Anatomy: Advanced

Picture Spotify in October: the Wrapped team needs a listening-sessions table that a recommendations team built 4 years ago for its own model. Royalty reporting reads it too. Wrapped wants column names frozen until December, recommendations wants rows by 06:00, royalties wants every row. That is 3 promises from 1 pipeline, and none of them is written down. This lesson is about writing them down.

Pipelines as Products

Daily Life
Interviews
The intermediate lesson drew pipelines as boxes and arrows: sources on the left, a curated layer in the middle, consumers on the right. Those diagrams answer where data moves. A product framing asks a second question of every box on the right edge: who reads this, and what did we promise them? A pipeline is a product when it has a named consumer and a written contract, and when someone has planned how it will be retired. A pipeline with none of those is a script that happens to run on a schedule, and the first person to notice it broke is a stranger on another team.

Name the consumer

Take a curated table of music listening sessions, one row per user per session, read by 3 teams. Each reads it for a different reason, so each breaks in a different way.
ConsumerWhat they readWhat breaks them
Recommendationssessions from the last 24 hours, by 06:00a table that lands at 09:00 misses the morning model refresh
Royalty reportingevery session in a calendar montha day with 2% of rows missing understates payouts
Yearly recap campaign12 months of sessions, read once in Decembera renamed `ms_played` column 3 days before launch

Without this list, every design decision is a guess. Should the pipeline ship a partial day at 06:00 or a complete day at 09:00? Recommendations wants partial and royalties wants complete. The answer is 2 outputs: a listening_sessions_early snapshot at 06:00 with recommendations as its only consumer, and the final table at 09:00 for everyone else.

Write the contract

A contract is the promise in writing: what a row means, which columns exist with which types, when the table is ready, how complete it is, how it may change, and who gets paged when any of that fails. It fits on one screen, and each consumer signs off on it before the first row lands. Here is the contract for the final table.
product: curated.listening_sessions
owner: listening-data-team
grain: one row per user per listening session
consumers:
- royalty-reporting
- yearly-recap
ready_by: "09:00 UTC"
completeness: at least 99.5 percent of the sessions landed in raw for that day
columns:
user_id: bigint, never null
session_start: timestamp, never null
ms_played: bigint
event_date: date, partition key
change_policy: additive only, 30 days notice before any removal or rename
paged: listening-data-oncall

Each line of the contract maps to a check the pipeline runs on itself. The ready_by and completeness lines become the query below, run at 09:00. It fails if the curated table holds under 99.5% of the sessions that landed in raw.listening_sessions for yesterday, or if yesterday's latest session started before 23:00, a sign the day was cut off. The columns line becomes a check on names and types.

SELECT
COUNT(*) >= 0.995 * (
SELECT
COUNT(*)
FROM raw.listening_sessions
WHERE event_date = CURRENT_DATE - INTERVAL '1' DAY
) AS is_complete,
MAX(session_start) >= CAST(CURRENT_DATE - INTERVAL '1' DAY AS TIMESTAMP) + INTERVAL '23' HOUR AS reaches_day_end
FROM curated.listening_sessions
WHERE event_date = CURRENT_DATE - INTERVAL '1' DAY

Define the deprecation path

Every table is eventually replaced. The product framing plans that replacement on day 1, because the consumer list is the only way to know who the replacement will hurt. The path has 4 steps, and the contract's consumers field drives the 3rd.

Ship the replacement under a new name, listening_sessions_v2, with its own contract
Ship the replacement under a new name, listening_sessions_v2, with its own contract
Run both for a fixed window, 30 days, and publish the differences between them
Run both for a fixed window, 30 days, and publish the differences between them
Contact each listed consumer and confirm they have moved; the list is the migration checklist
Contact each listed consumer and confirm they have moved; the list is the migration checklist
Announce a final date for the old table, then drop it on that date, not 'soon'
Announce a final date for the old table, then drop it on that date, not 'soon'
TIP
Write the contract before the first row lands. A contract written after launch describes what the pipeline happens to do; a contract written before launch describes what the consumer needs, and every surprise a consumer hits comes from the difference between the 2.
Multiple Choice

A team runs a nightly job that writes orders_daily. It has run for 3 years, 6 dashboards query it, and nobody on the team can say which 6. Which single change moves it furthest toward being a product?

The Cross-Cutting Undercurrents

Daily Life
Interviews
The beginner lesson had 4 roles: source and consumer at the ends, with transform and storage between them. The undercurrent map regroups them into 4 columns that read left to right. Source stays as it was. Ingest is the first transform, the one that pulls data out of the source. Transform covers the remaining steps and the storage they write. Serve is the stored output as consumers read it. The undercurrents run the other way. Each is a concern no single box owns, so each shows up in all 4 columns, and a design that handles it in 1 column and forgets the other 3 still fails.

The 6 undercurrents

  • Rerunsa step runs twiceA nightly load fails at 02:10 and someone restarts it at 08:00. If the first attempt had already written 400,000 rows, does the table now hold 800,000?
  • Failuresa step stops partwayThe transform step dies after writing 3 of 12 regions. Does the serving step wait, run on the 3, or run on yesterday's 12?
  • Changea source changes shapeThe orders service renames amount to amount_cents. Which step is the first to notice, and is that step a check or a dashboard?
  • Latenessa row arrives after its day closedA session that ended at 23:58 on Tuesday reaches the pipeline at 00:40 on Wednesday, after Tuesday's totals were served. Does Tuesday ever get corrected?
  • Visibilitysomeone asks if the table is rightAt 09:00 an analyst asks whether yesterday is complete. Can anyone answer from a dashboard, or only by running queries by hand?
  • Costthe bill arrivesThe pipeline scans 2 TB a night. Is that because consumers need 2 TB, or because a rerun also rescans 30 days it did not need to?
Each undercurrent has a different answer in each role, which is why mapping them is worth a table. Read it one row at a time: the reruns row asks what the source does under a rerun, then what ingest does, then transform, then serve. A cell that nobody on the team can fill is a gap in the design, found before it is found in production.
UndercurrentSourceIngestTransformServe
Rerunscan the source be read twice for the same windowdoes a 2nd pull for the same window replace the 1stdoes rerunning a day overwrite that day's outputdoes a consumer see the half-written state mid-rerun
Failuresdoes the source return partial data or an erroris a partial pull discarded or keptdoes a step that dies leave a marker the next step readsdoes serving fall back to the last complete day
Changedoes the source announce changes to its shapeare unknown columns kept, dropped, or rejecteddoes a renamed column fail the step or silently null the outputdoes the consumer's contract say which columns are stable
Latenesshow long after an event can the source still emit itis the pull window wider than the latenessare closed days reopened, and how many days backdoes the consumer know a day's number may move
Visibilitydoes the source expose its own row countsis the pulled row count recorded per runis output row count compared with inputis each day's ready time recorded where consumers look
Costdoes each pull hit a production databaseis the pull incremental or fulldoes a rerun touch 1 day or 30does each consumer's query read only the partitions it needs

Walking a design through the map

Take the listening sessions pipeline and walk the lateness row. Source: the mobile app buffers sessions while the phone is offline, so a session can arrive up to 3 days late. Ingest: the nightly pull reads yesterday only, so a 2-day-old session is never pulled. That cell already fails. Transform and serve never see the row, so nothing in their cells can recover it. The fix belongs in ingest: widen the pull to 3 days and rewrite those 3 days each night. That fix then changes the reruns row, because 3 days are rewritten and the write must replace rather than add, and the cost row, because 3 days are scanned instead of 1. One undercurrent pulled 2 others with it, which is the normal case.
How the rows pull on each other
  • ▸Lateness forces reruns: a day that reopens is a day that is written twice
  • ▸Reruns force cost: every reopened day is rescanned, so the rerun window is a bill
  • ▸Failures force visibility: a step that stops partway is safe only if the next step can see it stopped
  • ▸Change forces contracts: a renamed column is harmless to a consumer whose contract never promised it
Multiple Choice

A pipeline serves daily revenue. On Thursday a consumer notices that Monday's revenue figure changed by 0.4% overnight, with no alert and no note anywhere. Which cell of the map holds the gap?

When to Split, When to Merge

Daily Life
Interviews
A DAG, a directed acyclic graph, is one scheduled graph of steps that runs as a unit: the arrows point forward and no step waits on itself. When a step fails, every step downstream of it is skipped, and steps on other branches still run. So a fragile step upstream of an urgent one sits on its critical path. Put too much into 1 DAG and a step nobody cares about ends up upstream of the step everyone cares about. Split into too many and every boundary becomes a handoff that nobody monitors. 3 signals decide where the edges go. Ownership and cadence are the first 2; the third is what it costs to operate the boundary.

The 3 splitting signals

✓Split when
  • 2 teams own steps in the same graph, and 1 team's bug pages the other
  • Steps need different cadences: an hourly step inside a nightly DAG runs 24 times or once, never right
  • A failure in 1 consumer's steps should not block another consumer: 2 consumers of 1 source each get their own graph
  • A rerun of 1 half rescans the other half for no reason
✗Merge when
  • A step exists only to feed the next, and no other consumer reads its output
  • Both sides are owned by the same team and run at the same cadence
  • The boundary would need its own contract and a ready-time check with an alert on it, and nobody would own them
  • Each half would be fewer than 3 steps and the halves would always run together

Worked example: orders and inventory

An online store runs 1 nightly DAG: pull orders, pull inventory, clean both together, compute daily revenue, compute low-stock alerts. The low-stock alerts are wanted every hour, so the team schedules the whole DAG hourly. Now daily revenue is recomputed 24 times a day, the orders database is pulled 24 times, and when an orders region file is missing at 14:00, the shared cleaning step fails and no low-stock alert fires until the file appears. Cadence is the signal: 2 cadences in 1 DAG, and the slow half is on the fast half's critical path.
Nightly DAG
Hourly DAG
transactional database
orders db
scheduler
ingest orders
curated table
clean orders
finance report
daily revenue
SLAby 07:00 UTC
transactional database
inventory db
scheduler
ingest inventory
alerting
low stock alerts
SLAevery hour

After the split: the nightly DAG and the hourly DAG share no steps, so a missing region file at 14:00 delays revenue and nothing else.

After the split, the orders database is pulled once a night and the inventory database once an hour. The boundary costs almost nothing because the 2 graphs share no step, so there is no handoff to monitor. The decision gets harder when they do share one.

When they share a step

Suppose low-stock alerts need clean_orders too, to know what sold in the last hour. Now that table has 2 consumers at 2 cadences. The split still holds, but the shared step either moves into the hourly graph, which then pulls orders hourly, or becomes a product of its own with a ready-time check that both graphs read. The rule from the product framing applies: a step read by another DAG is a product and gets a contract, even a 3-line one. Merging the 2 DAGs back together to avoid that contract trades 1 monitored handoff for the 24-times-a-day revenue problem, which is the worse deal.

Multiple Choice

A single DAG owned by the marketing team has 9 steps. Step 7, finance_close, is owned by finance and is the only step finance uses. It runs nightly like the rest and fails about once a week on a finance-side input file. Should the DAG split, and on which signal?

Build vs Buy at Each Layer

Daily Life
Interviews
Every layer of a pipeline can be built in-house or bought as a managed service: the connectors that pull from sources, the storage the curated layer sits in, the transform logic that encodes your definitions, the engine that runs that logic, the scheduler that runs the DAGs, and the tools that serve the output. The decision is made per layer, and the same team reasonably builds 1 of the 6 and buys the other 5. Each decision comes down to 3 weights.

The 3 weights

  • Differentiationdoes building it make the product better for the consumerA connector that pulls a payment provider's API is identical whoever writes it; the consumer never sees it. A transform that encodes how your company defines an active user is yours alone, and buying it means buying someone else's definition.
  • Total cost of ownershipbuild cost plus every year of running itThe build is the cheap part. A connector takes 6 engineer-weeks to write and 1 engineer-day a month forever to keep working as the source changes, which is 2.5 more engineer-weeks a year, every year.
  • Strategic riskwhat happens if the choice has to be undoneA bought storage layer holds your data in its format; leaving means a migration measured in months. A bought connector holds nothing; leaving means rewriting 1 pull. Risk is highest where data and logic accumulate.
LayerDefaultException
Ingest connectorsbuythe source is internal and proprietary, so no vendor sells a connector for it
Storagebuyregulation requires the bytes to stay on hardware you control
Transform logicbuildthe logic is a commodity, such as currency conversion from a published rate table
Transform enginebuya query shape the managed engine cannot finish inside the contract's ready-by time
Schedulerbuyschedules depend on signals from internal systems the managed scheduler cannot see
Servingbuythe consumer is a product feature with latency needs no dashboard tool meets

A worked total cost

The listening data team needs sessions from a third-party podcast host's API. A managed connector costs $1,500 a month. Building takes 1 engineer for 6 weeks, and the API changes often enough that it needs 1 engineer-day a month after that. At a loaded cost of $15,000 a month per engineer, the build is $22,500 up front and $750 a month to maintain. Year 1 costs $31,500 built against $18,000 bought. Year 3 cumulative is $49,500 built against $54,000 bought. The crossover lands in month 30. Because the connector is undifferentiated, the team buys it and spends the 6 weeks on transforms instead. The same arithmetic with a $5,000 a month connector crosses over in month 6 and points the other way, so each layer needs the calculation redone with its own prices.
✓Do
  • Decide per layer; buying storage and building transforms is the common shape
  • Count maintenance in the build cost, in engineer-days per month, forever
  • Ask which layer your data and logic would be stuck in if the vendor vanished
✗Don't
  • Build a connector to a public API because it looks like a 2-week job
  • Buy transform logic; the active-user definition is the product
  • Compare year-1 costs only; the crossover is usually in year 2 or 3
Multiple Choice

A team is choosing where the transform that defines a 'paying customer' lives. Finance and sales disagree on that definition, and support has a third version. A vendor sells a package that computes paying customers from billing data. Which weight decides this, and which way?

Redesigning a Tangled Graph

Daily Life
Interviews
Here is a graph as it looks after 3 years of growth: 14 steps owned by 3 teams, all on 1 nightly schedule. Each step was added by whoever needed it at the time, reading from whichever step already had the data. Read it the way the intermediate lesson taught, left to right, then look for arrows that point the wrong way.
transactional database
orders db
event log
web events
CRM
crm db
scheduler
ingest orders
scheduler
ingest events
scheduler
ingest crm
platform team
orders clean
platform team
sessions
platform team
customer 360
marketing team
attribution
finance team
daily revenue
finance team
finance close
finance team
refund adjustments
dashboard
exec dashboard

14 steps on one nightly schedule. Marketing and finance own some of them and the platform team owns the rest. The arrow from attribution into daily_revenue is the one to stare at.

Diagnosis

What the graph shows
  • ▸daily_revenue, a finance figure, reads attribution, a marketing model: a change to the model moves the revenue number
  • ▸exec_dashboard reads 4 steps across 3 teams, so any team's failure blanks the dashboard
  • ▸customer_360 has 3 consumers and no contract; it is a product nobody declared
  • ▸Because there is one schedule, the attribution model reruns every night even though it is wanted weekly, and it sits on the critical path to finance
  • ▸refund_adjustments runs after finance_close, so a refund posted at 23:00 reaches the dashboard but never the close

Apply the 3 framings in order. Products first: accounting reads the close, marketing reads its attribution report, and executives read the dashboard. Behind them sit 3 products with 3 contracts: finance_close and attribution, plus the curated layer that both DAGs and the dashboard read, which holds orders_clean and sessions along with customers. Undercurrents second: walk the failures row and the critical path shrinks to the steps finance needs. Split third: 3 owners and 2 cadences, so 3 DAGs around a curated layer the platform team owns.

The redesign

Sources
Curated, platform DAG
Finance DAG
Marketing DAG
Serving
transactional database
orders db
event log
web events
CRM
crm db
scheduler
ingest orders
scheduler
ingest events
scheduler
ingest crm
curated table
orders clean
SLAby 03:00 UTC
curated table
sessions
SLAby 03:00 UTC
curated table
customers
SLAby 03:00 UTC
finance DAG
daily revenue
finance DAG
refund adjustments
finance product
finance close
SLAby 06:00 UTC
marketing product
attribution
SLAMonday 08:00 UTC
dashboard
exec dashboard

The 3 DAGs read 1 curated layer. Revenue no longer depends on the marketing model, refunds land before the close, and the dashboard reads 3 products instead of 4 internal steps.

ProductOwnerConsumersReady byChange policy
curated layer: `orders_clean`, `sessions`, `customers`platformfinance DAG, marketing DAG, dashboard03:00 UTCadditive only, 30 days notice
`finance_close`financedashboard, accounting export06:00 UTCcolumn set frozen for the fiscal year
`attribution`marketingdashboardMonday 08:00 UTCmodel version in a column, old column kept 2 cycles

Check the result against the undercurrents. Failures: a marketing model failure now delays 1 weekly report, and finance closes on time. Reruns: a curated rerun clears the ready marker first, then writes each day aside and swaps it in at once, so nobody reads a half-written day at 03:00 or at 10:00. Lateness: late refunds reopen refund_adjustments and the close, and the finance contract says so. Change: customer_360 was renamed customers through the deprecation path, with both names served for 30 days. Visibility: each product records its ready time, so the 09:00 question has a dashboard answer. Cost: the attribution model runs 1 night in 7 instead of 7.

Capstone

Multiple Choice

The marketing team, 6 months after the redesign, wants attribution to run nightly and asks to move it into the finance DAG so it can reuse the finance schedule. Using the 3 splitting signals, what is the right response?

Retiring a single table needs a finer list than the products table gives: every reader of that one table, including steps inside the owning DAG, the readers teams most often forget.
Multiple Choice

The platform team wants to retire sessions and replace it with sessions_v2, which has a different grain. Which part of the redesign makes this possible without a surprise, and what does the team do first?

PUTTING IT ALL TOGETHER

> A food delivery company has 1 nightly graph that feeds driver payouts, the restaurant partner dashboard, and the promotions model, which the growth team owns and needs refreshed only weekly. Payouts are late twice a month because the promotions model fails on a feature table it builds from the same deliveries data, and nobody on the payments team can say which dashboards read the deliveries table they maintain.

Products: the 3 consumers with contracts are payouts and the partner dashboard, plus the promotions model. The deliveries table is a 4th product the payments team never declared.
Undercurrents: the failures row shows the promotions model on the payout critical path, and the lateness row shows a delivery completed at 23:55 missing that day's payout with no contract saying so.
Split: growth and payments are 2 owners. Payouts run nightly while the model runs weekly, so 2 DAGs read 1 contracted deliveries table with a ready-time check between them.
Build versus buy: the company buys the connector to the third-party payment processor and the storage. It builds and owns the pull from its own courier app, which no vendor sells, and the definition of a completed delivery, because that definition decides who gets paid.
Redesign: with the consumer list in hand, the deliveries table can later move to a new grain through the deprecation path without a surprise on payout day.
KEY TAKEAWAYS
The consumer list comes first: a pipeline becomes a product once it has a named consumer, and the written contract and the deprecation path both depend on that list.
Reruns and failures, change and lateness, visibility and cost: each of these 6 undercurrents needs an answer in all 4 roles, and a fix in 1 row usually moves 2 others.
Split a DAG where owners differ or cadences differ, and weigh what the boundary costs to run; merge when a step feeds only the next step under the same owner at the same cadence.
Decide build versus buy per layer: buy connectors and storage by default, along with engines and schedulers, and build the transform logic because the definitions are the product.
Untangle a grown graph in order: name the products, walk the undercurrents, then split on owners and cadences around a shared contracted curated layer.

A pipeline is a product: name its consumer, write its contract, plan its retirement, then untangle the rest.

Category
Pipeline Architecture
Difficulty
advanced
Duration
35 minutes
Challenges
12 hands-on challenges

Topics covered: Pipelines as Products, The Cross-Cutting Undercurrents, When to Split, When to Merge, Build vs Buy at Each Layer, Redesigning a Tangled Graph

Lesson Sections

  1. Pipelines as Products (concepts: Data Quality & Validation)

    The intermediate lesson drew pipelines as boxes and arrows: sources on the left, a curated layer in the middle, consumers on the right. Those diagrams answer where data moves. A product framing asks a second question of every box on the right edge: who reads this, and what did we promise them? A pipeline is a product when it has a named consumer and a written contract, and when someone has planned how it will be retired. A pipeline with none of those is a script that happens to run on a schedule

  2. The Cross-Cutting Undercurrents (concepts: DAG Orchestration)

    The beginner lesson had 4 roles: source and consumer at the ends, with transform and storage between them. The undercurrent map regroups them into 4 columns that read left to right. Source stays as it was. Ingest is the first transform, the one that pulls data out of the source. Transform covers the remaining steps and the storage they write. Serve is the stored output as consumers read it. The undercurrents run the other way. Each is a concern no single box owns, so each shows up in all 4 colum

  3. When to Split, When to Merge (concepts: DAG Orchestration)

    A DAG, a directed acyclic graph, is one scheduled graph of steps that runs as a unit: the arrows point forward and no step waits on itself. When a step fails, every step downstream of it is skipped, and steps on other branches still run. So a fragile step upstream of an urgent one sits on its critical path. Put too much into 1 DAG and a step nobody cares about ends up upstream of the step everyone cares about. Split into too many and every boundary becomes a handoff that nobody monitors. 3 signa

  4. Build vs Buy at Each Layer (concepts: ELT vs ETL)

    Every layer of a pipeline can be built in-house or bought as a managed service: the connectors that pull from sources, the storage the curated layer sits in, the transform logic that encodes your definitions, the engine that runs that logic, the scheduler that runs the DAGs, and the tools that serve the output. The decision is made per layer, and the same team reasonably builds 1 of the 6 and buys the other 5. Each decision comes down to 3 weights. The 3 weights A worked total cost The listening

  5. Redesigning a Tangled Graph (concepts: DAG Orchestration)

    Here is a graph as it looks after 3 years of growth: 14 steps owned by 3 teams, all on 1 nightly schedule. Each step was added by whoever needed it at the time, reading from whichever step already had the data. Read it the way the intermediate lesson taught, left to right, then look for arrows that point the wrong way. Diagnosis The redesign Capstone Retiring a single table needs a finer list than the products table gives: every reader of that one table, including steps inside the owning DAG, th