dbt Mesh: When to Split One Project into Many
When should you split one dbt repo into many? Concrete signals, what breaks in production, and a safe migration path—plus CI, contracts, and ownership.
Your team is feeling the drag: slow parses, flaky CI, long builds, and unclear ownership in one repo. dbt mesh is the pattern of splitting a monolith into multiple projects with explicit interfaces and versioned contracts. The short answer: stay monolithic until you have at least two real domains with separate roadmaps and 5+ engineers stepping on each other. Split when ownership, not tooling, is the bottleneck. Expect costs—more repos, stricter interfaces, and new CI—but the payoff is safer change and faster delivery inside each domain. If your pain is primarily performance, tune first. If the pain is coordination and broken promises at boundaries, a mesh moves the constraint from interpersonal negotiation to code and policy.
When to split: concrete signals and real costs
Teams ask for more repos when pain peaks. First separate org issues from tuning. If you haven’t profiled parse time, tightened selectors, or fixed incremental logic, do that before fragmentation. If your data platform has multiple domain roadmaps and your queue blocks unrelated work, you’re ready to carve edges. This is about operating data at scale more sanely, not chasing trends.
| Signal | Split now? | Notes |
|---|---|---|
| Two domains (Finance, Marketing) with distinct SLOs/on-call | Yes | Conflicting priorities stall the queue. |
| Frequent downstream breakages from ad hoc model use | Yes | Publish a small interface; hide internals. |
| PR builds block for hours | Usually | Smaller DAGs cut blast radius per change. |
| dbt parse > 60–120s and growing | Maybe | Fix selectors/state before splitting. |
| Centralized data team of ≤4 engineers | No | Monolith remains simpler and faster. |
- Inter-project dependencies and version pins replace implicit sharing.
- Breaking changes require deprecation windows and release notes.
- Orchestrating joins across repos complicates the workflow.
- More governance: who owns a public contract, who approves schema changes.
If your primary pain is repo performance, start with an audit and refactor before you fragment ownership. See dbt Repo Performance and dbt run slow? Why it happens and 7 fixes. Those fixes apply across a modern data stack as much as they do within a single project.
Designing boundaries: domains, interfaces, and architecture
Split along domains that own their inputs and semantics. A good boundary yields a small, stable interface and a big, fast-moving interior. In practice:
- Domain projects own ingestion-to-mart for their area. Example: Finance owns revenue and cost marts; Marketing owns campaign attribution from its data source.
- Each interface exposes a handful of public tables with contracts and clear owners. Treat each as a data product, not a grab bag.
- Shared dimensions (dates, geography) live in a “foundation” project or are duplicated intentionally with precedence rules.
Keep the architecture boring:
- One warehouse schema per project environment for isolation—dev, staging, and prod function as virtual data environments.
- Consistent naming:
<domain>__<subject>__<grain>on public models; keep the data model at the interface stable. - Thin interfaces: don’t leak Finance business logic into Marketing.
Team-size heuristics from production: a centralized data team of 3–4 stays in one repo. At 6–12 engineers across two domains with independent backlogs, start marking interfaces with contracts and access, then split. Over 12 with decentralized data ownership, expect multiple projects per domain over time. This tracks the core principles and principles of data mesh—domain ownership, data as a product, self-serve platform, and federated governance—without ceremony-heavy data architectures.
Access modifiers, contracts, and versioning at the edges
Boundaries only work if edges are enforced. Three tools matter most, and they’re effective even inside one repo before you split to multiple dbt projects.
- Access modifiers to control visibility of dbt models:
# models/finance/public/orders.sql
{{ config(access='public') }}
select ...
# models/finance/staging/_stg_orders.sql
{{ config(access='private') }}
select ...
- Model contracts to freeze schemas at interfaces:
# models/finance/public/orders.yml
version: 2
models:
- name: orders
config:
contract:
enforced: true
columns:
- name: order_id
data_type: int
- name: order_date
data_type: date
- name: total_amount
data_type: numeric
With contracts enforced, accidental column drops or type changes fail fast in CI. If you’re formalizing agreements with upstream platform teams, pair this with a written data contract; field notes: Data Contracts in Practice.
- Versioned models to manage breaking changes between projects:
# models/finance/public/customer_orders.sql
-- v1 (deprecated but still built)
{{ config(version=1, access='public') }}
select ...
-- v2 (new schema)
{{ config(version=2, latest=True, access='public') }}
select ...
Run both versions during a grace period. Pin consumers to a specific version, communicate a cutoff date, then retire the old one. That keeps data lineage clean and avoids the Friday surprise where a consumer silently picks up a breaking change.
Dependencies across projects and cross-platform mesh
There are two reliable patterns for dependencies between projects:
- Package installs (Core and Cloud): install the producer repo and use a two-arg ref.
# packages.yml in the marketing project
packages:
- git: "https://github.com/your-org/finance-dbt.git"
revision: main
# models/marketing/marts/campaign_roi.sql
select
m.campaign_id,
sum(spend) as spend,
sum(revenue) as revenue
from {{ ref('finance', 'orders') }} m
...
- Artifact deferral (Cloud-friendly): orchestrate so the consumer runs after the producer; use
--deferto resolve references to the producer’s last successful state.
# Consumer build
dbt build --select marketing+ --defer --state s3://artifacts/finance/prod
Is it available in Core? Yes—the building blocks (access control, contracts, versions, packages) are in Core. dbt Labs improves operations in dbt Cloud and dbt Explorer with inter-project lineage and environment management. That visibility helps when you’re operating cross-platform mesh patterns: you still need explicit pipes to share data across warehouses (shares, external tables, or copies) and you schedule the handoff. Treat dev, staging, and prod as separate, controlled environments; your lineage and artifacts become the handshake.
Ownership and governance that actually scales
Mesh fails when “everybody owns everything” or “nobody owns anything.” Make these explicit on day one:
- Domain ownership: who merges, pages, and approves interface changes—name people, not teams.
- Public catalog: a short, curated list per domain with service levels and contacts.
- Change policy: minimum deprecation window, which versions build by default, and how consumers are notified.
- Review gates: breaking-change PRs require sign-off from all downstream owners.
Governance without bloat: keep a single lightweight RFC template for schema changes and a weekly sync among domain leads. Automate the rest. Tags like pii or restricted on columns, plus tests, let you enforce data governance checks in CI. This is how you scale data quality without centralizing every decision. In an enterprise, keep a small central platform group to run shared services while domain teams own analytics logic. That split respects data practitioners who know the semantics and avoids a bottlenecked central authority.
CI/CD, DAG joins, and state: what breaks and how to fix it
Splitting increases CI complexity. The mistakes we see most in data engineering:
- Rebuilding the world in every PR: use selectors and state to limit scope; keep the workflow fast for feedback.
- Ignoring inter-repo DAG joins: connect runs via artifacts and schedules.
- Skipping contract checks:
dbt testanddbt buildwith contracts enforced catch schema drift.
# .github/workflows/dbt.yml
- name: Build changed nodes with state
run: |
dbt deps
dbt parse
dbt build \
--select state:modified+ \
--defer --state s3://artifacts/prod \
--fail-fast
For orchestration, keep DAGs readable. In Airflow, have a producer DAG publish artifacts and a consumer DAG wait on them:
from airflow import DAG
from airflow.operators.bash import BashOperator
with DAG("finance_build", schedule_interval="@hourly", start_date=...):
build = BashOperator(
task_id="build",
bash_command="dbt build --project-dir finance --profiles-dir . && aws s3 cp target s3://artifacts/finance/prod --recursive"
)
with DAG("marketing_build", schedule_interval="@hourly", start_date=...):
wait = BashOperator(
task_id="wait_for_finance",
bash_command="aws s3 ls s3://artifacts/finance/prod/manifest.json || exit 1"
)
build = BashOperator(
task_id="build",
bash_command="dbt build --project-dir marketing --defer --state s3://artifacts/finance/prod"
)
wait >> build
Want a turnkey starting point that catches issues before prod? Borrow from our write-up: dbt CI/CD That Catches Problems Before They Reach Prod. Those are pragmatic best practices you can adapt to your data stack and governance rules.
Migration path: from one repo to multiple projects without outages
Don’t jump to many repos overnight. De-risk with an incremental path that works whether you self-host or run dbt Cloud.
- Stabilize the monolith: prune dead nodes, wire freshness/tests to critical paths, speed up builds. If you’re moving to managed tooling, plan it with dbt Cloud Migration.
- Introduce boundaries in-place: add
access, declare contracts, and tag public interfaces. Start versioning where you expect schema churn. - Carve out a pilot domain: create a new repo and install it as a package back into the monolith. Point a few consumers at it.
# In the monolith's packages.yml
packages:
- git: "https://github.com/your-org/finance-dbt.git"
revision: main
- Flip refs gradually: change
ref('orders')toref('finance', 'orders')area by area, with CI and canaries. - Wire inter-project CI: publish producer artifacts; run consumers with
--defer; validate data lineage in Explorer. - Retire duplicates: after a defined window, remove the old models and update docs. Use dbt Explorer to confirm fan-out.
| Repo strategy | Pros | Trade-offs |
|---|---|---|
| Mono-repo (multiple projects) | Single PR across producer/consumer; shared tooling | Still need per-project selectors; broader reviews |
| Multiple repos (one per project) | Isolation and clear ownership | Requires version pinning and inter-repo orchestration |
This migration pattern fits most implemented dbt estates. Whether you’ve already implemented dbt or you’re planning a new dbt implementation, keep dev/staging/prod clean, treat schemas as environments, and avoid surprises. It also plays well with tools like dbt in a broader ecosystem and the modern data stack you already run.
Practical FAQ: dbt Mesh in the real world
What is dbt data mesh?
A practical application of data mesh architecture using dbt: domain-aligned projects expose a few public tables with contracts and versions; other domains consume them via refs and orchestrated handoffs.
Is dbt mesh available in dbt Core?
Yes for essentials (access, contracts, versions, packages). dbt Cloud and dbt Explorer improve inter-project lineage and scheduling.
What are the 4 pillars of data mesh?
Domain ownership, data as a product, self-serve platform, and federated governance. The first two map directly to how you use dbt; the latter two live in your platform and processes.
What is dbt vs Snowflake?
dbt manages code, testing, and data transformation. Snowflake runs the SQL and stores data. They complement each other.
SQLMesh or dbt?
Prefer SQLMesh when you need a planning/diff-first approach with heavy Python. We often pick dbt for ecosystem, dbt community, and Cloud features; it fits most analytics workflows. Mixed stacks are fine.
How does it work across platforms?
Move or share the data, then build. The coordination is orchestration and artifacts, not cross-warehouse SQL. That’s the reality of cross-platform mesh.
How to adopt quickly?
Pick one boundary, mark public nodes, turn on contracts, and add deferred CI. If you’re moving off self-hosted orchestrations, we can help stabilize first.
Your next step: pick one boundary, mark public models, enable contracts, and set up a deferred CI build. If you want a checkpointed plan with production guardrails, start a project with Vertex.
About the author
Eric Provencio — Analytics engineer who has built and run production data platforms for Disney, Hulu, Nike, Peloton, Gopuff, and Kaplan. Founded Vertex Data Consulting to do the deep work most data teams never find time for: dbt Cloud migrations, repo performance, Airflow reliability, and AI agents that actually touch the stack.