Chapter 24 of 27 · Part 10: Refactoring to microservices

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

Aggregate exposing monolith, then extractionStep 1: the monolith exposes the aggregateInvoice serviceneeds employeesEmployee APIon the monolithMonolith DBemployee tablesStep 2: the API shape reveals the next service boundaryInvoice servicecalls EmployeeEmployee serviceowns employee dataMonolithnow calls it tooThe monolith still enforces valid state changes. Expose operations, not a raw table.Use a view or CDC only if the monolith cannot be changed.

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

Change data ownership: Invoice data moves outBeforeAfterMonolithInvoicetablesOthertablesInvoice svcsource of truthInvoice DBown schemaMonolithcalls APIRead-only viewlast resortIf the service runs the logic that changes the data, the data belongs to it.Expect broken foreign keys and transaction boundaries (sections below).
SituationDo this
Data owned by the monolithExpose an API or event stream from the monolith.
Data managed by new service logicMove it; the monolith calls the new service.
Monolith only needs readsOptionally 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.

Logical split versus physical splitLogical onlyLogical and physicalSchema ASchema BSchema ASchema BLogical: independent change, information hiding. Views need the same engine.Physical: fewer shared failures, less resource contention, more cost.
Three orderings for extracting a serviceSchema firstSplit schemaSplit codeEarly warning; slow paybackCode firstSplit codeSplit schemaQuick win; risk of stoppingBoth at onceOne big stepLate feedback; avoid
OrderBenefitRiskPick when
Schema firstReveals performance and transaction problems early; easy to revertLittle short-term gain; not possible for black-box softwareYou worry about performance or consistency and can change the monolith
Code firstIndependent deployable sooner; shows which data is neededTeams stop and keep a shared DB; join pain is discovered lateDefault for most teams, if you commit to finishing the data split
TogetherOne stepToo big; you learn the impact lateAvoid

Patterns that prepare either path

PatternWhat you doWhy
Repository per bounded contextSplit 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 contextSeparate schemas inside one deployable, such as a modular monolithHedges your bets; the author nearly always advises it for brand-new systems.
Monolith as data access layerService calls an API on the monolithAvoids splitting data yet; good for a mostly stateless service.
Multischema storageNew data in a new schema, old data read from the monolithStops the monolith schema growing; move tables later one at a time.
Hot take. If you can change the monolith and worry about performance or consistency, split the schema first. Otherwise split the code, and use it to learn about data ownership. Track schema changes as versioned delta scripts (for example Flyway).

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.

Moving data: write to both, then switch readsBulk copy and catch up, then write to both while reading old, then read from new.1 Bulk copythen catch up on changesApplicationOld DBNew DB2 Write to bothstill read from oldApplicationOld DBNew DB3 Read from newkeep writing to bothApplicationOld DBNew DBTeal marks the source of truth. Old DB is retired only after the new one has bedded in.This works best when only one writer (monolith or service) is active at a time.
Pattern: synchronize data in application (the Danish medical records story).

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.

Tracer write: move the source of truth in slicesBasic data, then contacts, then payments are synchronized, and consumers switch one by one.Step 1basic invoice dataStep 2+ contact detailsStep 3+ payment recordsConsumers switch to the new service as each slice arrives; the old copy retires last

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.

Change data capture feeds a new serviceThe monolith database feeds a change capture step, an event stream and a new service.Monolith DBChange captureEvent streamNew serviceTwo ways to captureQuery with a watermarksimple, but may squash updatesRead the database logsees every change, leaks table shape
Based on Practical Event-Driven Microservices Architecture.

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

Synchronize data in the application, step by step1. Bulk syncsnapshot + CDCboth in sync2. Write bothread from oldold is truth3. Write bothread from newnew is truth4. Retire olddrop old schemaYou can switch back at any step. Run a reconciliation (even simple SQL) to prove they match.Example: a Danish medical-record system moved from MySQL to Riak this way.Fine with one writer at a time; hard if monolith and service both sync (canary).

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.

Tracer write moves data in slicesStage 1Basic invoice (new)Contact infoPaymentsStage 2Basic invoice (new)Contact info (new)PaymentsStage 3Basic invoice (new)Contact info (new)Payments (new)Two sources of truth are tolerated for a while. Consumers switch as data arrives.When the last consumer has moved, stop synchronizing and retire the old copy.

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.

Reconcile. With two sources of truth you need a check that the sync works, or you find out too late. Running the new source with no consumers first, as Square did, is sensible.

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.

Square: moving fulfilment data out of the Order conceptClients: restaurant and driver codeExisting systemOrder dataBackground workerfeature-flaggedFulfillments svcnew source of trutheach change goes to bothThe worker copied old data through the service API, not the DB, and could be switched off.

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.

A join becomes a service callIn one schema a foreign key and one query join Ledger and Albums. After the split Finance asks Catalog for the details.Before: one schema, one queryLedgerAlbumsFKThe database joins and keeps integrityAfter: two servicesFinanceCatalogSKUs in bulkLedger DBAlbums DBWhat if an album row is deleted?Check before deleterace-prone, adds couplingHandle it gracefullyshow "not available", 410 GoneDo not deletesoft delete or graveyard tableDo not split an aggregate: order lines belong with their order, so move them together.
MusicCorp best-sellers report from Building Microservices and the database chapter.

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.

