A system diagram might contain several application components:
catalog
basket
inventory
order
payment
Then one large database appears underneath them:
CATALOG BASKET INVENTORY ORDER PAYMENT
\ | | | /
\ | | | /
└────────┴───────────┴───────────┴─────────┘
│
▼
SHARED DATABASE
The applications have different names. They may run as different processes. They may even be deployed separately.
But if they all read and write the same tables, where are the real boundaries?
A catalog deployment might change a column used by the basket. The payment service might update order state directly. A reporting query might rely on an internal status value nobody remembers promising. One team might be unable to change its schema without coordinating with four others.
The boxes are separate.
The data model is not.
This raises a question that sounds administrative but is actually architectural:
Who owns the data?
My first answer would have been:
The team that owns the database.
That is not precise enough.
A database server can contain data belonging to several areas. A platform or database team may operate the infrastructure without owning the business meaning of every row. A service might be the only writer while many systems read its tables directly. The application may own the code while somebody else carries legal or regulatory responsibility for the information.
Here, ownership means architectural authority over application data. It does not mean legal ownership of information, and it does not merely mean responsibility for operating the database platform.
For an application boundary, the most useful definition seems to be:
The owner defines what the data means, decides which changes are valid, controls its internal representation, and provides the contracts through which other parts of the system use it.
That is much stronger than knowing which server stores the bytes.
Ownership begins with meaning
Suppose a furniture store has a product with this identifier:
chair-42
Several parts of the business may care about it.
Catalog knows the name, description, images, dimensions, listed price and category.
Inventory knows the warehouse location, quantity available, quantity reserved and reorder threshold.
Ordering knows the product identifier, description at purchase, unit price at purchase, tax treatment and quantity ordered.
Search knows the searchable name, keywords, category facets, popularity score and an availability indicator.
All four areas refer to the same real-world product. That does not mean they need one universal Product record containing every attribute.
REAL-WORLD PRODUCT
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
CATALOG VIEW INVENTORY VIEW ORDER VIEW
│ │ │
descriptive stock state purchase facts
information
Each part of the system has a different reason for knowing about the product. The identifier may be shared. The model does not have to be.
The same applies to language.
Consider the word customer.
A marketing system might mean a person who can receive campaigns. An ordering system might mean the buyer responsible for an order. A support system might mean a person associated with cases and conversations. A payment system might care only about a payer identifier, billing details and risk information.
Trying to force all of these meanings into one enterprise-wide object can produce a model full of marketing preferences, order history, support priority, payment methods, fraud scores, addresses, account status, loyalty tiers and dozens of fields nobody may remove.
Everybody reuses the same model.
Nobody can change it safely.
A boundary gives language a local meaning. Within ordering, customer can mean exactly what ordering needs it to mean. Within support, the same person can be represented differently.
This does not make the models carelessly inconsistent. It recognises that one real-world thing can participate in several business contexts.
A boundary protects meaning, rules and behaviour
A useful application boundary surrounds:
- language
- rules
- behaviour
- data
For example:
ORDER BOUNDARY
│
├── language
│ ├── order
│ ├── order line
│ ├── checkout
│ └── cancellation
│
├── rules
│ ├── an order must contain at least one line
│ ├── confirmed orders cannot be edited freely
│ └── cancellation depends on fulfilment state
│
├── behaviour
│ ├── create order
│ ├── confirm order
│ └── cancel order
│
└── data
├── orders
├── order lines
└── order status history
The data is not an unrelated storage concern beneath the boundary. It holds the state on which the boundary’s rules operate.
If another component updates that data directly, it can bypass those rules.
Suppose the order table contains:
order_id
status
total
Another component might discover that this SQL works:
UPDATE orders
SET status = 'cancelled'
WHERE order_id = 5001;
The database accepted the statement. That does not prove the transition was valid.
Cancellation may require checking whether payment has settled, fulfilment has begun, items have shipped or a refund is required.
The meaningful operation is not:
update status column
It is:
cancel order
The order boundary should own the decision.
CALLER
│
│ CancelOrder
▼
ORDER BOUNDARY
│
├── load order
├── evaluate cancellation rules
├── change state
├── record consequences
└── commit
Direct table access replaces a domain operation with a storage mutation. It lets the caller decide which rules matter.
That is not data ownership.
It is a communal stationery cupboard containing production state.
Database constraints are still valuable. Application ownership does not mean every rule should live only in application code.
CREATE TABLE order_lines (
order_id bigint NOT NULL,
product_id text NOT NULL,
quantity integer NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
Constraints can protect required values, uniqueness, valid ranges and referential integrity. Transactions can group related changes.
The important point is that these constraints belong to the order boundary’s implementation. The order boundary decides which tables exist, what their columns mean, which constraints apply and how state transitions occur.
Other components should consume the boundary’s public contract rather than treating its tables as their own API.
Private persistence makes ownership real
Suppose the basket reads catalog data directly:
SELECT name, price
FROM products
WHERE product_id = ?;
That looks efficient. There is no network call, API client or duplicated data.
But the basket is now coupled to the table name, column names, data types, status representation, price representation, schema location, migration timing and database availability.
The database schema has become a public interface.
It just lacks most of the things I would normally expect from one:
- explicit ownership
- versioning policy
- compatibility guarantees
- documentation
- deprecation process
- consumer tests
The dependency is still there. It is merely hiding inside SQL.
The problem becomes sharper when several applications write the same tables.
SERVICE A ─────┐
├────► shared table
SERVICE B ─────┤
│
SERVICE C ─────┘
Who decides which transitions are legal? Which writer validates which rule? Which writer publishes change events? Who may alter the schema? Who repairs conflicting updates? Who owns an incident caused by invalid state?
If the answer is, “It depends which code path changed the row,” then the data has no clear behavioural owner.
A table may have many readers. Having many independent writers is a much stronger warning sign.
Read access can also weaken ownership.
“Only the owner writes the table. Other services merely read it” is better than shared writes, but those readers are still bound to the internal schema.
Suppose catalog changes price from one decimal column into amount_minor and currency because the domain now supports multiple currencies. Every direct reader must change with the migration. If five consumers query the table, the catalog team may need to coordinate five deployments.
Catalog does not fully control its implementation anymore. Its internal representation has become a cross-system contract.
A useful default is therefore:
Only the owner reads and writes its persistence directly.
Other components use a supported contract such as an API, event, export, published view or purpose-built read model. The exact mechanism can vary. The boundary should remain explicit.
This does not require one database server per service.
Several arrangements are possible:
SHARED SERVER, PRIVATE TABLES
database server
├── catalog tables
├── basket tables
└── order tables
SHARED SERVER, PRIVATE SCHEMAS
database server
├── catalog schema
├── basket schema
└── order schema
SEPARATE DATABASES
catalog database
basket database
order database
A smaller system could use one database server while giving each application separate credentials, access only to its own schema and control over its own migrations.
The important property is that one application cannot casually reach into another application’s state.
Physical isolation can increase later for availability, performance, security, scaling or operational independence. Ownership begins with enforced logical boundaries.
Credentials can help make the architecture real.
Documentation might say:
The order service must not access inventory tables.
But if its database user can run:
SELECT *
FROM inventory.stock;
the boundary depends on continued restraint.
A more enforceable design gives each application credentials limited to its own data.
catalog_runtime
└── catalog schema only
inventory_runtime
└── inventory schema only
order_runtime
└── order schema only
This turns “please do not cross the boundary” into “the system prevents ordinary boundary crossing”.
Good architecture should not require every developer to resist the same convenient shortcut forever.
Owners publish contracts, not tables
If the basket needs product information, catalog can provide it through an API.
BASKET
│
│ GetProduct
▼
CATALOG
│
│ ProductSummary
▼
BASKET
The response might contain:
{
"product_id": "chair-42",
"name": "Gopher reading chair",
"price_minor": 24900,
"currency": "GBP",
"purchasable": true
}
That contract does not have to mirror the catalog database. Catalog may store the information across several tables.
The API contract is designed for consumers. The database schema is designed for catalog persistence.
Separating them gives the owner room to change its implementation without changing every consumer. It also lets the service expose meaning rather than storage accidents.
For example, purchasable: true may be more useful than exposing several internal flags and expecting the basket to reconstruct catalog policy.
An API preserves authority, but it introduces runtime coupling. The basket may now depend on catalog being available during the interaction. That brings network latency, timeouts, partial failure, availability dependencies and contract versioning.
This may be appropriate when the basket needs current authoritative information, such as whether the product is currently purchasable.
Ownership does not mean every consumer must always call the owner synchronously. It means the owner decides how authoritative information is made available.
Events are another option.
Suppose search needs product information. Calling catalog for every search result could be expensive and unnecessarily coupled. Catalog could publish events such as:
ProductCreatedProductNameChangedProductPriceChangedProductDiscontinued
Search consumes those events and maintains its own model.
CATALOG
│
│ ProductPriceChanged
▼
MESSAGE SYSTEM
│
▼
SEARCH
│
└── update search document
Search owns its documents, tokenisation, ranking fields and facets. Catalog remains the authority for catalog facts.
That is duplication, but it is not necessarily confused ownership.
The distinction is clear when everybody knows:
- who may originate the fact
- who stores a copy
- how the copy is refreshed
- how staleness is detected
- how the copy is rebuilt
A copied value should also have understood provenance.
SEARCH INDEX FIELD
product_name
├── source: catalog
├── refreshed by: catalog events
├── allowed staleness: 60 seconds
└── rebuild source: catalog export
Without this, duplicated data becomes archaeological sediment. Nobody knows where it came from, whether it is current, whether it may be edited locally or how to repair it.
A derived model should normally be read-only with respect to the facts it copies. Search may change ranking, tokenisation or index structure. It should not become a second place for editing the official product name.
Facts have owners, lifecycles and copies
I had previously treated duplicated data as automatically bad. Normalisation taught me to avoid unnecessary repetition inside a relational model.
Across application boundaries, some duplication may be the price of autonomy.
Suppose an order records:
product_id
product_name
unit_price
The current product name and price also exist in catalog.
Is the order duplicating catalog data?
Yes.
But the order may need a historical fact:
What description and price were agreed when this purchase was made?
If catalog later changes Gopher reading chair to Deluxe Gopher reading chair, the existing order should not necessarily be rewritten.
The values now have different meanings:
CATALOG
current product presentation
ORDER
description captured at purchase
They are not competing authorities over the same fact. They are different facts with different lifecycles.
This is why ownership is usually divided by facts, not rows.
A convenient products row might contain:
name
price
stock_quantity
average_rating
search_rank
But those fields belong to different business areas.
Catalog owns description and listed price. Inventory owns stock and reservation state. Reviews owns review records and rating calculations. Search owns ranking and indexing.
Several teams updating different columns in one row creates a shared boundary.
A composed view may present those facts together to users. Storage convenience should not determine business ownership.
The phrase “single source of truth” also needs precision.
One system might be authoritative for the current customer name, while another is authoritative for the name captured on an order. A reporting store may be authoritative for an approved monthly financial report without being the operational owner of every underlying transaction.
Instead of asking:
Which database is the source of truth?
Ask:
Which boundary has authority over this specific fact at this point in its lifecycle?
For example:
current listed price
└── catalog
price agreed for order 5001
└── order
captured payment amount
└── payment
searchable price copy
└── search owns the projection
catalog owns the originating fact
Truth has a subject.
Invariants help reveal the right boundary
An invariant is a rule that must remain true when state changes.
Examples include:
- a reservation cannot reduce unallocated stock below zero
- an order total matches the accepted lines, taxes, discounts and charges
- a basket quantity must be positive
- the same payment capture request must not be applied twice
- a confirmed shippable order must have a valid delivery address
The data and behaviour needed to enforce one invariant usually belong together.
Suppose reserving stock requires checking available quantity, existing reservations and reservation expiry. If those facts are split across several independently controlled services, enforcing the rule becomes a distributed workflow.
That may be necessary. But it should make me ask whether the boundary has been drawn through the middle of one consistency requirement.
A useful heuristic is:
State that must change together to protect an immediate invariant often belongs within one transactional boundary.
That does not mean every business process belongs in one service. It means boundary design should follow rules and behaviour, not a desire to create the maximum number of boxes.
A service per table is not ownership.
A database might contain customers, orders, order_lines, payments and shipments. Turning each table into its own service would be decomposition by storage structure.
orders and order_lines may participate in one transactional model. An order line may have no useful independent lifecycle outside its order. Putting a network boundary between them would turn ordinary consistency into distributed coordination.
A service can own several tables:
ORDER BOUNDARY
│
├── orders
├── order_lines
├── order_addresses
└── order_status_history
The phrase “database per service” does not mean “table per service”. It means the service’s persistent state is private to that service.
A service can use many tables, one document collection, several key spaces or multiple internal stores, provided the ownership remains coherent.
Strong boundaries expose distributed-system costs
Private ownership gives each boundary autonomy, but it removes the convenience of one transaction spanning the whole system.
Inside one relational database, the order service can use a transaction:
BEGIN;
INSERT INTO orders (...);
INSERT INTO order_lines (...);
INSERT INTO order_status_history (...);
COMMIT;
Either the related changes commit together or they do not.
Once data is divided between independently owned stores, one local transaction cannot simply update all of them.
ORDER DATABASE INVENTORY DATABASE PAYMENT DATABASE
│ │ │
└──────── no ordinary local transaction ───────┘
Suppose placing an order requires:
- creating the order
- reserving inventory
- authorising payment
If those are separate owners, the operation spans several local transactions.
Possible outcomes include an order being created while inventory reservation fails, inventory being reserved while payment is declined, or payment being authorised while the response is lost.
The system needs workflow rules:
- which step happens first
- what state records progress
- which failures are retryable
- which actions compensate earlier work
- who owns the overall process
- what the customer sees while it is incomplete
The database no longer provides one atomic umbrella over the whole business process. Intermediate states must become explicit.
Shared databases can hide this question by placing everything in one transaction. That is simple when one model genuinely owns all three changes. It becomes problematic when separate business capabilities are editing one another’s state.
Independent ownership forces decisions about which rules require immediate consistency, which information can propagate later, which failures require compensation and which service is authoritative at each stage.
This is harder. It can also be a more honest model of a business process that was never truly one indivisible event.
Queries become harder too.
A shared relational database makes cross-domain joins straightforward. Once data belongs to separate boundaries, a query may need one of several approaches:
- API composition
- a purpose-built read model
- an analytics pipeline
- a data warehouse
API composition keeps authority with each owner.
ACCOUNT VIEW
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
PROFILE ORDER LOYALTY
This gives fresh authoritative data and avoids a duplicated read model. It also adds several network calls, combined latency and partial-failure handling.
A read model can optimise a frequent cross-boundary query.
PROFILE EVENTS ─────┐
│
ORDER EVENTS ───────┼────► ACCOUNT READ MODEL
│
LOYALTY EVENTS ─────┘
The page reads one store instead of calling several services. The cost is event processing, lag, replay, duplicate handling, ordering policy, monitoring and rebuild procedures.
Reporting deserves its own architecture as well. Allowing reporting tools to query every operational database directly can create production load, schema coupling, broad credentials and migration constraints.
A separate analytical pipeline can receive exports, events or change streams from operational owners and load them into a reporting store. That store owns reporting schemas, historical integration, aggregations and analytical performance. It does not become the authority for operational changes.
The loss of easy transactions and joins is a real cost. It should not be waved away.
Ownership includes evolution, operations and responsibility
If catalog owns its internal schema, it should be able to change its table layout, indexes, normalisation, storage technology and migration strategy without requiring every other application to update simultaneously.
Its public contracts still create obligations.
INTERNAL SCHEMA
owner may evolve independently
PUBLIC API OR EVENT
owner must respect consumer contract
If catalog publishes ProductPriceChanged, consumers may depend on its field names, field types, meaning, delivery expectations and ordering assumptions.
The owner must manage versioning, backward compatibility, deprecation, documentation and consumer communication.
Owning a contract means being responsible for its stability, not being free from responsibility.
The same applies to events. A service should usually publish facts it has authority to assert.
Catalog can publish ProductDiscontinued because it owns catalog publication state. Inventory can publish StockReservationFailed because it owns reservation state. Order can publish OrderConfirmed because it owns the order lifecycle.
A service should be cautious about publishing conclusions that belong to another owner.
Ownership also includes vocabulary.
What does PaymentCompleted mean?
Authorised? Captured? Settled? Recorded internally? Accepted by an external provider?
The owner must define its terms. It is not enough to own the table containing status = 3. The owner must define what status 3 means and which transitions make it valid.
Operational responsibility matters too.
The application owner should understand:
- migrations
- backups and restores
- retention and archiving
- monitoring and capacity
- access controls
- incident response
Infrastructure specialists may provide the database platform, but the application owner still needs to define the data’s semantics and operational requirements.
PLATFORM TEAM
operates database capability
APPLICATION OWNER
defines data semantics and requirements
Ownership can be collaborative without becoming ambiguous.
Security and privacy also follow the boundary.
A catalog service does not need payment credentials. A payment service does not need product descriptions. A shipping service should receive only the delivery information required for fulfilment.
Purpose-specific contracts support least privilege, data minimisation, auditability and retention control.
A boundary should not become an excuse to duplicate sensitive data everywhere. Every copy creates another lifecycle and another place to protect.
Ownership can also move.
Suppose catalog initially stores stock_quantity. As inventory becomes more complex, the business introduces multiple warehouses, reservations, replenishment, allocation and expiry. Inventory may become its own model.
A migration might:
- define the new inventory contract
- create the new inventory store
- copy existing stock state
- keep old and new models synchronised temporarily
- move readers to the new owner
- move write authority
- remove the old field
During the transition, ownership must remain explicit. Otherwise catalog may say 12 are available while inventory says 9.
Two sources of truth are usually one source of arguments.
Logical ownership matters more than deployment shape
Nothing about data ownership requires separate network services.
A modular monolith can contain catalog, basket, inventory and order modules. Each can own its domain model, tables, migrations and public application interface.
Other modules avoid direct access.
BASKET MODULE
│
│ catalog interface
▼
CATALOG MODULE
│
└── catalog persistence
The calls may remain in-process. The database server may be shared. The ownership boundaries can still be real.
This can provide strong modularity without introducing network calls, distributed deployment, cross-service tracing or remote failure.
The reverse is also true.
A system can have twenty repositories, twenty containers and twenty deployment pipelines while every service uses one shared database user, one shared schema and one shared domain library.
That is physical distribution without genuine autonomy.
The architecture has paid the cost of distribution while preserving the coupling of a poorly modularised monolith.
A process boundary does not automatically create an application boundary. Data ownership is one of the clearest tests.
Team responsibility should follow the same shape.
A useful alignment is:
BUSINESS CAPABILITY
│
▼
APPLICATION BOUNDARY
│
▼
DATA OWNERSHIP
│
▼
TEAM RESPONSIBILITY
The team responsible for the capability should be able to change its code, evolve its internal data model, deploy, operate the application and respond to incidents within agreed organisational controls.
This does not mean teams never collaborate. It means routine internal change should not require an interdepartmental summit around one nullable column.
A practical ownership test
For any important data, ask:
Meaning
Who defines what this field or record means?
Authority
Who is allowed to originate or change this fact?
Rules
Who enforces the invariants around it?
Persistence
Which application controls the storage schema?
Access
How do other components obtain the information?
Contracts
Which APIs, events, views or exports are publicly supported?
Copies
Which systems store derived copies, and how are they refreshed?
Failure
What happens when a copy is stale or unavailable?
Operations
Who owns migrations, backup requirements and restoration testing?
Lifecycle
Who decides retention, archival and deletion?
Warning signs include:
- several applications writing the same table
- one service updating another service’s schema
- consumers querying undocumented internal columns
- schema changes requiring coordinated deployment everywhere
- nobody knowing which copy is authoritative
- cross-service reports running against production databases
- one service publishing facts another service owns
- every boundary sharing the same domain object
- credentials allowing all services to access all tables
- business rules being duplicated across several writers
None of these automatically proves the architecture is wrong. Legacy systems and transitional designs have constraints.
But they should be explicit compromises rather than invisible defaults.
A simple furniture-store ownership map might look like:
CATALOG
├── product descriptions
├── categories
├── listed prices
└── publication state
BASKET
├── active baskets
├── basket items
└── basket lifecycle
INVENTORY
├── stock positions
├── reservations
└── availability rules
ORDER
├── accepted orders
├── order lines
├── purchase-time descriptions
├── purchase-time prices
└── order lifecycle
PAYMENT
├── payment attempts
├── authorisation state
├── capture state
└── provider references
These boundaries collaborate through supported operations and events. The exact number of services is not the important part.
The important part is that each fact has one clear owner.
My old model was:
data belongs to
the database containing it
The model I am keeping is:
DATA OWNER
├── defines meaning
├── enforces rules
├── controls writes
├── controls internal schema
├── publishes supported contracts
├── manages evolution
└── accepts operational responsibility
Other components may hold copies, but a copy does not automatically become another authority.
The physical database arrangement sits underneath the ownership model. Private tables, schemas, databases or servers can all support it. The required degree of isolation depends on team size, system complexity, security requirements, availability needs and operational maturity.
So when I draw an application boundary, I should not stop at separate code, process or deployment.
I should ask:
Can this boundary define and change its model without another component reaching into its state?
If the answer is no, the database may be revealing the architecture more honestly than the service diagram.
The real boundary is wherever authority over behaviour and data changes hands.
Everything else is just a box.
References and further reading
Bounded contexts
Martin Fowler: Bounded Context
Introduces the domain-driven design concept of dividing a large domain model into explicit contexts within which terms and models have precise local meanings.
Data ownership and microservice boundaries
Microsoft: Data sovereignty per microservice
Explains the principle that each microservice owns its domain data and logic, and that other services access that data through synchronous or asynchronous contracts rather than direct database access.
Martin Fowler: Microservice trade-offs
Discusses strong module boundaries, decentralised data management, independent deployment and the distributed-system costs introduced by microservices.
Chris Richardson: Database per service
Describes keeping each service’s persistent state private and explains logical isolation options including private tables, schemas, databases and database servers.
Microservice architecture
Martin Fowler and James Lewis: Microservices
Describes common characteristics of microservice architecture, including componentisation around business capabilities and decentralised data management.
Cross-service queries
Chris Richardson: API Composition
Describes implementing cross-service queries by retrieving information from its owners and composing the results in application code.
Chris Richardson: Command Query Responsibility Segregation
Describes maintaining query-specific views from events when information from several service-owned databases must be queried efficiently.
Relational integrity and transactions
PostgreSQL 13: Data Definition
Documents relational schemas, tables, constraints, privileges and other structures a data-owning application can use to enforce its local model.
PostgreSQL 13: Constraints
Explains NOT NULL, CHECK, uniqueness, primary-key and foreign-key constraints used to protect relational data integrity.
PostgreSQL 13: Transactions
Introduces grouping several SQL operations into one all-or-nothing transaction within a database.
PostgreSQL 13: Concurrency Control
Documents PostgreSQL’s transaction isolation and concurrency model for maintaining consistent access to data within a database.