CertSafari
    DBT Analytics Engineering· Lessons

    Domain 1 · Lesson 5/31

    Layering dbt models into a clean DAG

    Creating a logical flow of models and building clean DAGs

    16 min read
    3.23% of exam
    7 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    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.

    How each layer is grouped, named and exposed
    LayerSubfolders grouped byFile name patternExposed to end users?
    StagingSource systemstg_[source]__[entity]s.sqlNo — building blocks
    IntermediateArea of business concernint_[entity]s_[verb]s.sqlNo — kept out of the main production schema
    MartsDepartment or area of concern (once there are enough marts)Plain-English entity, e.g. customers, ordersYes — 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.

    Sources12

    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).

    The rename CTE of stg_orders: casting cents to dollars and truncating a timestamp, with no joins or groupingsql
            ---------- 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 renamed

    Base 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') }}
    
    ),

    Checkpoint 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 ```

    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?

    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?

    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)

    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.

    The error dbt raises when two models ref each othertext
    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?

    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?

    Sources3561

    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. 1.Refactor by moving logic back upstream into intermediate models
    2. 2.Write the SQL for that output and identify the tables involved
    3. 3.Mock up the output design in a spreadsheet with stakeholders
    4. 4.Combine the staged pieces into the mart
    5. 5.Stage all the atomic pieces the logic depends on

    Sources1

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 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. 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.

      Covered in Reading DAG shape: the arrowhead and the cycle

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 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. 2.
      “Determine the order to run the models by creating a dependent acyclic graph (DAG).”
      ↩︎ Why a dbt project flows in layers
    3. 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. 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. 5.
      “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

    Also cited

    Ready to test yourself?

    Practise the 11 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.