Splitting a table that serves two contextsCase 1: columns divide cleanlyItemname, price, stockCatalog itemname, priceStock itemSKU, stockCatalog owns namesWarehouse owns stockCase 2: two contexts change one columnFinanceunpaid billsCustomer serviceowns StatusNOT_VERIFIED, VERIFIED,SUSPENDEDKeep a state machine in one service: Finance calls Customer to suspend. Split the table insidethe old schema first. You lose the single transaction (see Transactions).

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.

Replacing a join with service callsLedger tableSKU, amountFinancetop SKUs for monthCatalog servicebulk SKU lookupAlbums tabletitles, artistsBefore: one SELECT with a join. After: query, call, then merge in Finance.Latency rises. Mitigate with bulk lookups and caching; measure it (tracing helps).A monthly report tolerates this; a hot path may not.

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.

Check the aggregate first. An order and its order lines are one unit. Do not split them; move both together. Bigger bites can avoid breaking the key at all.

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.

Four ways to share static reference dataDuplicatecopy per service, local schemafine if data stays inside a servicerisk: services disagreeDedicated schemaone versioned shared schemajoins possible on same engineschema change hurts everyoneLibraryenum or file bundled in codesmall, rarely changing dataversions may differ in productionServiceone source, cached by clientsevents refresh cachescost of one more serviceExample: about 249 country codes. The last new country was South Sudan in 2011.
OptionBest whenWatch out
DuplicateLarge data used only locally, such as postal code filesInconsistency matters if the data is part of messages between services
Dedicated schemaYou want one source and cross-schema joinsTreat it as a versioned interface; may need one engine
LibrarySmall, stable data; consistency across services is not criticalNot independently deployable if all must update at once; mixed stacks
ServiceData must be consistent, or has an update API or eventsOverkill if creating services is slow; can run as a function
Author's pick. Without strict consistency: a shared library for small simple data, or local copies for larger data. If all services need the same view: a dedicated service. Dedicated schema only when a service is hard to justify. Tolerating duplication can beat adding coupling.

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.

Four ways to share static reference dataDuplicate it, put it in a shared schema, ship it in a library or serve it from a service.Copy per servicemay drift apartShared schemaa versioned interfaceLibrarysmall, rarely changesServiceall must agreeNewman's pick: a library for small data. A service when every consumer must agree.Some duplication beats extra coupling, if the data stays inside one service.
Example: country codes.
OptionGoodWatch out
DuplicateNo coupling at allCopies can differ, which matters only if they are exchanged between services
Shared schemaOne source. Can still joinA shared database. Changes hit everyone
LibrarySimple and visibleDifferent services may run different versions. Needs one tech stack
ServiceOne source plus events or cache refreshOnly 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.

From problem to patternMany consumers read my tablesView or wrapping serviceClients need SQL or reportsRead-only exposed databaseNew service needs monolith dataAggregate exposing monolithService logic changes the dataChange data ownershipNeed fallback between old and newSync in applicationWant to move data in slicesTracer writeTransaction across split dataSaga, or keep it together
PatternUse whenMain risk
Database viewCannot decompose yet; read-only consumersRead-only, same engine, stale if materialized
Wrapping serviceSchema too tangled; stop it growingConsumers must change; only a bandage
Database-as-a-serviceClients need SQL or reportsMapping engine lag and upkeep
Aggregate exposing monolithData stays in monolith for nowMonolith must change
Change data ownershipService holds the logicFK and transaction fallout
Synchronize in applicationNeed fast fallback; single writerDual-writer mistakes
Tracer writeGradual data moveTwo truths; needs reconciliation
Split table / FK to codeTable spans contextsLatency, dangling references
Static data optionsShared reference dataDrift or lock-step releases
SagaProcess across servicesNo 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.

StepActionPattern
1Split data-access code into catalog, finance, warehouse and customer repositories; chart foreign keysRepository per bounded context
2Item mixes names and stock: split into Catalog item and Stock item inside the monolithSplit table
3Customer Status is also set by Finance: Customer owns it; Finance will call itSplit table
4Extract Catalog; Ledger needs album titles: bulk lookup, no album deletion, tolerate missingMove FK to code
5Move Catalog data via dual write, then switch reads, then retire the old copyTracer write
6Give each service its own schema; country codes become a shared libraryStatic data library
7Order placement across Warehouse, Payment and Loyalty becomes a saga, with refunds as compensationSaga
MusicCorp after the splitOrder saga: reserve, pay, award points, dispatch; compensate on failureWarehouse svcown schemaCatalog svcown schemaFinance svcown schemaCustomer svcown schemaFinance asks Catalog for album details by SKU, and calls Customer to suspend an account.Country codes: shared librarysmall and stable; no schema or service neededStock levels split from Item into Warehouse; Catalog keeps names and prices.

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

Quick check

You split the code first and the new service still reads the monolith schema. What is the main risk?
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.

An album row is deleted in Catalog but Finance holds ledger rows for it. What does the author do?
Show answer

Disallow deletion in Catalog (soft delete) and make Finance handle a missing record gracefully, for example with a 410 response.

Which static-data option when small data needs no strict consistency?
Show answer

A shared library, accepting that services may run different versions.