Leer en español · Lire en français · Auf Deutsch lesen
Section 7.1 of 7.8 · Intermediate · 24 min read
Data ownership and database per service
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
Code splits into services relatively easily; data does not. In this lesson you'll learn the first and most important data question: who owns each piece of data.
Picture a kitchen notice board where anyone can rub out and rewrite the day's orders. With two cooks, that works. With ten, orders get overwritten and nobody knows who changed what. A better kitchen gives each station its own board. The grill writes the grill orders. Other stations read them, or ask the grill to change them. That is data ownership: deciding which part of a system is allowed to change each piece of information.
A system of record is the authoritative source for a piece of data: the place whose value wins when copies disagree. In an online bank, the core ledger is the system of record for balances. The balance in the mobile app is a copy. The owner is the only component that writes the data, enforces its rules, decides its schema and publishes it to others. A useful rule: the service that writes to a table owns it.
This diagram shows two ways of giving three services of a food delivery platform their data. On the left, they all share one database. Every table becomes a public interface, so renaming a column can break services nobody knew were reading it, and schema changes need a coordinated release. Business rules also get duplicated in every service that writes a table, and one service's heavy report slows down all the others. On the right is database per service. Each schema is private, there are no joins across databases, and data crosses boundaries only through API calls and events. Each owner can change its schema alone. The price is that joins, transactions and integrity checks across services must be replaced by APIs, events, sagas and reporting pipelines.
Ownership gets hard when two services both change the same data. This is joint ownership, and there are four ways to resolve it. You can split the table, so each service owns the columns it writes. You can keep a small shared data domain: simple, but a deliberately limited shared database. You can delegate: pick one owner, and have the other service send it requests instead of writing. This is the most common answer for orders, payments and bookings. Or you can merge the services, the honest answer when they were split too finely.
Here's a worked example. Three services all write the status column of one shared orders table, and customers have seen orders flip from picked up back to preparing. That's joint ownership. The last write wins, late messages move the status backwards, and the cancellation rule is checked in two services, differently. The key move is noticing that one column hides three facts: the kitchen ticket's state, the delivery job's state, and the status the customer sees, which combines them. A table split was rejected, because the cancellation rule would still have no home. Merging was rejected too: three teams, with very different loads. So Ordering becomes the only writer of order status. The status only moves forward, so a late message is recorded but cannot move it back.
Here is the result. The restaurant and delivery services own their own facts, and publish events such as food ready and picked up. The order service is the only writer of the status, and stores it in its own database. A cancellation from the restaurant is a request to its API, which it can accept or refuse. Everyone else, such as the customer app and the support tool, reads the status through the API. Each fact has exactly one writer.
Other services can call the owner's API, which is current but couples them to it, or keep a local copy fed by events, which is fast but eventually consistent. The common mistake is letting a copy become a master. Once a reader corrects its own copy, there are two systems of record, and they will drift apart. Between teams, a data contract states the schema, the meaning of each field, quality expectations and freshness, ideally checked automatically. So, for each important fact, ask: who is the only writer, where are its rules enforced, and how stale may each copy be? If any answer is it depends, the design isn't finished.
The full section also covers reporting across services and data contracts in detail. Next, in Section seven point two, we look at choosing a database. See you there.
What you need to know
- A system of record is the authoritative source for a piece of data; data ownership means one component writes it, enforces its rules, controls its schema and publishes it to others.
- A shared (integration) database couples services through schema, deployment, semantics and runtime load; Fowler (2004) and most architects advise against it between independently developed applications.
- Database per service keeps each service's data private (private tables, schema or server) at the cost of cross-service joins, transactions and referential integrity, which must be replaced by APIs, events, sagas and reporting pipelines.
- Joint ownership can be resolved by table split, a small shared data domain, delegation to one owner or service consolidation, each with a distinct trade-off (The Hard Parts, 2021).
- Data you do not own can be read through synchronous calls, column schema replication, replicated caches or events; every copy is read-only and possibly stale.
- Data contracts state schema, meaning, quality, service levels and ownership for data shared between teams, and are most useful when checked automatically.
- For each important fact, a sound design can name its only writer, where its rules are enforced and how stale each copy may be.
Why ownership is the first data question
Chapter 6 split systems into services that are deployed, scaled and changed independently. Code splits relatively easily; data does not. An order refers to a customer, a menu item, a restaurant and a courier, and every team wants to read, and often change, some part of it. The most important question in a distributed data architecture is therefore not which database to buy but who owns each piece of data: which component is allowed to change it, which one enforces its rules, and how everyone else gets to see it.
Ownership matters because data outlives code. Services are rewritten every few years; customer accounts, ledgers and medical records are kept for decades. Getting ownership wrong produces the expensive kind of architectural debt: two services that cannot be released separately because they share tables, a status field that three teams overwrite in turn, or a monthly report that nobody can reproduce because the numbers come from five copies of the same data that disagree.
This section uses two running examples that recur throughout the chapter: a food-delivery platform (customers, restaurants, couriers, orders) and an online bank (customers, accounts, payments, a ledger).
System of record and data ownership
A system of record is the authoritative source for a given piece of data: the place whose value wins when copies disagree. In the bank, the core ledger is the system of record for account balances; the balance shown in the mobile app, the one in the fraud engine's cache and the one in last night's data warehouse load are all copies. In the food-delivery platform, the restaurant's own menu service is the system of record for prices, even though the search index, the mobile app's offline cache and the order lines all hold copies of them.
Data ownership turns this into a responsibility. The owning component (and the team behind it) is the only one that writes the data, enforces its invariants (rules that must always hold, such as a ledger entry is never edited, only reversed
), decides its schema, and publishes it to others through an interface it controls. Ownership is a design decision, not a property of the database: two services can sit on the same database server and still have clear ownership, and two services with separate databases can still fight over the same facts if the boundary between them is drawn badly.
A useful rule from Software Architecture: The Hard Parts (Neal Ford, Mark Richards, Pramod Sadalage and Zhamak Dehghani, 2021) is that the service that writes to a table owns it. Reading is cheap to share; writing is not, because whoever writes must enforce the rules.
The shared database and why it couples services
The simplest way to let several applications work with the same data is to give them all access to one database. Martin Fowler called this an integration database (2004): a database that is the integration point between applications, as opposed to an application database that serves one application only. It looks attractive: no APIs to build, joins work across everything, and one transaction can update orders and payments together. Fowler's verdict, shared by most architects, is that integration databases should be avoided, because the coupling they create is deep and mostly invisible.
Consider the forms of coupling it introduces. Schema coupling: every table and column is effectively a public API. Renaming orders.state to orders.status breaks any service that reads it, and nobody can be sure which ones do, because SQL queries are scattered through many codebases, reports and spreadsheets. Deployment coupling: a schema change has to be coordinated across every team that touches the table, so services that were meant to be released independently are released together. Semantic coupling: the schema must serve every client, so it grows more general and more confusing (Fowler notes that it ends up trying to unify concepts that belong in separate bounded contexts), and the business rules for a table end up duplicated, inconsistently, in every service that writes it. Runtime coupling: one service's slow report query or lock-heavy batch job degrades every other service, and the database becomes a shared single point of failure and a ceiling on connections.
None of this is fatal for a single application or a modular monolith whose modules respect each other's tables, or for the coarse-grained service-based architecture in which a handful of domain services often share a database deliberately. It becomes a problem when teams expect independent deployability but keep the shared tables, which is a classic route to a distributed monolith.
Shared database versus database per service
Database per service
The alternative, associated with microservices, is the database-per-service pattern: each service's persistent data is private to it and other services can reach it only through the service's API or the events it publishes. Chris Richardson's pattern catalogue (microservices.io) notes that this does not have to mean a separate server per service. There are three levels of separation, and the important line is the first one.
Three ways to keep a service's data private
| Level | What is separate | Strength of the boundary | Typical use |
|---|---|---|---|
| Private tables per service | Each service has its own set of tables on a shared database, with separate credentials that can only reach those tables. | Weakest: relies on permissions and discipline; still shares capacity and failure. | Early in a migration; small systems; modular monoliths. |
| Schema per service | Each service has its own schema (namespace) on a shared server. | Clear ownership and access control; still shares the server's performance and availability. | Most teams starting with several services; cheaper to run. |
| Database server per service | Each service has its own database instance, possibly of a different type. | Strongest: separate scaling, failure and technology choice. | Services with very different load, data shape or availability needs. |
The benefits mirror the costs of the shared database: owners change their schemas freely, scale and tune their stores independently, choose the right kind of database, and contain failures. The costs are real and should be stated in any decision record. Joins across services are gone and must be replaced by API composition or replicated data. A single ACID transaction can no longer span orders and payments; cross-service changes become sagas with compensating steps. Referential integrity across services (every order points to a real customer) is no longer enforced by foreign keys. Reporting needs a separate pipeline. And there are more databases to back up, patch and monitor.
The Hard Parts frames the decision as a balance of forces. Disintegrators push towards splitting data: change control (how many services break when a table changes), connection limits, scalability, fault tolerance, the wish for independently deployable units and the need for different database types. Integrators pull data together: relationships that need referential integrity, and operations that genuinely need one atomic transaction. If two tables are always changed together in one transaction and have no independent life, that is strong evidence they belong to the same owner.
When several services write the same data
Ownership questions become hard when more than one service legitimately changes the same entity. The Hard Parts distinguishes three situations. With single ownership, only one service writes the table, and it simply owns it. With common ownership, most services write the same table (an audit log or a set of configuration settings, for example); the usual answer is to create a dedicated service that owns the data and give everyone else an API, often an asynchronous one so writers are not slowed down. With joint ownership, a small number of services, typically two, write the same table, usually within the same domain. The book offers four techniques for joint ownership.
Resolving joint ownership
| Technique | How it works | Main trade-off |
|---|---|---|
| Table split | Split the table so that each service owns the columns it writes, in its own table. The food-delivery orders row becomes an order owned by Ordering and a kitchen ticket owned by Kitchen. | Clean ownership, but creating or deleting the parent entity now has to be synchronised between services, and any view of the whole needs both. |
| Data domain | Keep the table shared by the few services involved, inside a small shared schema that they jointly control. | Simple and fast, with transactions still possible, but it is a deliberately limited shared database: the services are coupled on schema change and the boundary between them weakens. |
| Delegate | Pick one owner; the other service sends it requests (synchronous calls or asynchronous commands) instead of writing. The owner is chosen by primary domain (whose concept is it?) or by operational characteristics (which service needs the fastest, most available writes?). | Clear ownership and rules in one place, at the cost of an extra network hop, a new failure mode and possibly eventual consistency for the delegating service. |
| Service consolidation | Merge the services that share the data into one service. | Removes the problem and restores local transactions, but creates a larger service with coarser deployment, scaling and fault isolation. |
No technique is free, and the right one depends on how often the data changes, how strict its rules are and how independent the teams need to be. In practice, delegation is the most common answer for business entities with a lifecycle (orders, payments, bookings), table split works well when the jointly written columns are really different facts that happened to be stored together, and consolidation is the honest answer when the two services were split too finely in the first place.
Reading data you do not own
Once ownership is settled, every other service still needs to read the data. The kitchen display needs the customer's delivery notes; the fraud engine needs the account's recent payments. There are four broad options, and each trades freshness against coupling. Note that event-driven designs, covered in Chapter 6, provide the third and fourth.
Ways to read another service's data
| Option | How it works | Freshness | Coupling and cost |
|---|---|---|---|
| Synchronous call | Ask the owner's API at the moment the data is needed. | Current, as of the call. | Runtime coupling: if the owner is slow or down, so is the caller; latencies add up along a chain. |
| Column schema replication | Copy the few columns needed (for example, restaurant name and address) into the reader's own database, kept up to date by events or change data capture. | Eventually consistent: stale by the replication delay. | Fast local reads and joins; the reader must handle late or missed updates and must never write the copy. |
| Replicated cache | An in-memory data grid (such as Hazelcast or Apache Ignite) keeps a copy of the owner's data in each consuming service, synchronised by the grid. | Near real time. | Very fast reads; memory cost, start-up loading and the operational complexity of the grid. |
| Events (event-carried state transfer) | The owner publishes events that carry the changed data; readers keep whatever they need. | Eventually consistent. | Loose temporal coupling; readers must cope with duplicates, ordering and schema evolution. |
Fowler calls the fourth option event-carried state transfer: events such as MenuItemPriceChanged carry the new values, so consumers do not need to call back. A related mechanism, change data capture (CDC), reads the owner's database log and turns each committed change into an event, which avoids the dual-write problem (writing to the database and publishing an event separately, so that one can succeed while the other fails). The transactional outbox, which solves the same problem from inside the application, is covered with sagas.
Reporting across services
Database per service makes one old task harder: the cross-cutting report. Revenue by restaurant by week, net of refunds, split by courier type
needs orders, payments, restaurants and deliveries. Running it by calling four APIs page by page is slow and puts reporting load on operational services. Querying their databases directly reintroduces the shared-database coupling through the back door.
The usual answer is to separate reporting from operations. Fowler described a reporting database: a separate store, shaped for queries and fed from the operational systems, which reporting users may read freely. Today this is usually a data warehouse or lakehouse fed by CDC or events, discussed in analytics and streaming. For small, near real-time views inside the product (a restaurant's dashboard of today's orders), a service can build its own read model from events, the idea behind CQRS. Either way, the pipelines that feed reporting depend on the owners' data structures, which is why they need contracts.
Data contracts
A data contract is an explicit, versioned agreement between the owner of a data set and its consumers. It states the schema (fields, types, which may be null), the meaning of each field (is amount in pence or pounds, including VAT or not?), quality expectations (no duplicate order identifiers, at most 0.1% of rows with a missing restaurant), service levels (how fresh the data is, how long it is kept), who owns it and how changes are announced. It applies the idea of an API contract to data that flows between teams through events, CDC streams and warehouse tables.
Contracts are most useful when they are machine-readable and checked automatically: a pipeline that rejects a schema change which would break registered consumers, or a test that fails when a quality rule is violated. The Open Data Contract Standard (ODCS), maintained by the Bitol project under the Linux Foundation AI & Data, defines a YAML format covering schema, quality rules, service levels, roles and support channels. Data contracts are not free: someone must write and maintain them, and overly strict contracts slow down change. The Hard Parts contrasts strict contracts (precise, verifiable, tightly coupling) with loose ones (easier to evolve, needing more testing), and suggests consumer-driven contracts, in which consumers state what they actually use, as a middle path. How to change a schema without breaking consumers is the subject of schema evolution.
Worked example
Worked example: who owns order status in the food-delivery platform?
The platform has three services that care about an order's progress: Ordering (takes and pays for orders), Restaurant (the kitchen tablet: accepts orders, marks food ready) and Delivery (assigns couriers, tracks pick-up and drop-off). Today all three write the status column of a shared orders table. Customers have seen orders flip from Picked up
back to Preparing
, and nobody can say where the cancellation rules live.
| Diagnose | Joint ownership of one column by three writers. Each writes what it knows, last write wins, and late messages move the status backwards. The rule an order can be cancelled only before the kitchen startsis checked in two services, differently. |
|---|---|
| Separate the facts | There are really three facts: the kitchen ticket's state (owned by Restaurant), the delivery job's state (owned by Delivery) and the customer-facing order status, which combines them and carries the business rules (cancellation, refunds, compensation). |
| Option: table split | Give each service its own status column and let readers combine them. Rejected: every reader (app, support tool, notifications) would reimplement the combination, and the cancellation rule would still have no home. |
| Option: service consolidation | Merge all three into one service. Rejected: three teams, very different load (courier location updates arrive every few seconds) and different release cycles. |
| Decision: delegate | Ordering is the only writer of order_status. Restaurant and Delivery own their own facts and publish events (OrderAccepted, FoodReady, PickedUp, Delivered). Ordering consumes them and applies a state machine. A cancellation from the restaurant tablet is a request to Ordering's API, which accepts or refuses it. |
| State machine | PLACED, ACCEPTED, PREPARING, READY, PICKED_UP, DELIVERED, with CANCELLED allowed only from PLACED or ACCEPTED. Transitions only move forward, so a late FoodReady arriving after PickedUp is recorded but does not change the status; duplicates are ignored. |
| Consequences | One place for the rules; no backwards jumps; each team releases independently. Costs: the status lags the kitchen by the event delay (typically well under a second), Ordering must be available to accept cancellations, and the events become a contract that must be versioned. |
The key move was not choosing a technique from the table but noticing that one column was hiding three different facts with three natural owners.
Write the decision down as an Architecture Decision Record, including the rejected options; the next team to ask why can't we just update the status from the courier app?
will need it.
Order status after the decision
Common mistakes
Splitting services but keeping the shared tables. Teams get the operational cost of distribution without the independence it was meant to buy. If services cannot be deployed without coordinating a schema change, they are not independent. Entity services. A CustomerService that is nothing but create, read, update and delete over a table, called by every other service, is a shared database with extra latency; ownership should follow business capabilities and their rules, not nouns. Copies that quietly become masters. See the warning above. Ignoring reporting until the end. The first finance report after the split often reveals that nobody planned how data would be brought back together. Treating ownership as purely technical. Ownership is also organisational: someone must answer for the data's quality, and per Conway's law data boundaries that cut across team boundaries tend not to last.
A good test of any data design is to ask, for each important fact, three questions: who is the only writer, where are its rules enforced, and how stale may each copy be? If any answer is it depends
or several services
, the design is not finished.
Check your understanding
Two services each update the status column of the same table. What kind of ownership is this, and name two ways to resolve it.
Joint ownership. It can be resolved by splitting the table, delegating writes to one owner, keeping a small shared data domain or consolidating the services.
Why does a shared database undermine independent deployment?
Every table is effectively a public interface, so a schema change must be coordinated with every service that reads or writes it, forcing joint releases.
What is the difference between column schema replication and a synchronous call when reading another service's data?
Replication keeps a local, eventually consistent copy that is fast and available even if the owner is down; a synchronous call returns current data but couples the caller's availability and latency to the owner.
What does a data contract add beyond a schema?
Meaning of fields, quality expectations, freshness and retention levels, ownership and how changes are announced and versioned.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “We are splitting our monolith into services but they all still use the same orders table. How should we decide which service owns which data?”