$ dbt build --select +mart_revenueRunning with dbt=1.9.0Found 3 models, 9 tests, 2 sources1 of 5 OK created view model stg_orders ......... [OK 0.42s]2 of 5 PASS not_null_stg_orders_order_id ......... [PASS 0.11s]3 of 5 OK created table model int_order_items .. [OK 1.08s]4 of 5 OK created table model mart_revenue ..... [OK 0.94s]5 of 5 PASS unique_mart_revenue_revenue_key ...... [PASS 0.13s]Completed successfullyDone. PASS=5 WARN=0 ERROR=0 SKIP=0 TOTAL=5
What is dbt model development?
dbt model development is the practice of writing modular SQL transformations, organised into staging, intermediate, and mart layers, with automated tests and generated documentation, so an analytics team can trust and trace every metric back to its source.
The problem
Symptoms of a project that outgrew its structure
Most dbt projects start clean. They don't stay that way.
A project starts with eight models and clear naming conventions. It grows to two hundred, with a couple of people left who remember why int_orders_v2_final exists, CI that passes while the numbers are wrong, and a warehouse bill that climbs every quarter.
This is a common failure mode in growing dbt projects, not a rare one, and it's fixable with the same layering discipline that would have prevented it in the first place.
The project has two hundred models and nobody is sure which ones anything still reads from.
One model breaks, five dashboards go quietly wrong, and nobody gets paged.
Onboarding an analytics engineer takes a month, because the conventions only exist in someone's head.
Our approach
Every model lives in one of three layers
Staging models map one-to-one with source tables: renamed, cast, and nothing else. Intermediate models hold the joins and the business logic, built to be reused and never queried directly by a BI tool. Marts are the final, documented tables your dashboards and stakeholders actually read.
Every model gets a primary-key test and a not-null test at minimum. Every project gets dbt docs generation and a CI pipeline that runs dbt build on each pull request. This isn't a stylistic preference. It's what stops the failure modes above from coming back.
A dbt project rarely lives alone. If yours needs to sit inside a broader ingestion and warehousing setup, see Data Platform Architecture.
models/
staging/← 1:1 with sources, renamed & cast
intermediate/← business logic, joins
marts/← final metrics, BI-ready
tests/
macros/
dbt_project.yml
Our methodology
How we structure dbt models
01
Staging layer
stg_*
One model per source table. Raw data cleaned, renamed, and cast to the right types. No joins, no business logic. This layer is what makes everything downstream stable and refactorable.
One source, one model
Type casting and renaming only
Tested for not-null and uniqueness
02
Intermediate layer
int_*
Joins, deduplication, and business logic live here, with ref() dependencies declared explicitly. This is where data quality issues get caught before they reach analysts.
Business logic encapsulated
Explicit ref() dependency graph
Uniqueness and referential integrity tests
03
Mart layer
fct_*dim_*
Analytics-ready fact and dimension tables, designed for direct BI tool consumption, with metric definitions aligned to how the business actually measures performance.
Kimball-style facts and dimensions
Metric-aligned naming
Documentation generated from the project itself
What we deliver
Six capabilities, one coherent standard
dbt project build
Greenfield setup: naming conventions, layering strategy, and a dbt_project.yml that still makes sense in year two.
dbt rescue
Existing projects: untangling circular refs, adding coverage to untested load-bearing models, and fixing slow builds caused by missing incremental strategies.
Testing & documentation
Generic and singular tests, full dbt docs generation, and source freshness checks on the tables everything else depends on.
CI/CD for dbt
dbt build on every pull request, state-deferred runs so only affected models rebuild, and a promotion path into production.
Performance & cost
Incremental materialisation, partitioning and clustering strategy, and removing the work a build repeats on every single run.
Warehouse & AWS plumbing
Schema migration, versioned DDL, and the Lambda, ECS, S3, and Terraform layer a dbt project needs in order to actually run in production.
Where we work
The warehouse layer underneath the models
Redshift
Production dbt.
dbt in production: staging through mart layers, medallion architecture
Schema migration and versioned DDL
AWS-side plumbing: Lambda, ECS, S3, Terraform
Snowflake
Schema and platform layer, stated with its real boundary.
Schema migration and versioned DDL in production
Hybrid tables (Unistore) in production
COPY and external stages for ingestion
What we don't claim: senior-level warehouse operations on Snowflake. Credit management, clustering strategy, RBAC hierarchy design and Snowpipe are real, current work for someone else. We would rather say so up front than have a project find out midway through.
Credibility, not claims
What our dbt models look like
models/staging/stg_orders.sql
-- One model per source table: rename, cast, nothing else.with source as (
select * from{{ source('ecommerce', 'orders') }}
),
renamed as (
select
order_id,
customer_id,
cast(created_at astimestamp) as order_created_at,
status as order_status
from source
)
select * from renamed
models/intermediate/int_order_items.sql
-- Joins and business logic, on ref() only, never raw tables.with orders as (
select * from{{ ref('stg_orders') }}
),
order_items as (
select * from{{ ref('stg_order_items') }}
),
joined as (
select
o.order_id,
o.customer_id,
o.order_created_at,
oi.product_id,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price as line_total
from orders o
join order_items oi using (order_id)
)
select * from joined
One model doing four jobs, split into three that each do one
models/orders.sql, before
select
o.id as order_id,
o.customer_id,
c.email,
o.total / 100.0as total_usd,
casewhen o.status = 'A'then'active'when o.status = 'C'then'cancelled'else'unknown'endas status,
o.created_at
from raw.orders o
leftjoin raw.customers c
on o.customer_id = c.id
where o.deleted_at isnull
staging + marts + tests, after
-- staging/stg_orders.sqlselect
id as order_id,
customer_id,
total / 100.0as total_usd,
status_code as status
from{{ source('raw', 'orders') }}where deleted_at isnull-- marts/fct_orders.sqlselect
o.order_id,
o.customer_id,
c.email,
o.total_usd,
o.status
from{{ ref('stg_orders') }} o
join{{ ref('stg_customers') }} c
using (customer_id)
How we work
A structured, de-risked process
01
Audit the existing project, or scope a greenfield one
We map every model, source, and test that exists today, or the sources you need to model from scratch.
3 to 5 days
02
Design the layering and naming conventions
Staging, intermediate, and mart boundaries, plus naming standards your team will actually follow.
About a week
03
Build and test the models
Models and tests are built and reviewed layer by layer, not all at once at the end.
2 to 6 weeks, by scope
04
Wire up CI/CD
dbt build in your existing CI, with state-deferred runs once the project is large enough to benefit.
3 to 5 days
05
Handover and documentation walkthrough
A working session with your analytics engineers, so the project doesn't depend on us to maintain.
2 to 3 days
Why layering matters
What changes when a project has real structure
Ad-hoc SQL in the warehouse
dbt without conventions
Layered dbt project
Lineage
Undocumented, lives in people's heads
Partially traceable through ref()
Full DAG, generated from the project
Test coverage
Manual spot checks, after the fact
A few tests, on whichever models someone remembered
Not-null and uniqueness on every key, enforced in CI
Changing a definition
Find and edit every query that uses it
Edit several models and hope nothing downstream breaks
Change one staging model; everything downstream follows
Onboarding an engineer
Ask whoever wrote it
Read the models and infer the rules
Read the conventions, then the docs the project generates
Ways to work together
How we engage
Currently offered
dbt pipeline audit
Fixed-price, five business days
Top 10 issues, ranked by risk
2-3 models refactored as a working example
A written report your team can act on without us in the room
A dbt model is a SQL (or Python) file that defines a transformation as a select statement. dbt compiles it, manages the dependency graph between models via ref(), and materializes it in your warehouse as a table or view.
dbt Core is the open-source CLI you run yourself, in your own CI/CD. dbt Cloud is a hosted service that adds scheduling, an IDE, and a managed environment on top of the same underlying engine. We work with both, depending on what a project already has in place.
A greenfield build is typically four to eight weeks depending on the number of source systems and the complexity of the business logic. A rescue of an existing project, auditing and restructuring what's already there, is usually two to four weeks. The fixed-price audit sprint above is five days and answers the question of where to start.
Production dbt work on Redshift, and Snowflake at the schema and platform layer: schema migration, versioned DDL, and hybrid tables. See the warehouse section above for exactly what that does and doesn't cover.
Knowledge transfer is built into every engagement: documentation generated from the project itself, plus a walkthrough session so your team can maintain what's delivered without us in the room. A standalone training engagement can be scoped on request.