What you will be able to do
- Place a transformation in the correct layer: staging, intermediate or marts
- Name and group model files the way each layer expects
- Recognise healthy and unhealthy DAG shapes, and fix a circular reference
- Order the steps for developing a new mart and refactoring it into layers
Key concept
Modular layers — A dbt project moves data through layers that build on each other, from many narrow source-conformed models to fewer, wider business-conformed ones. Each transformation lives in exactly one layer, so it is written and changed in only one place.
1.Why a dbt project flows in layers
A dbt project is a graph before it is a set of files. Every ref() adds an edge, and dbt uses those edges to decide the order in which models run. That graph is the DAG. In a clean DAG, the shape itself tells you how the data moves: raw, source-conformed tables come in on the left, and a smaller number of wide, business-conformed entities come out on the right.
Layering is what gives a DAG that shape. The recommended structure has three layers, and each one builds on the layer before it. Staging turns source data into atomic building blocks. Intermediate adds logic, one clear purpose per model, to get those blocks ready to join. Marts combine them into the entities the organisation cares about. The benefit is that each transformation lives in one place, so a definition change happens once instead of in every model that copied it.
You can see the structure in three places: the DAG, the folder tree, and the objects dbt creates in the warehouse. The folder layout counts as an interface to the project, just like the DAG. It should follow the same step-by-step flow, from many source-conformed models to fewer, richer business-conformed ones.
| Layer | Subfolders grouped by | File name pattern | Exposed to end users? |
|---|---|---|---|
| Staging | Source system | stg_[source]__[entity]s.sql | No — building blocks |
| Intermediate | Area of business concern | int_[entity]s_[verb]s.sql | No — kept out of the main production schema |
| Marts | Department or area of concern (once there are enough marts) | Plain-English entity, e.g. customers, orders | Yes — first layer intended for end users |
Checkpoint 1 of 9· Match them up
Match each layer to its job in the flow
Tap a term, then the definition that fits it.
Each layer builds on the previous one. Staging makes the atoms, intermediate prepares them, and marts assemble them into business entities.
“Intermediate — stacking layers of logic with clear and specific purposes to prepare our staging models to join into the entities we want”Source: docs.getdbt.com
2.Staging: one clean model per source table
Staging is the way into the project. Each source table gets exactly one staging model, and staging models are the only ones that call source(). Every later layer reads staging through ref(). [Raw object dependencies](lesson:1.1) explains how the two functions differ. Keeping source() inside one layer gives each raw table a single entry point.
A staging model does only a few kinds of work: renaming columns, casting types, simple computations such as converting cents to dollars, and sorting values into buckets or booleans. Joins and aggregations do not belong here. A join creates duplicated computation and confusing relationships that spread downstream. An aggregation changes the grain, which cuts off access to source rows you will probably need later. To decide whether something belongs in staging, apply the DRY test: if every downstream model would want it, do it once here. The principle is covered in [modularity and DRY](lesson:1.3).
---------- numerics
subtotal as subtotal_cents,
tax_paid as tax_paid_cents,
order_total as order_total_cents,
{{ cents_to_dollars('subtotal') }} as subtotal,
{{ cents_to_dollars('tax_paid') }} as tax_paid,
{{ cents_to_dollars('order_total') }} as order_total,
---------- timestamps
{{ dbt.date_trunc('day', 'ordered_at') }} as ordered_at
from source
)
select * from renamedBase models are the approved exception to the no-joins rule. Sometimes a concept can't be staged cleanly without a join, for example a separate table of deleted records, or several symmetrical sources that need a union. In that case, create base models in a subdirectory. They read the raw data and do the non-joining work, and the staging model then joins them.
Folders and file names follow the source system. A transactional database, Stripe and Snowplow each get their own staging subdirectory. Grouping by loader (Fivetran, Stitch) is too broad to be useful. Grouping by business area (marketing, finance) invites conflicting definitions before any modelling has started. Files are named stg_[source]__[entity]s.sql. The double underscore keeps google_analytics__campaigns unambiguous, and the plural reflects a table with many rows. Staging models are usually materialised as views. They are building blocks rather than tables people query, so views avoid wasting warehouse space; the trade-offs are covered in [core materializations](lesson:1.2).
Checkpoint 2 of 9· Fill the gap
Which function belongs in the first CTE of a staging model?
-- stg_orders.sql
with
source as (
select * from {{ ? ('ecom', 'raw_orders') }}
),Staging is the only layer that reads raw tables, so it is the only layer that uses the source macro. Later layers use ref() to point at staging models.
Source: docs.getdbt.comCheckpoint 3 of 9· Exam question
A teammate's model fails to rebuild in the right order during `dbt run`: the compiled SQL itself is correct, but the model finishes before an upstream model it actually depends on. Which line in this snippet breaks dbt's dependency resolution for the DAG? ```sql with stg as ( select * from {{ ref('stg_orders') }} ), enriched as ( select * from analytics_raw.raw_customers ) select stg.*, enriched.name from stg join enriched on stg.customer_id = enriched.customer_id ```
Correct answer: A — The `enriched` CTE selects directly from `analytics_raw.raw_customers` instead of calling `source()`, so dbt never records a dependency edge for that raw table.
- A. Because that CTE reads a hardcoded schema and table name instead of wrapping it in `source()`, dbt's parser never sees a graph edge for `raw_customers`, so nothing forces this model to wait on anything related to that raw table.
- B. Multiple CTEs inside one model, each referencing a different upstream object, is a completely ordinary pattern; dbt resolves each `ref()`/`source()` call independently and this is not a source of dependency-resolution failure.
- C. Join clauses operate on already-selected CTEs and have no bearing on dbt's graph parsing, which only inspects `ref()` and `source()` calls, not downstream SQL operations like joins.
- D. Column selection in the final `select` has no effect on dependency resolution; dbt builds its graph from `ref()`/`source()` calls during parsing, not from which columns a query later reads.
Sources1
3.Intermediate: purpose-built steps between staging and marts
Small projects can skip the intermediate layer. The Jaffle Shop example goes straight from staging to marts. Larger projects add it so their marts stay readable. Every intermediate model has a single job: preparing staging models for marts.
From this layer on, the project is organised around the business. Subdirectories group models by area of business concern instead of by source system. File names describe what the model does, using verbs: int_[entity]s_[verb]s.sql, as in int_order_items_summed_to_orders. The double underscore drops out because the model now refers to one unified entity, not a system plus an entity. It stays only when an intermediate model still works at source-system level, as in int_shopify__orders_summed.
Intermediate models are used in three common ways:
- Structural simplification: join a manageable number of inputs, typically 4 to 6, into a model that is later joined with a similar one. The mart then joins two models instead of ten. - Re-graining: expand or collapse rows to the right grain. One example is turning each order into one row per item based on its quantity, so you can confirm the grain is correct before mixing it with anything else. - Isolating complex operations: move hard-to-follow logic into its own model, where it is easier to debug and test, and where later models can reference it by a clear name.
Intermediate models are not meant for end users. They should generally stay out of the main production schema. A common default is to make them ephemeral. That is simple, but harder to troubleshoot, because an ephemeral model is inserted into the models that ref it instead of existing on its own. The sturdier option is to build them as views in a custom schema with special permissions.
Checkpoint 4 of 9· Check yourself
A mart joins ten staging models in one query, and reviewers find it hard to follow. What does the layering guidance recommend?
Structural simplification is one of the main reasons intermediate models exist. Each one holds part of the complexity, which makes the mart easier to read and gives more surface for testing. Joins in staging and per-department marts are both anti-patterns.
“rather than have 10 joins in our mart, we can join two intermediate models that each house a piece of the complexity”Source: docs.getdbt.com
Sources3
4.Marts: one model per business entity
Marts are the first layer built for end users. Each mart represents one entity or concept at its own grain, such as an order, a customer or a payment, with one row per instance. Name the file after that entity in plain English: customers, orders.
Marts are wide on purpose. In a project without the Semantic Layer, the guidance is to denormalise heavily. Storage is cheap and compute is expensive, so copying order data into the customers mart costs less than re-joining it every time someone asks a question.
Each concept gets one mart. Building finance_orders and marketing_orders is an anti-pattern, because two teams end up with two different answers, which is exactly what layering is meant to prevent. Sometimes a department really does need a different concept, for example when finance reports revenue to the government differently from how the company measures it day to day. In that case, name it as a separate concept (tax_revenue alongside revenue) rather than as one department's version of the same thing. Department subfolders are fine once the layer grows, but with fewer than about ten marts you rarely need them.
Checkpoint 5 of 9· Check yourself
Finance must report revenue to the government in a way that differs from how the company tracks revenue day to day. Which pair of marts fits the guidance?
Different needs are fine, but they should be modelled as clearly separate concepts, not as departmental views of the same concept.
“tax_revenue and revenue not finance_revenue and marketing_revenue”Source: docs.getdbt.com
Checkpoint 6 of 9· Exam question
Your team inherited a dbt project where the DAG is hard to reason about: a few marts models directly reference dozens of raw source tables, some models hardcode the same schema in three different places, and there is no staging layer. Which of the following changes would genuinely make the DAG cleaner and easier to trace? (Select all that apply.)(Select 3)
Correct answers: A, B, C — Introduce a staging layer so every source table is wrapped once in a `stg_` model, and have downstream models call `ref()` against that staging model instead of the raw source directly.; Replace the hardcoded schema references with calls to `source()` in each place they occur, so dbt tracks the dependency and can run freshness checks against the declared source.; Add an intermediate layer that isolates join and aggregation logic into single-purpose models, so each transformation exists in exactly one place instead of being repeated across marts.
- A. A single staging model per source is the standard fix for scattered raw-table references: every downstream model then depends on one well-known node instead of duplicating source logic, which is exactly what makes the DAG traceable.
- B. Wrapping hardcoded schema references in `source()` restores the missing DAG edges and unlocks freshness checks, directly addressing the root cause of the untraceable dependencies described.
- C. An intermediate layer that owns join and aggregation logic in one place removes duplicated transformations from the marts layer, which is a core part of keeping a DAG modular and easy to follow.
- D. Making every layer ephemeral removes queryable, debuggable checkpoints and hides lineage inside compiled CTEs rather than exposing it as distinct DAG nodes, which makes the project harder to reason about, not easier.
- E. Chaining marts models directly into each other encourages deep, hard-to-trace dependency chains and raises the risk of circular references between business-facing models, which works against a clean DAG.
- F. Deleting `.yml` property files removes documentation and tests rather than restructuring dependencies; it does not change how the DAG is built and only hides information that helps trace it.
Sources4
5.Reading DAG shape: the arrowhead and the cycle
Up to the marts layer, a healthy DAG looks like an arrowhead pointing right: many narrow source-conformed models narrowing into fewer, wider ones. For a single model, the rule of thumb is many arrows in, few arrows out. Several inputs to a post-staging model are normal. A model that feeds several downstream models is a warning sign worth questioning, although it is not an absolute ban.
dbt rejects a cycle outright. It uses ref() to work out the run order by building an acyclic graph. If two models reference each other, there is no valid order, and dbt stops with an error that names the loop.
Encountered an error:
Found a cycle: model.jaffle_shop.customers --> model.jaffle_shop.stg_customers --> model.jaffle_shop.customers
Your dbt DAG is not acyclic, and needs to be fixed!To fix a cycle, change the ref calls so the dependency runs in one direction only. In a layered project, a staging model never refs a mart. When a model really does need to read its own existing relation, use {{ this }} instead of a ref to itself. this is equivalent to a ref to the current model, and it avoids the circular dependency.
Clean structure also helps when you select what to run. Because folders match the layers, dbt build --select staging+ rebuilds the staging layer and everything downstream of it after fresh data arrives. The command itself is covered in [dbt commands](lesson:1.4).
Checkpoint 7 of 9· Check yourself
The orders model needs to select from the relation it previously built in the warehouse. How should it reference that relation?
A ref to itself creates a cycle. this stands for the current model and is the documented way to avoid that circular dependency.
“If you need to reference the current model, use the {{ this }} variable instead.”Source: docs.getdbt.com
Checkpoint 8 of 9· Exam question
A finance analyst needs a one-off check that isn't a general column-level rule: the total of the `fct_payments` model's `amount` column, summed across the whole table, must never come out negative. No other model or column needs this exact check. What should you add?
Correct answer: A — A singular test: a standalone SQL file in `tests/` that selects rows where the aggregated payment total is negative, since this belongs to one table alone.
- A. A one-off, table-specific aggregate assertion with no reuse case is exactly what singular tests are for: a standalone `.sql` file under `tests/` that returns the failing rows for this exact business rule.
- B. `not_null` only checks for missing values and has no awareness of sign or aggregation, so it cannot detect a negative summed total, which is a different kind of assertion entirely.
- C. Building a reusable, parameterized macro is unnecessary overhead for a check the scenario explicitly says only applies to this one table, and it adds project complexity with no reuse benefit.
- D. `accepted_values` validates that a column's values fall within a fixed, enumerable list, which does not fit a continuous numeric column or an aggregate-level rule like a summed total staying non-negative.
6.Building the flow: start from the mart, refactor upstream
The order you read a DAG in, left to right, is not the order you write it in. Development usually starts from the output and works back, and the clean layering comes at the end, as a refactor. The usual sequence is:
1. Mock up the desired output in a spreadsheet so stakeholders agree on the goal. 2. Write the SQL that produces that output, and identify the tables it uses. 3. Make sure every atomic piece it needs is staged in the project. 4. Combine the staged pieces into the mart using the logic you wrote. 5. With the mart working, refactor by splitting its logic and moving parts back upstream into intermediate models.
The last step is where the DAG gets clean. A mart that works but holds every join and calculation in one file is hard to read and has only one place to test. Moving pieces into intermediate models keeps each model readable, makes the DAG's story clear, and gives you more places to add tests.
Checkpoint 9 of 9· Put it in order
Put the steps for developing a new mart in the recommended order
- 1.Refactor by moving logic back upstream into intermediate models
- 2.Write the SQL for that output and identify the tables involved
- 3.Mock up the output design in a spreadsheet with stakeholders
- 4.Combine the staged pieces into the mart
- 5.Stage all the atomic pieces the logic depends on
Development starts from the agreed output, stages what it needs, builds the mart, and only then refactors logic into intermediate models.
“we should start by mocking out a design in a spreadsheet”Source: docs.getdbt.com
Sources1
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Staging subfolders should be split by business area, such as marketing and finance, so each team owns its own models.Why is that wrong?
Staging is grouped by source system. Splitting by business area this early leads to overlapping and conflicting definitions of the same entity.
Covered in Staging: one clean model per source table
2.Joining two source tables in a staging model is fine, because staging is where data gets cleaned up.Why is that wrong?
Staging does renaming, casting, simple computations and categorising. Joins are almost always wrong there; base models are the narrow exception.
Covered in Staging: one clean model per source table
3.Intermediate models should be built in the main production schema so analysts can query them directly.Why is that wrong?
Intermediate models are not intended for end users. They are made ephemeral or kept in a separate custom schema with special permissions.
Covered in Intermediate: purpose-built steps between staging and marts
4.A model that feeds many downstream models is a sign of good reuse.Why is that wrong?
The rule of thumb is many inputs, few outputs. Several arrows coming out of a post-staging model are a warning sign to examine.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“from a wide variety of source-conformed models into fewer, richer business-conformed models”
↩︎ Why a dbt project flows in layers“Staging models are the only place we'll use the source macro”
↩︎ Staging: one clean model per source table“aggregations entail grouping, and we're not doing that at this stage”
↩︎ Staging: one clean model per source table“Common use cases include joining in separate delete tables or unioning disparate but symmetrical sources.”
↩︎ Staging: one clean model per source table“the double underscore between source system and entity helps visually distinguish the separate parts”
↩︎ Staging: one clean model per source table“staging models should typically be materialized as views”
↩︎ Staging: one clean model per source table“we can easily run dbt build --select staging+”
↩︎ Reading DAG shape: the arrowhead and the cycle“developing models does not typically move linearly through the DAG”
↩︎ Building the flow: start from the mart, refactor upstream“By splitting the logic up and moving parts back upstream into intermediate models”
↩︎ Building the flow: start from the mart, refactor upstream“By breaking things up too early, we open ourselves up to creating overlap and conflicting definitions”
↩︎ Exam trap 1“joins are almost always a bad idea here”
↩︎ Exam trap 2“we should start by mocking out a design in a spreadsheet”
↩︎ Checkpoint - 2.https://docs.getdbt.com/docs/build/sql-modelsOfficial docs
“Determine the order to run the models by creating a dependent acyclic graph (DAG).”
↩︎ Why a dbt project flows in layers - 3.
“Larger projects often add an intermediate layer between staging and marts.”
↩︎ Intermediate: purpose-built steps between staging and marts“The best guiding principle is to think about verbs”
↩︎ Intermediate: purpose-built steps between staging and marts“Intermediate models are often used to fan out or collapse models to the right composite grain”
↩︎ Intermediate: purpose-built steps between staging and marts“default to intermediate models being materialized ephemerally”
↩︎ Intermediate: purpose-built steps between staging and marts“we ideally want our DAG to look like an arrowhead pointed right”
↩︎ Reading DAG shape: the arrowhead and the cycle“several arrows coming out is a red flag”
↩︎ Reading DAG shape: the arrowhead and the cycle“Intermediate models should generally not be exposed in the main production schema.”
↩︎ Exam trap 3“allowing multiple inputs to a model, but not multiple outputs”
↩︎ Exam trap 4“rather than have 10 joins in our mart, we can join two intermediate models that each house a piece of the complexity”
↩︎ Checkpoint - 4.
“all our marts are meant to represent a specific entity or concept at its unique grain”
↩︎ Marts: one model per business entity“In a project without the Semantic Layer we recommend you denormalize heavily”
↩︎ Marts: one model per business entity“finance_orders and marketing_orders is typically considered an anti-pattern.”
↩︎ Marts: one model per business entity“If you have fewer than 10 or so marts you may not have much need for subfolders”
↩︎ Marts: one model per business entity“tax_revenue and revenue not finance_revenue and marketing_revenue”
↩︎ Checkpoint - 5.https://docs.getdbt.com/guides/debug-errorsOfficial docs
“Update the ref functions to break the cycle.”
↩︎ Reading DAG shape: the arrowhead and the cycle“Your dbt DAG is not acyclic, and needs to be fixed!”
↩︎ Prediction“If you need to reference the current model, use the {{ this }} variable instead.”
↩︎ Checkpoint - 6.
“is a neat way to avoid circular dependencies”
↩︎ Reading DAG shape: the arrowhead and the cycle
Also cited
“stacking our transformations in optimized, modular layers means we can apply each transformation in only one place”
↩︎ Key concept“Intermediate — stacking layers of logic with clear and specific purposes to prepare our staging models to join into the entities we want”
↩︎ Checkpoint