In my early days of using dbt, everything was simple. I had one project, three models, and I was the only one touching the code. But as I started working with larger organizations, I saw a recurring pattern: the ‘dbt sprawl.’ What starts as a clean project quickly turns into a nightmare of 500+ models, circular dependencies, and merge conflicts that take hours to resolve.
Implementing dbt best practices for large teams isn’t just about writing cleaner SQL; it’s about creating a governance framework that allows ten different engineers to commit code simultaneously without breaking the production dashboard. Whether you are just starting to scale or are currently drowning in a monolithic project, these strategies will help you regain control.
1. Implement a Strict Layering Architecture
The biggest mistake I see large teams make is mixing business logic with cleaning logic. To avoid this, you must enforce a rigid folder structure. I recommend the following hierarchy:
- Staging (stg_): 1:1 mapping to source tables. Only renaming and basic type casting here.
- Intermediate (int_): Where the complex joins and transformations happen. These are not exposed to end-users.
- Marts (fct_ / dim_): Final, gold-standard tables optimized for BI tools.
If you’re new to this setup, I suggest checking out this dbt tutorial for beginners to get the basics of model creation down before scaling your architecture.
2. Use dbt Mesh and Project Splitting
When a single project becomes too large, the compilation time kills productivity. In my experience, once you hit 20+ developers, a monolith becomes a bottleneck. This is where dbt Mesh comes in.
By splitting your project into multiple smaller projects (e.g., finance_dbt, marketing_dbt, core_dbt), you can define Public Models. This allows the Finance team to depend on a Core model without having to compile the entire company’s DAG. This is a critical part of how to build a modern data stack that actually scales.
3. Enforce a Standardized Naming Convention
In a large team, user_final_v2_fixed.sql is a crime. You need a shared dictionary. I’ve found that the most successful teams use a prefix system:
-- GOOD
stg_stripe__payments.sql
int_order_items_joined.sql
fct_monthly_revenue.sql
Consistency allows any developer to look at a model name and instantly know its purpose and location in the pipeline without opening the file.
4. Leverage dbt Cloud or Airflow for Targeted Runs
Running dbt run on the entire project in a large environment is a recipe for warehouse cost spikes and long wait times. Teach your team to use selector flags.
Instead of running everything, use state-based execution: dbt run --select state:modified+. This only runs models that have changed and their downstream dependencies, saving hours of compute time daily.
5. Mandatory Documentation and YAML Tests
Code is read more often than it is written. For large teams, a model without a description in the schema.yml should not be merged. I treat documentation as a first-class citizen.
Beyond descriptions, every primary key must have a unique and not_null test. As shown in the image below, a well-documented DAG allows new joiners to onboard in days rather than weeks.
6. Use Macros for DRY (Don’t Repeat Yourself) Code
If you see the same 20 lines of SQL for currency conversion across five different models, it’s time for a macro. Macros centralize logic, meaning when the business logic changes, you update it in one place rather than hunting through 50 files.
7. Implement a Robust CI/CD Pipeline
Never let a developer push directly to main. I recommend a pipeline that:
1. Spawns a temporary schema (Slim CI).
2. Runs only the modified models.
3. Executes dbt test on those models.
4. Blocks the merge if any test fails.
8. Modularize with CTEs (Common Table Expressions)
I’ve seen models in large teams that are 1,000 lines of nested subqueries. They are impossible to debug. Enforce a style where all sources are imported as CTEs at the top of the file.
with payments as (
select * from {{ ref('stg_payments') }}
),
orders as (
select * from {{ ref('stg_orders') }}
),
final as (
select ... from payments join orders ...
)
select * from final
9. Establish an ‘Ownership’ Matrix
When a model fails at 3 AM, who gets the alert? In large teams, shared ownership is actually no ownership. Use the meta tag in your YAML to assign owners:
models:
- name: fct_revenue
meta:
owner: "@finance_data_team"
slack_channel: "#alerts-finance"
10. Regular ‘DAG Pruning’ Sessions
Data debt accumulates. Every quarter, I lead a session to identify unused models. If a model isn’t being queried by any BI tool or downstream model, delete it. A leaner DAG is a faster, more reliable DAG.
Common Mistakes When Scaling dbt
- Over-Engineering: Creating 10 intermediate layers for a simple transformation.
- Ignoring the Warehouse: Writing dbt code that looks clean but creates massive Cartesian products in Snowflake or BigQuery.
- Manual Deployment: Manually running commands from a local terminal instead of using an orchestrator.
Measuring Success
How do you know if these dbt best practices for large teams are working? Track these three KPIs:
- CI Pipeline Duration: Is it getting faster or slower as you add models?
- Time to Onboard: How long does it take a new engineer to push their first production model?
- Test Failure Rate: Are you catching bugs in CI, or are they reaching the BI dashboard?