What you'll learn: By the end of this lesson you will be able to test a proposed service boundary against deployment, data and runtime coupling, use the write-set of each business transaction to find cuts that hold, give every piece of data exactly one owning service and enforce that in the database, say what replaces the joins and transactions you lose (API composition, local read models, sagas), and recognise a distributed monolith before it ships.
Picture a company that splits its accounts office into three departments, each with its own door and sign. All three still file papers in one cabinet. The org chart says three teams, but nobody can rearrange a drawer without asking the other two, and when the cabinet jams, all three stop.
That is the failure this lesson is about. Architecture styles already names the distributed monolith and gives the strangler pattern, and ShopStream Stage 8 splits a whole system into seven services. Here the subject is narrower and deeper: how to choose the cut, and what "each service owns its data" means in practice. Choosing the contexts themselves is the job of domain-driven design and bounded contexts.
Verified against the sources in "Official documentation" and the pages linked in the text, and PostgreSQL 18 documentation, on 2026-11-08.
The core idea
A split into services is real only if each service can change, deploy and fail without the others, and that depends mostly on who owns the data. Give each piece of data one owning service: the only code that writes it, and the only door to it. Everyone else gets it by asking the owner, or by keeping a labelled copy fed by the owner's events.
Three illustrations run through the lesson, each labelled:
- Minimal example: a schema per service with database permissions, and one event that feeds a read-only copy.
- Realistic example: ShopStream's "in stock" badge when Inventory leaves the shared database, in "In practice".
- Failure example: a reporting job that reads another service's table.
What you are actually splitting
Fowler and Lewis describe microservices as a way to build an application as a suite of small services that are independently deployable. The word that matters is independently. A service boundary can be coupled in three separate ways, and each has a quick test.
| Coupling | What it looks like | The test |
|---|---|---|
| Deployment | Two services must be released together, in order, or from one pipeline | Can this service go to production alone on an ordinary afternoon? |
| Data | Another service reads or writes this service's tables, or a library of shared domain classes forces lockstep upgrades | Can the owner change its schema without asking anyone? |
| Runtime | A synchronous call chain means one slow service stalls the rest | With every other service stopped, what does this one still do? |
microservices.io describes the shared-database variant the same way: developers must coordinate schema changes with every other service that uses the same tables, and the services can interfere with each other at runtime. Fowler calls an integration database "a point of coupling between the applications that access it" and says it should be avoided. A shared schema is the most common reason a "microservice" is not independent.
Where to draw the line
Start from the business, not the technology. The Azure Architecture Center gives a sequence: begin with a bounded context (a microservice should not span more than one), examine the aggregates inside it (they are often good candidates because they come from business requirements, are cohesive, are a boundary of persistence and should be loosely coupled), then adjust for non-functional needs such as team size, scale and security.
A practical companion is the write-set of each business transaction. List every use case that changes data (place an order, reserve stock, take a payment) and write down which aggregates it must change together. Evans's reference defines the aggregate as the unit that governs transactions: inside its boundary consistency rules apply synchronously, and across boundaries updates are handled asynchronously. Vernon adds that modifying several aggregate instances in one transaction can be a sign the consistency boundaries are wrong, or that eventual consistency is the right tool.
Figure 1. A decision per pair of data, not per noun. How to read this: start at the top for each pair. The first question is the strict one: if a business rule needs both to change together, they stay one service. Only a pair that survives all three questions earns a network between them.
Two cautions come from the sources. When in doubt, the Azure guidance says to start with coarser services, because splitting one into two is easier than moving functionality across several. Fowler's "monolith first" note says even experienced architects in familiar domains have great difficulty getting boundaries right at the beginning. Boundaries are hypotheses, cheaper to move inside one deployable.
Data ownership, concretely
"Each service owns its data" is a rule about write authority and access. microservices.io states the pattern as keeping each service's persistent data private and accessible only via its API, so that a service's transactions involve only its own database. It lists three ways to do this in a relational database: private tables per service, a schema per service, or a database server per service. They differ in how strongly the boundary is enforced and in cost.
| Option | What protects the boundary | Cost |
|---|---|---|
| Private tables | Convention only | Cheapest; the first shortcut breaks it |
| Schema per service | Database permissions, if you set them | One database to run; shared failure and capacity |
| Database server per service | The network and the server's own login rules | The most to run; the strongest isolation |
The cheap options hold only if permissions back them. In PostgreSQL, users cannot access objects in schemas they do not own unless the schema owner grants USAGE on it, but schemas are not rigidly separated: a user can reach objects in any schema of the database if they have the privileges. That makes the minimal example a permissions example (illustrative names; the statements follow the PostgreSQL 18 syntax and were not run here):
CREATE ROLE inventory_svc LOGIN;
CREATE ROLE catalogue_svc LOGIN;
CREATE SCHEMA inventory AUTHORIZATION inventory_svc;
CREATE SCHEMA catalogue AUTHORIZATION catalogue_svc;
-- catalogue_svc has no USAGE on schema inventory, so a query such as
-- SELECT * FROM inventory.stock run as catalogue_svc is refused.
A shared application login would make every schema reachable, so use one credential per service.
Figure 2. One writer per piece of data, copies fed by events. How to read this: solid arrows are calls, flowing dashes are events. Catalogue never touches Inventory's store. It reads a copy it holds itself, and the copy is only ever written by the consumer of Inventory's events.
What you give up, and what replaces it
Splitting the data removes tools you used without thinking. Fowler and Lewis note that distributed transactions are notoriously difficult, so microservice architectures emphasise coordination without transactions, accept eventual consistency, and deal with problems through compensating operations. Remote calls also cost more than in-process calls, so remote APIs need to be coarser-grained, which is often more awkward to use.
| You lose | What replaces it | Cost |
|---|---|---|
| A join across the two tables | API composition (call both owners), or a local read model | Extra calls, or a copy that can be stale |
| One ACID transaction over both | A saga: a reliably coordinated sequence of local transactions, undone by compensating transactions (microservices.io) | A failure path to design and test; see distributed transactions |
| A foreign key | An id plus a rule for orphans | Dangling references need reconciliation |
| Reporting straight from live tables | Events or exports into a warehouse | A pipeline to run |
Copies are caches
A local read model is a table the consumer owns and the producer's events keep fresh. The owner publishes a small, versioned event:
{ "type": "StockChanged", "productId": "p-100", "storeId": "s-12", "available": 5, "version": 42 }
The consumer applies an event only if its version is higher than the stored one, so redelivery and reordering are harmless. Decisions that need the truth, such as reserving the last unit, are made by the owner, never from the copy. How the events travel reliably (outbox, brokers, replay) is in message queues, event streaming and ShopStream Stage 5.
Figure 3. The copy serves reads, the owner decides writes. How to read this: reads are fast and slightly stale, which is fine for a badge. The reservation at the bottom goes to the owner, which is the only place the real number is allowed to decide.
Failure example: the table someone else reads
A distributed monolith often begins with one harmless SELECT. A nightly finance export, written by another team, reads Orders' tables directly because that was quickest. No one records the dependency. A later release renames a column.
Figure 4. A dependency with no contract. How to read this: the owner's tests pass because the reader is invisible to them. The coupling exists only in the database, not on any diagram.
What breaks: a report, and trust in the boundary. Detection: connection metadata (PostgreSQL's pg_stat_activity view shows each session's user and application name) exposes sessions that should not exist. Prevention: separate credentials, no cross-schema grants, and a published contract (an API or an event) for every legitimate consumer.
Spotting a distributed monolith
Architecture styles lists the symptoms. The three tests turn them into a review gate: a service that fails any one is not decoupled yet. Conway's law explains why this recurs. Melvin Conway observed that an organisation that designs a system produces a design that is a copy of its communication structure. The Azure guidance draws the practical rule: if one team must own several unrelated bounded contexts, or one bounded context needs coordination across many teams, revisit either the boundaries or the team structure.
Fowler's "microservice premium" is the price: work on automated deployment, monitoring, failure handling and eventual consistency. His guideline is not to consider microservices unless the system is too complex to manage as a monolith.
| Choice | Gain | Cost | Wrong when |
|---|---|---|---|
| Keep one deployable, modular inside | One transaction, no network between modules | Releases move together | Teams block each other constantly |
| Split with private data per service | Independent change and deployment | Sagas, copies, more to operate | You cannot say who owns each table |
| Split but keep a shared database | Joins and ACID stay | Schema and runtime coupling | You wanted independence: this is the distributed monolith |
The other side of this topic
The same pressure appears in the browser. A large web application owned by many teams can be cut into independently deployed front-end pieces (micro-frontends), and Conway's law applies there too: the pieces tend to follow the teams. The Frontend System Design track plans a lesson on this; it is not published yet, so there is no link. The rule carries over: a boundary pays only where a team can ship without waiting for another.
In practice: ShopStream's stock badge leaves the shared database
This is the realistic example, an educational scenario on ShopStream with illustrative figures. Nothing in it describes a real company.
This is a realistic scenario, not a case history.
The situation
At Stage 8, ShopStream's Catalogue and Inventory code runs in one modular deployable on one PostgreSQL database with a schema each. The product list shows an "in stock at your store" badge by joining catalogue.product to inventory.stock. The team wants to extract Inventory first, because stock changes constantly and Inventory's release pace differs from Catalogue's. The join stands in the way.
Starting architecture
Figure 5. The seam to cut. How to read this: one deployable and one database. The join across schemas is the coupling that must become an event flow before Inventory can leave.
Options considered
| Option | Behaviour | Cost | Risk |
|---|---|---|---|
| A. Keep the cross-schema grant "temporarily" | Nothing changes for Catalogue | None now | The grant becomes permanent; Inventory cannot change its schema freely |
| B. Catalogue calls Inventory per request | Always current | A call per browse request, up to about 2,300 per second at the shared peak | Inventory's outages become Catalogue's outages |
C. Inventory publishes StockChanged; Catalogue keeps a read model |
Reads stay local and fast | A consumer, a copy, a lag to watch | Stale badge; mitigated by owner-decided reservation |
The decision
Option C. The badge is advisory, so seconds of staleness are acceptable. Reserving stock stays with Inventory ("available" there means on hand minus reserved), so a stale badge can disappoint but cannot oversell. The Catalogue schema gets an availability table, one row per product and store, written only by the consumer.
Rollout and verification
Run the consumer beside the old join and compare badge values. Track consumer lag and rows that disagree with Inventory. When disagreement stays at zero, revoke the grant with REVOKE USAGE ON SCHEMA inventory FROM catalogue_svc; so the old path cannot return, then move Inventory to its own deployable and database.
The 2 a.m. failure
The page reads: checkouts fail with "out of stock" for items the site shows as available. The consumer has stalled after a bad message, so the copy is hours old. Checks: consumer lag, the newest version applied against the newest published, and a sample comparison with Inventory. Mitigation: restart the consumer, send the bad message to a dead-letter queue, replay from the last good position, and meanwhile change the badge text to "check at checkout" when lag is high. The lasting fix is an alert on lag, a periodic snapshot reconcile, and a rule that the badge is never an input to a reservation.
Architect's notes
- Draw the data first. List each table and write one owner next to it. Two owners means the cut is wrong.
- Credentials enforce what diagrams promise. One login per service, no cross-schema grants.
- Contracts, not tables. Every reader goes through an API or a versioned event.
- Prefer coarse, then split. A merged service splits more easily than scattered ones merge.
- Give copies a name and an age. A read model needs a staleness budget.
Common mistakes
- Splitting the code and keeping the database. The coupling moved to the schema.
- Shared domain libraries. One "common" model package forces lockstep upgrades.
- Cutting by layer or noun. "Validation service" follows technology, not the business.
- Writing to a copy, or deciding from it. Stock checks and balances belong to the owner.
Key takeaways
- Independence has three tests: deploy alone, change the schema alone, keep working when others are down.
- Draw boundaries around bounded contexts, using the write-set of each business transaction to find cuts that hold.
- One owner per piece of data, enforced by database permissions, not by naming.
- Joins become composition or read models, ACID across services becomes a saga, and reports come from events.
- A copy is a cache: versioned, read-only, never the source of a decision.
- A shared database behind several services is a distributed monolith; Conway's law says the team structure must support the cut.
Practice exercises
Exercise 1 (30 min). For ShopStream's seven contexts, write the write-set of "place an order" and mark every pair of aggregates it changes together. Which pairs would a saga have to cover?
Exercise 2 (45 min). Take a system you know. List its tables and give each exactly one owner. Count the tables that have two or more writers.
Exercise 3 (1 hour). Write the StockChanged consumer's apply rule in pseudocode, including redelivery and reordering, and list what the read model must never be used for.
Exercise 4 (2 hours). Design the permissions (roles, schemas, grants) for Orders, Payments and Inventory in one PostgreSQL database, and say what a migration to a database per service would add.
Related reading
- Architecture Styles: monolith, modular monolith, microservices, and the strangler pattern.
- Domain-Driven Design and Bounded Contexts: how to find the contexts that service boundaries follow.
- ShopStream Stage 8: the whole system split into services.
- Distributed Transactions: sagas and their failure paths.
- CQRS and Event Sourcing: read models in depth.
Official documentation
- Lewis and Fowler, Microservices: the definition, decentralised data management, and the cost of remote calls. Fowler's Monolith First, Microservice Premium and Conway's Law.
- Fowler, Integration Database.
- microservices.io, Database per service, Shared database and Saga.
- Microsoft Azure Architecture Center, Identify microservice boundaries and Use domain analysis to model microservices.
- Evans, Domain-Driven Design Reference (2015): the Aggregates pattern.
- Vernon, Effective Aggregate Design, Part II, and the PostgreSQL 18 manual, Schemas.