Schema Sovereignty
One owner per schema.
A schema is shared mutable state. Shared mutable state needs an owner. Without one, every team that touches the database becomes a writer, every change is a coordination meeting, and the tables drift away from the application that thinks it knows them.
Opinion
I've worked in two organisations where the “shared database” was the actual production architecture, and the cost was always the same: a schema change took a week of Slack threads, a quarterly outage was traced back to a SELECT that locked a write table, and the only person who knew which application owned which column had left the company in 2019. The rule isn't microservice dogma; it's the simplest rule that survives Conway's Law. One application owns the writes. Everyone else reads through a contract you can change without asking the BI team for permission.
The structural form is the directory. db/application/ matches the application schema; db/session/ matches the session schema; the table name in the migration is the same word as the type name in the code. See A1 Screaming Architecture: the vocabulary in ls is the vocabulary in the database. See A6 Bounded Contexts: the schema is the database side of the bounded context, and the type system is the in-memory side. The ubiquitous-language thread runs from the column name through the type to the function name to the test name. Schema sovereignty is the rule that keeps the thread intact.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Each schema has exactly one owning application. Schema changes ship as migrations from that repo. External tools read through their own tables, materialised views, or replicas. The directory layout mirrors the schema; cross-context reads go through an ACL. /tenet/schema-sovereignty/T4
AI eyes only
Rule: one owner per schema. Cross-context reads go through views or APIs, never direct.
Reject: a migration that touches another application's tables. Reject: a SELECT that joins across bounded contexts. Reject: writing to a schema this service does not own.
Generate: each schema has a single owning service. Other services read via published views, a BI replica, or an explicit API. Migrations live in the owning repo.
Diagnostic: list the schemas the change touches. If more than one, justify per schema or split the change.
Why?
- Schema changes ship from one repo. No coordination meeting, no DBA queue, no cross-team Slack thread — the migration runs in the deploy that ships the code that needed it.
- No spooky action at a distance. The application that owns a table is the only application that writes to it; a row that changes outside that path is a defect, not a mystery.
- Analytics, BI and downstream services live in their own tables, materialised views, or replicas. Renaming a column does not page the data team.
- The directory layout matches the schema.
lsis a map of the database; pair it with A1 Screaming Architecture and the same vocabulary runs from the column to the type to the test. - A bounded context that grows into its own service inherits its schema with it.
db/session/moves with the session service the day you extract it. - Cross-context reads go through an Anti-Corruption Layer. The translation lives in code that the next reader can audit, not in a JOIN buried in a query plan.
- The agent can't accidentally edit two schemas in one PR. The directory enforces the boundary; the architecture test enforces the import direction.
Origins
The argument is older than microservices and broader than any one architectural era. Eric Evans's 2003 Domain-Driven Design chapter on Bounded Contexts gave the modern vocabulary: a model only makes sense within its context, and the context is the unit of ownership.4Domain-Driven Design (Addison-Wesley, 2003). Ch. 14 Bounded Context establishes the unit of ownership; ch. 16 Anti-Corruption Layer establishes the cross-context translation pattern. The architectural form of the rule. Where two contexts share a database, the model is leaking; where they share a table, the leak is structural. The Anti-Corruption Layer pattern from the same book is the working surface: cross-context reads translate the external schema into the bounded-context type at the boundary, never inside the domain.
Greg Young's 2010 CQRS Documents sharpened the rule: the read model and the write model serve different consumers, and each owns its own schema.5CQRS Documents (2010). The read model and the write model serve different consumers; each owns its own schema. The seminal essay that sharpened the discipline. Martin Fowler's PolyglotPersistence bliki entry from 2011 added the operational case: different stores for different jobs, each owned by one bounded context.6PolyglotPersistence (martinfowler.com bliki, 2011). Different stores for different jobs, each owned by one bounded context. The operational case for one schema per writer. Vlad Khononov's Learning DDD (2021) carries the modern restatement: the database is part of the bounded context, not a shared resource.
The microservices literature operationalised the rule. Sam Newman's Building Microservices made “don't share databases” the load-bearing chapter title; Chris Richardson's Microservices Patterns formalised the database-per-service pattern.7Microservices Patterns (Manning, 2018), ch. 2. Database-per-service formalised as a pattern; cross-service reads go through the API, never the join planner. ThoughtWorks' Tech Radar has carried “shared databases” on the Hold ring for over a decade. Robert C. Martin's Clean Architecture ch. 30 gives the framing in plain English: The Database Is a Detail — the application owns the schema; the database is an implementation choice.8Clean Architecture (Prentice Hall, 2017), ch. 30 The Database Is a Detail. The application owns the schema; the database is an implementation choice. The framing in plain English.
The structural form — the directory mirrors the schema — is owned framing in the Prickles canon, drawn from PUP-Phoenix's db/application/ and db/session/ directories that match the live PostgreSQL schemas. The underlying argument is the database-layer expression of A1 Screaming Architecture and ubiquitous language: the directory layout is the same vocabulary as the schema, which is the same vocabulary as the domain. The ubiquitous-language thread runs end to end.
Quotes
We want to make sure that databases are hidden inside service boundaries. Sharing databases is one of the worst things you can do if you are trying to achieve independent deployability.
Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organization, usage within specific parts of the application, and physical manifestations such as code bases and database schemas.
The driving factor here is that bounded contexts represent a much better integration boundary than database integration. We learned the hard way over the last twenty years that integrating through a shared database is a bad idea.
The database is a detail. The architecture of a system has very little to do with whether or not it uses a database. The architecture treats the database as a detail that does not rise to the level of an architectural element.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The architectural form of one owner per schema. A bounded context owns its model; the schema is the model’s database side. Cross-context reads translate at the boundary.
- 02CQRS DocumentsSupportsThe seminal essay that sharpened the rule: read and write models serve different consumers and each owns its own schema.
- 03PolyglotPersistence (bliki)SupportsThe operational case for one schema per bounded context: different stores for different jobs, each owned by exactly one writer.
- 04The bridge between Evans’s vocabulary and a real codebase. The chapter formalises the Anti-Corruption Layer as the mechanism for honouring schema sovereignty across contexts.
- 05The load-bearing chapter title is “Don’t share databases”. The 2e softens to “be careful with shared databases”; the underlying ownership rule survives the softening.
Sixteen sources, the supporters anchored in CQRS, Polyglot Persistence and the database-per-service literature. The qualifiers (Newman's second-edition softening, Fowler on monolith-first) carry the steelman the rule has to address: shared databases are sometimes the right answer for small teams who don't need the boundary yet. The opposers (the data-warehouse-as-source-of-truth school) argue for centralised schema as a feature, not a bug.
Examples
// Before: analytics writes into the application's table. Two owners; both lose.export async function recordObservation(tagId: string, weight: number): Promise<void> { await appDb.query( "UPDATE hedgehogs SET last_weight = $1, last_seen = NOW() WHERE tag_id = $2", [weight, tagId], );}
// After: analytics owns its own schema; integration is an explicit consumer.export async function recordObservation(event: HedgehogSpotted): Promise<void> { await analyticsDb.query( "INSERT INTO analytics.hedgehog_observations (tag_id, weight, seen_at)", "VALUES ($1, $2, $3)", [event.tagId, event.weight, event.observedAt], );}// integration: application emits HedgehogSpotted on the outbox;// analytics consumes events. No cross-schema writes.
Enforcement
Apply these rules in drizzle.config.ts. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| drizzle-kit generate / migrate | drizzle-kit | schema drift between the application’s declarative schema and the database. The kit refuses to generate when the database is ahead of the migrations. |
| schemaFilter (drizzle.config.ts) | drizzle-kit | migrations that touch a schema other than the one the application owns. Restricts every kit invocation to one schema name. |
| prisma migrate deploy | Prisma Migrate | the same forward-only discipline for Prisma shops. Schema-first declaration; migrations derived and version-controlled. |
| import/no-restricted-paths | eslint-plugin-import | cross-context db imports — src/contexts/application can’t reach into src/contexts/session/db. Same rule as A2 Three-Tier Hoisting, applied to schema boundaries. |
| dependency-cruiser custom rule | dependency-cruiser | the same upward-only direction expressed as a standalone CI step. Useful when ESLint is not the right surface (build scripts, monorepo boundary checks). |
drizzle.config.tsconfiguration snippet
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/application/schema.ts',
out: './db/application/migrations',
dialect: 'postgresql',
dbCredentials: { url: process.env.DATABASE_URL! },
schemaFilter: ['application'],
verbose: true,
strict: true
});AI rules
.cursor/rules/t4-schema-sovereignty.mdc---
description: Prickles T4 — Schema Sovereignty
globs: "**/*.{ts,tsx,js,jsx,py,java,php,sql}"
alwaysApply: false
---
## Prickles T4 — Schema Sovereignty
Each schema has exactly one owning application. That application is the only writer; everyone else reads through a defined contract.
Schema changes ship as migrations from the owning application's repository. No DBA-side ALTER, no shared-schema convenience.
External tools (analytics, replication, BI) read from their own tables, materialised views, or replicas — never the canonical write tables.
The directory layout mirrors the schema. `db/<context>/` ↔ `<context>` schema; the same vocabulary runs from the table to the type to the function name.
Cross-context reads go through an Anti-Corruption Layer; cross-context writes are forbidden. The boundary is a published contract, not a JOIN.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is Sam Newman's second-edition softening. The original “don't share databases” line in Building Microservices (2015) became “be careful with shared databases” in the second edition (2021), with explicit acknowledgement that database-per-service is operationally expensive and architecturally over-engineered for the team that does not need it yet.1Building Microservices, 2nd ed. (O’Reilly, 2021). The original “Don’t share databases” chapter title from 2015 became “Be careful with shared databases”; the underlying one-owner-per-schema rule survives the softening. Martin Fowler's MonolithFirst argument extends the same line: when the bounded contexts cannot be drawn on a whiteboard with confidence, the shared database is a cheaper place to learn the boundaries than the wrong distributed split.2MonolithFirst (martinfowler.com bliki, 2015). If you cannot draw the bounded contexts on a whiteboard, the shared database is a cheaper place to learn the boundaries than the wrong distributed split. The right warning at the wrong target for schema sovereignty. The deeper steelman is the data-warehouse position: a single canonical schema where every business event lands is the cleanest way to answer cross-domain questions, and schema sovereignty by application boundary fragments the warehouse before the warehouse exists.
Counter-argument retort
Newman's softening is correct as a description and wrong as a doctrine. The second edition admits that database-per-service is operationally expensive; the first edition's load-bearing claim — that exactly one application owns the writes — survives the softening unchanged.1Building Microservices, 2nd ed. (O’Reilly, 2021). The original “Don’t share databases” chapter title from 2015 became “Be careful with shared databases”; the underlying one-owner-per-schema rule survives the softening. Schema sovereignty is not database-per-service. A monolith with one schema and one writer is a sovereign-schema deployment; a monolith whose tables are written by a cron job, a Lambda, and an in-database trigger is not. The rule is about ownership of writes, not the count of databases.
The Fowler “monolith-first” counter is the right warning at the wrong target. Monolith-first is about service boundaries, not schema boundaries. A monolith should still have one writer per logical schema; a sub-domain that grows into its own service inherits its schema with it. The directory layout makes the inheritance trivial: db/session/ moves with the session service when you split it out. See A6 Bounded Contexts for the in-memory side of the same argument.
The data-warehouse counter answers itself. The warehouse is a downstream consumer that lives in its own schema, fed by extract / change-data-capture / replication. The warehouse’s schema is sovereign too — one owner, one writer, migration-controlled.3Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006). The canonical reference for migration discipline; the warehouse’s schema is sovereign too — one owner, one writer, migration-controlled. The mistake is letting the warehouse read live application tables; the fix is the materialised view, the read replica, or the CDC pipeline. None of those break schema sovereignty; all of them honour it.
The genuine residue is the small-team monolith where coordination is cheap and the schema changes once a month. There, the rule says: one writer is enough; you do not need database-per-service; you do need to know which application owns each table and to ship every change as a migration from that application’s repo. The directory mirrors the schema; the same vocabulary runs from ls to the information_schema; the next developer reads the layout and knows the architecture.
In production code the shared schema is the comment that lies. The next reader trusts the table; the column has been written by three applications; one of them assumed it was a string, the other an enum; a bug ships. Migrate — one writer, one schema, one repo. The discipline is small. The cost of pretending you can share writes is large.
Notes
- [1]Sam Newman — Building Microservices, 2nd ed. (O’Reilly, 2021). The original “Don’t share databases” chapter title from 2015 became “Be careful with shared databases”; the underlying one-owner-per-schema rule survives the softening.
- [2]Martin Fowler — MonolithFirst (martinfowler.com bliki, 2015). If you cannot draw the bounded contexts on a whiteboard, the shared database is a cheaper place to learn the boundaries than the wrong distributed split. The right warning at the wrong target for schema sovereignty.
- [3]Pramod Sadalage & Scott Ambler — Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006). The canonical reference for migration discipline; the warehouse’s schema is sovereign too — one owner, one writer, migration-controlled.