See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
Where isolation actually happens
The metastore is regional and shared. It is the top of the hierarchy, but it is not the isolation boundary you should be designing around, because a single metastore is meant to serve many workspaces in the same region. Treating one metastore per team as the isolation strategy creates a sharing problem you will spend the next year working around.
The catalog is the primary unit of isolation, and that is not an opinion of ours: the Databricks Unity Catalog best practices page states it directly, describing catalogs as the primary unit of data isolation in a typical Databricks governance model. Grants, ownership, and default storage all attach cleanly at the catalog level, a catalog can be bound to specific workspaces so certain data is only processed there, and the three-level namespace keeps catalog.schema.table readable in every query. When somebody asks where the boundary is, the answer should be the catalog.
The schema is the unit of team ownership inside a boundary. The pattern that ages well is a schema per team or per domain, with USE SCHEMA and CREATE TABLE granted to that team and USE CATALOG on the parent. External tables are cleanest with one external location per schema, which keeps storage permissions aligned with the namespace instead of cutting across it.
Three layouts, and what each one costs
Environment first: catalogs named dev, staging, and prod, with medallion schemas inside each. This is the layout most teams end up with, because it makes the isolation that matters most, production versus everything else, the strongest boundary in the system. Promotion is a deep clone or a rerun of the pipeline against the production catalog. The cost is that domain ownership has to be expressed at the schema level, which gets crowded once you have twenty domains.
Domain first: one catalog per domain, with layer schemas inside and environments separated by workspace binding or by a suffix. This suits organizations that already work as data product teams, and it makes cross-domain sharing an explicit grant rather than an accident. The cost is that a mistake in production is now possible in twenty places instead of one, so grants need to be automated rather than clicked.
Medallion first: bronze, silver, and gold as catalogs, with domains as schemas inside them. This reads well on a slide and creates friction in practice, because a domain team then owns schemas in three different catalogs and the natural unit of ownership is split across boundaries. It works when the layers really do have different governance owners, which is rare.
Whichever you pick, assign catalog ownership to a group rather than an individual, which is also what the Databricks best practices page tells you to do for production catalogs and schemas. An owner who leaves the company is the most common way a catalog becomes unmanageable.
# catalogs/main.tf
resource "databricks_catalog" "env" {
for_each = toset(var.environments) # ["dev", "qual", "prod"]
metastore_id = var.metastore_id
name = each.key
owner = "platform-data-owners"
comment = "Isolation boundary for the ${each.key} environment"
}
resource "databricks_schema" "layers" {
for_each = local.env_layer_pairs # dev/bronze, dev/silver, ...
catalog_name = databricks_catalog.env[each.value.env].name
name = each.value.layer
owner = each.value.owner_group
}
resource "databricks_grants" "team_schema" {
schema = "${databricks_catalog.env["prod"].name}.gold"
grant {
principal = "finance-analytics"
privileges = ["USE_SCHEMA", "SELECT"]
}
}Getting the real structure onto a diagram
Catalog layouts are hard to explain in words and trivial to explain in a picture, which is why most teams have a slide that was accurate once. If your catalogs, schemas, and grants are declared in Terraform, that repository is the current structure, and Datadef reads it directly. Connect the repository read-only from GitHub, GitLab, or Azure DevOps, pick a branch or tag, and every .tf and .tfvars file is parsed. No terraform init, no state file, no workspace credentials.
What lands on the canvas is curated rather than complete, and the curation is deterministic rather than left to a model. Datadef carries an explicit opinion per resource type: databricks_metastore and databricks_catalog are boxes the reader came to see, databricks_schema is supporting detail that nests inside its catalog through catalog_name, and databricks_grant, databricks_grants, databricks_permissions and databricks_metastore_assignment are wiring that never becomes a box at all. They are counted instead, so a stack summary reads like 41 wiring resources not worth boxes: 18 grants, 12 permissions, 6 role assignments. A permission graph drawn as arrows is the fastest way to make a catalog diagram unreadable, and this is the decision that prevents it.
Multiplicity comes out honest rather than tripled. A catalog resource created with for_each over environments stays one node stating the real counts read from the tfvars under environments/DEV and environments/PROD, such as 10 catalogs in DEV and 1 in PROD, instead of a copy of the platform per environment. Modules become zones, so a catalogs module and a grants module read as separate areas of the diagram.
The sync runs daily and on demand, regenerates both the diagram and an architecture.md, skips commits that change nothing structural, and preserves nodes you moved by hand. Put the result in the platform wiki with a one-line live embed so the picture updates without anyone exporting anything.
If the structure is not in Terraform
FAQ
How many catalogs should a Unity Catalog metastore have?
Should medallion layers be catalogs or schemas?
Can a query join tables across two catalogs?
Who should own a catalog?
How do you keep a Unity Catalog diagram from going stale?