Decomposing the database
Move data out of the monolith safely: who owns it, which to split first, keeping both sides in sync.
key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral
Who owns the data? Two directions
Ask first where each piece of data should live. If it comes with the service's logic, move it. If it stays with the monolith, access it through an interface.
Aggregate exposing monolith
The new service calls the monolith for data it does not own. This is little more work than reading the tables directly and keeps the boundary clean. The author calls a close cousin monolith as data access layer and is surprised it is not better known; it suits a service that is mostly stateless.
Change data ownership
| Situation | Do this |
|---|---|
| Data owned by the monolith | Expose an API or event stream from the monolith. |
| Data managed by new service logic | Move it; the monolith calls the new service. |
| Monolith only needs reads | Optionally project a view from the new database, but prefer direct calls. |
From Monolith to Microservices, ch. 4: "Pattern: Aggregate Exposing Monolith" and "Pattern: Change Data Ownership".
Which to split first: schema or code?
An extraction is done only when the code runs in its own service and its data lives in its own logical schema. The order is a choice. First, a reminder that logical and physical are separate decisions.
| Order | Benefit | Risk | Pick when |
|---|---|---|---|
| Schema first | Reveals performance and transaction problems early; easy to revert | Little short-term gain; not possible for black-box software | You worry about performance or consistency and can change the monolith |
| Code first | Independent deployable sooner; shows which data is needed | Teams stop and keep a shared DB; join pain is discovered late | Default for most teams, if you commit to finishing the data split |
| Together | One step | Too big; you learn the impact late | Avoid |
Patterns that prepare either path
| Pattern | What you do | Why |
|---|---|---|
| Repository per bounded context | Split data-access code by context (for example one Hibernate mapping per context) | Shows which code touches which tables. Add SchemaSpy to see database-level foreign keys. |
| Database per bounded context | Separate schemas inside one deployable, such as a modular monolith | Hedges your bets; the author nearly always advises it for brand-new systems. |
| Monolith as data access layer | Service calls an API on the monolith | Avoids splitting data yet; good for a mostly stateless service. |
| Multischema storage | New data in a new schema, old data read from the monolith | Stops the monolith schema growing; move tables later one at a time. |
From Monolith to Microservices, ch. 4: "Splitting the Database First, or the Code?" and "Physical Versus Logical Database Separation".
Moving the data safely
When a new service takes over a feature, it must also take over the data the feature changes (the change data ownership pattern). The monolith then calls the service, or reads a view of its data. During the switch the two sides must agree.
The database change is staged so you can fall back at any step: first get a copy, then write to both while still reading the old one, then read the new one while still writing to both. A Danish medical-records project used this with MySQL and Riak because it could not go offline.
A tracer write tolerates two sources of truth for a while. Move one slice of data at a time and let consumers switch as slices become ready. For syncing you can write to one source and copy afterward, or write to both. Avoid letting clients write to either, since that needs two-way sync. Whatever you pick, expect a window of inconsistency and add a reconciliation check.
If you cannot change the monolith, or it has stored procedures, support-team data fixes and side applications that all change the data, publishing events from code will miss changes. Practical Event-Driven Microservices Architecture suggests change data capture instead. Query-based capture works on any database but can miss intermediate updates and deletes. Log-based capture sees everything, but both approaches can expose the internal table shape. Where you can, turn raw changes into events with business meaning.
Synchronizing data while both sides run
If you want to switch back and forth between monolith and service (see the strangler approach in lesson 23), both must see the same data. A shared DB for a very short time is one option. Otherwise, synchronize.
Pattern: synchronize data in application
Pattern: tracer write
Move the source of truth gradually instead of one big switchover. The old system keeps its local copy but also writes to the new service. Start small, then add data and consumers.
Keeping two sources consistent: write to one and sync onward (simple, preferred); send writes to both (the client or an intermediary may fail halfway); or accept writes at either with two-way sync (avoid; very hard). All give eventual consistency, so a nightly batch means up to a day of drift.
Worked case: Square
An Order served customers, restaurants and drivers. Square created a Fulfillments service for the restaurant and driver data. Callers made two API calls; the writes were not atomic, which was fine because the gap was tiny. An event stream could have done the same job via a shared topic. Switching consumers became a non-event.
From Monolith to Microservices, ch. 4: "Data Synchronization", "Pattern: Synchronize Data in Application", "Pattern: Tracer Write" and "Example: Orders at Square".
Splitting tables, keys and joins: the short tour
Once tables live in different schemas, three things change: a table may need to be cut, a foreign key stops working, and a join becomes slower code.
- Split a table: when one table serves two contexts (an item table with both catalog data and stock levels) cut it by column ownership first, inside the old schema. If two codebases write one column, such as a customer status, give it to the service that owns the concept. Others then call that service to change it.
- Move the foreign key to code: the join moves into the calling service, so latency usually rises. Bulk lookups and caching help. Measure with distributed tracing before worrying.
- Accept the loss of integrity: no database stops you deleting a referenced row. See the choices below.
The authors prefer handling missing rows gracefully, together with not deleting at all. A 410 Gone answer says the item once existed, which a plain 404 does not. And if you worry that the split cuts through one aggregate, move the whole aggregate together.
Splitting tables and foreign keys
These patterns work on relational schemas but also guide NoSQL moves.
Split table
Split along the bounded contexts in the old schema before you separate schemas. If one column is changed by two contexts, decide who owns it by domain concept.
Move foreign key to code
A Ledger row references an Albums row by SKU, and the monthly report joins them. Once Albums moves to Catalog, the database cannot join or enforce the reference.
The second problem is integrity: nothing stops a delete of a referenced album. Options: checking Finance before deleting is racy and creates a reverse dependency (avoid); handling a missing record gracefully, for example HTTP 410 GONE and an "album not available" line; or forbidding deletion with a soft delete or graveyard table. The author does both of the last two.
The short tour above covers the same moves in brief.
From Monolith to Microservices, ch. 4: "Pattern: Split Table" and "Pattern: Move Foreign-Key Relationship to Code".
Shared static reference data
Country codes, clothes sizes and similar data sit in a shared database surprisingly often. Four options exist.
| Option | Best when | Watch out |
|---|---|---|
| Duplicate | Large data used only locally, such as postal code files | Inconsistency matters if the data is part of messages between services |
| Dedicated schema | You want one source and cross-schema joins | Treat it as a versioned interface; may need one engine |
| Library | Small, stable data; consistency across services is not critical | Not independently deployable if all must update at once; mixed stacks |
| Service | Data must be consistent, or has an update API or events | Overkill if creating services is slow; can run as a function |
From Monolith to Microservices, ch. 4: "Example: Shared Static Data".
Reference data and transactions
Small, rarely changing data such as country codes or clothing sizes is often stored in the database and shared. Decomposing the Database gives four options.
| Option | Good | Watch out |
|---|---|---|
| Duplicate | No coupling at all | Copies can differ, which matters only if they are exchanged between services |
| Shared schema | One source. Can still join | A shared database. Changes hit everyone |
| Library | Simple and visible | Different services may run different versions. Needs one tech stack |
| Service | One source plus events or cache refresh | Only worth it if creating a service is cheap |
Transactions are the last problem. A single database transaction cannot span two schemas, so splitting a table that was updated in one transaction means you give up atomic updates. The chapter suggests avoiding distributed transactions, because one failed node stalls the commit and bigger systems fail more often. It prefers modeling the business process as a saga. See lesson 09. For analysis across services, Building Microservices adds a reporting database: each service pushes a deliberately small subset of its data to a database built for readers, and keeps that interface compatible like any other endpoint.
Decision guide: which pattern when
Start from your problem, not from a favourite pattern.
| Pattern | Use when | Main risk |
|---|---|---|
| Database view | Cannot decompose yet; read-only consumers | Read-only, same engine, stale if materialized |
| Wrapping service | Schema too tangled; stop it growing | Consumers must change; only a bandage |
| Database-as-a-service | Clients need SQL or reports | Mapping engine lag and upkeep |
| Aggregate exposing monolith | Data stays in monolith for now | Monolith must change |
| Change data ownership | Service holds the logic | FK and transaction fallout |
| Synchronize in application | Need fast fallback; single writer | Dual-writer mistakes |
| Tracer write | Gradual data move | Two truths; needs reconciliation |
| Split table / FK to code | Table spans contexts | Latency, dangling references |
| Static data options | Shared reference data | Drift or lock-step releases |
| Saga | Process across services | No atomicity; compensation design |
Reporting: this chapter covers only the exposed read-only database for one service. Feeding a cross-service warehouse is left to another chapter of the book; see lesson 08 for the course view.
From Monolith to Microservices, ch. 4: chapter-wide synthesis; the author stresses small, incremental steps.
Worked example: MusicCorp
Apply the chapter to the music shop. The monolith holds Item, Albums, Ledger, Customer and country codes in one schema.
| Step | Action | Pattern |
|---|---|---|
| 1 | Split data-access code into catalog, finance, warehouse and customer repositories; chart foreign keys | Repository per bounded context |
| 2 | Item mixes names and stock: split into Catalog item and Stock item inside the monolith | Split table |
| 3 | Customer Status is also set by Finance: Customer owns it; Finance will call it | Split table |
| 4 | Extract Catalog; Ledger needs album titles: bulk lookup, no album deletion, tolerate missing | Move FK to code |
| 5 | Move Catalog data via dual write, then switch reads, then retire the old copy | Tracer write |
| 6 | Give each service its own schema; country codes become a shared library | Static data library |
| 7 | Order placement across Warehouse, Payment and Loyalty becomes a saga, with refunds as compensation | Saga |
Result: each service owns its data, relies on calls instead of joins, and accepts eventual consistency. Reporting could be added later through an exposed read-only database or a warehouse. Each step shipped on its own.
From Monolith to Microservices, ch. 4: MusicCorp examples (Catalog, Finance, Warehouse, Ledger, Albums, Item, Customer); the combined sequence here is an illustration, not the book's own case.
Recap
- Sync data by writing to both, tracer writes or change data capture, and expect a short window of inconsistency.
- A join becomes a service call and a foreign key becomes a check in code. Keep aggregates together.
- Data follows the logic that changes it. Expose monolith-owned aggregates by API.
- Splitting schema first reveals problems early; splitting code first pays off sooner but risks stopping with a shared DB. Avoid doing both at once.
- Sync in application and tracer write let you move data in steps with a fallback. Always reconcile.
- Choose static-data options by consistency needs: library for small data, service for strict consistency.
Quick check
Show answer
The team stops there and a shared database stays in place long term, with join problems discovered late. Plan to finish the data split.
Show answer
Disallow deletion in Catalog (soft delete) and make Finance handle a missing record gracefully, for example with a 410 response.
Show answer
A shared library, accepting that services may run different versions.