ADR 0044: dbt multi-project โ one project per business domain¶
Status: Accepted โ multi-group expansion validated live (sales + marketing in one DAG) Date: 2026-07-03 Companions: ADR 0042 (dbt support), ADR 0043 (TaskGroup split/fused execution), ADR 0024 (parser shim), the editions split (Lite/Pro)
Context¶
A single monolithic dbt project does not scale once several teams and business subjects share it: one CI, one ownership boundary, one ever-growing manifest, and a merge-conflict funnel. The industry answer โ dbt Labs' dbt Mesh โ is to split a warehouse's transformations into domain-oriented projects: one per business subject (sales, marketing, finance), each with its own ownership, CI, and lineage.
Leoflow already admits this at the schema level: dbt_groups in leoflow.yaml is a
map, so a dag.py can embed more than one named group, and separate DAGs trivially
carry separate projects. This ADR records that multiple dbt projects, one per
domain, is a first-class supported pattern โ its guarantees, its best practices, and
its limits โ so the shape is intentional rather than incidental.
Decision¶
Leoflow supports multiple dbt projects, one per business domain, in two shapes:
-
Multiple dbt groups in one DAG.
dbt_group("sales")anddbt_group("marketing")in onedag.py, each configured underdbt_groups.<name>pointing at its own project directory. The compiler expands each group and namespaces its tasks as<group>__<node>(e.g.sales__stg,marketing__orders), so groups never collide and can be wired around shared operators. -
Multiple DAGs, one project each. Each domain DAG compiles independently โ the natural boundary when domains schedule, own, and deploy separately.
# biz/leoflow.yaml
dag_id: biz
dbt_groups:
sales: { project: ./sales, connection: warehouse, granularity: node }
marketing: { project: ./marketing, connection: warehouse, granularity: node }
# biz/dag.py โ two domains, mixed with operators
with DAG("biz", schedule="@daily"):
extract = PythonOperator(task_id="extract", python_callable=pull)
extract >> dbt_group("sales")
extract >> dbt_group("marketing")
Guarantees (validated)¶
- Namespacing.
<group>__<node>โ no cross-grouptask_idcollision. - Independence. Each group has its own
dbt_project.yml, manifest, and profile (connection); one domain's compile never touches another's. - Mixes with operators (ADR 0043). A group's roots depend on its upstream operators; downstream operators depend on the group's leaves.
- Validated live.
sales+marketingin one DAG compiled tostart,sales__raw,sales__stg,marketing__raw,marketing__stgโ both groups expanded, namespaced, and wired.
Best practices โ organizing dbt by domain¶
- One project per domain (sales, marketing, finance): a real ownership boundary, a smaller mental model, independent CI, and clearer lineage than a monolith.
- Group name = domain. It becomes the task prefix (
sales__orders), so the graph reads as lineage at a glance. - One DAG per domain when domains schedule/own/deploy independently โ the common
case. Multiple groups in one DAG when domains share a run cadence or an
orchestration seam (a shared
extractupstream, a sharedpublishdownstream). - Keep projects independent. Share across domains through the warehouse (a
domain reads another's published tables by name), not through cross-project dbt
ref()(see limits). - Config in
leoflow.yaml, notdag.py(ADR 0043): packing (Lite vs Pro), the connection, and granularity change per environment without editing Python. - A connection per domain. Each group may target its own warehouse/connection.
Limits¶
- No cross-project
ref()(full dbt Mesh). Each group is an isolated project with its own manifest; Leoflow does not resolve a model in one project referencing a model in another. Cross-domain dependencies go through the warehouse (published tables) or explicit task edges โ not dbt refs. A future dbt-mesh cross-ref resolution (multi-manifest, version pinning) is out of scope here. - Node-name uniqueness within a project (issue #398): cross-package duplicate node
names need
unique_idnamespacing; todaytask_ids must be unique within a group.
Consequences¶
- Teams own domain projects independently and compose them at the DAG layer, mixed with operators and other domains.
- The
dag.jsonstays a flat, namespaced task graph โ the Go scheduler needs no dbt or mesh awareness; multi-project is entirely a compile-time expansion. - Cross-domain lineage is expressed at the warehouse/table level, not the dbt-ref level โ a deliberate simplification that keeps projects decoupled.
Alternatives rejected¶
- Single monolithic dbt project. Does not scale to multiple teams/domains: one CI, one ownership, one huge manifest, a merge-conflict funnel.
- Full dbt Mesh with cross-project refs now. Powerful but heavy (multi-manifest resolution, cross-project version pinning); deferred until there is real demand โ the warehouse-level sharing above covers the common case.