Extracting code is the visible half of a migration; the data is the hard half, and it gets the longest chapter in the book. Every extraction has to answer the same questions: who owns this data, how do we move it without downtime, and what replaces the joins and transactions we used to get for free?
Here I group the chapter's patterns by the problem they solve, with examples from an online course platform: learners, courses, enrollments, payments and certificates.
Chapter 4 in the book.
Key ideas
Living with a shared database while you work
A shared database is implementation coupling at its strongest: nobody can tell which columns are safe to change, and the logic behind an entity's state is scattered across codebases. If the enrollment status can be changed by the course service, the payment service and a nightly job, no single place enforces what a valid transition is.
Newman accepts direct sharing in only two cases: static, read-only reference data, and a database that a service deliberately publishes as an endpoint for many consumers. Everything else should eventually split. Until it can, three coping patterns stop things from getting worse:
| Pattern | How it works | Limits |
|---|---|---|
| Database view | Consumers read a projection instead of the raw tables | Read-only, same database engine, needs an owner |
| Database wrapping service | A thin service in front of a tangled schema becomes the only way in | Consumers must move from SQL to API calls |
| Database-as-a-service interface | A separate read-only database for reporting and SQL tools, filled by a mapping process | Data lags; the mapping needs real engineering |
A view is the cheapest first step. If the marketing team's tool reads learner rows directly, a dedicated schema with a narrow view lets the course team restructure the underlying table freely:
CREATE SCHEMA marketing_read;
CREATE VIEW marketing_read.active_learners AS
SELECT l.id AS learner_id,
l.display_name,
l.country_code,
l.last_active_at
FROM learner AS l
WHERE l.deleted_at IS NULL;
The book treats a published view as an interface owned by the source team, and frames all three patterns as stepping stones towards a real split. Newman also recommends giving each consumer its own database credentials, so you can at least see who reads what.
Deciding who owns the data
Before moving any rows, decide where each piece of data should live. Newman's rule follows from treating a service as behaviour plus state: whoever owns the state transitions owns the data.
If the new service needs data the monolith still owns, use the aggregate exposing monolith pattern. The monolith publishes an API (or events) for that aggregate, with real operations rather than raw table access. Say a new certificate service needs to know whether a learner completed a course. The enrollment logic still lives in the monolith, so the monolith exposes GET /enrollments/{id} and the certificate service never touches the enrollment table. As the book points out, that API is also a sketch of a future enrollment service.
If the new service owns the logic that changes the data, use change data ownership. The data moves into the service's schema, and the monolith becomes a client that calls the service. If the monolith only needs to read, the service could project a view back into the monolith's database, but Newman prefers changing the monolith to call the service.
A related rule is easy to apply: new data the monolith never had goes into the new service's own schema from day one (the book calls this multischema storage), even while old data is still read from the monolith.
Moving the source of truth without a big bang
The book offers two patterns for moving data while old and new code are both live, and both keep a rollback path open.
Synchronize data in application comes from a migration of Danish medical records to a new database. It runs in stages:
stage 1 bulk copy old -> new, then catch up with change data capture
stage 2 write to both, read from old (new store is on probation)
stage 3 write to both, read from new (old store is the fallback)
stage 4 stop writing to old, retire it
It works best when only one codebase writes at a time; if both might receive writes for the same records, as during a canary, syncing gets very hard.
Tracer write moves the source of truth gradually and tolerates two sources of truth for a while. You synchronise a small slice of data, move consumers one by one, then add the next slice. On the course platform you might move basic payment records first, then refunds, then invoices. The book's example is Square, where a new fulfilment service was filled by a background worker behind a feature flag and kept in sync by callers updating both systems.
The design question is how writes reach the two sources:
| Option | How it works | Verdict |
|---|---|---|
| Write to one source | Clients write to one side; a background process copies to the other | Simple, one-way |
| Send writes to both | Clients (or an intermediary) send every write to both sides | Workable; clients must handle partial failure |
| Seed writes to either | Clients write to either side; data syncs both ways | Avoid: two-way sync is very hard |
Either way you get eventual consistency: a nightly batch can leave you a day behind, streaming CDC only seconds. The advice I value most is to build a reconciliation check from the start, even just a few count and sum queries run against both stores. Without it, you find drift only when a learner complains.
Schema first or code first?
An extraction is finished only when the code runs in its own service and its data lives in its own logically isolated schema. (Newman uses schema to mean a logical boundary; several can share one database engine.) There are three orders:
| Order | Benefits | Risks |
|---|---|---|
| Schema first, code stays | Surfaces join, latency and transaction problems early; easy to revert | Little short-term payoff; impossible if the monolith is a black box |
| Code first, schema later | A deployable service sooner; clearer view of the data it needs | Teams stop halfway and keep the shared database forever |
| Both at once | One step | A big step with late feedback; the book advises against it |
Newman's own take: if he can change the monolith and worries about performance or consistency, schema first; otherwise code first, with an honest commitment to finish the data split. Two patterns help either way. A repository per bounded context shows which code touches which tables, and a tool such as SchemaSpy reveals foreign keys that will cross future boundaries. A database per bounded context keeps separate schemas even inside a monolith; Newman recommends it for most brand-new systems, and his ThoughtWorks example never needed to split at all.
Schema-level surgery: tables, foreign keys and reference data
Split table. One table often serves two contexts. A course table might hold catalogue fields (title, syllabus) and sales fields (price, discount). Split it inside the existing schema first, then move each half. When one column is written by two contexts, decide who owns the state machine: in the book, a customer status changed by both sign-up and finance code goes to the customer side, and finance asks the customer service to suspend someone.
Move foreign-key relationship to code. Once payment and course live in different schemas, the foreign key between them disappears, and you lose two things:
- Joins. A revenue report that joined payments to course titles becomes a query plus a service call. It will be slower, so batch lookups or cache course data, and measure whether the latency matters.
- Referential integrity. The database no longer stops someone deleting a course that payments still reference.
For deletions, the book rejects checking with every consumer before deleting (fragile and highly coupled). Better options are handling a missing record gracefully, for example with HTTP 410 Gone so callers can tell "removed" from "never existed", and not deleting at all, using a soft-delete status. Newman would combine the two. Before cutting any foreign key, check you are not splitting an aggregate: an order and its order lines belong together.
Static reference data. For shared lookups like country codes or course levels there are four options:
| Option | When it fits |
|---|---|
| Duplicate per service | Large datasets where services need not agree exactly |
| Dedicated reference data schema | Large data or cross-schema joins; treat the schema as a versioned interface |
| Static reference data library | Small, rarely changing data where versions may differ for a while |
| Static reference data service | Data that must be consistent everywhere, or whose life cycle you manage in code |
Newman's default is the library for small data that need not be perfectly consistent, a service when it must be, and a dedicated schema only when a new service is hard to justify. A library can be this small:
export const courseLevels = ["beginner", "intermediate", "advanced"] as const;
export type CourseLevel = (typeof courseLevels)[number];
export function isCourseLevel(value: string): value is CourseLevel {
return (courseLevels as readonly string[]).includes(value);
}
Services will run different versions of it, so add a new level well before anyone depends on it.
Transactions and sagas
Splitting data breaks atomic transactions. When a learner pays, the platform records the payment, activates the enrollment and grants access; in the monolith one transaction did all three. Newman explains why he avoids two-phase commit: participants hold locks between vote and commit, commits do not land at the same instant, and a participant that votes yes and then goes silent leaves everyone stuck. His first suggestion: if state truly needs atomic changes, do not split it yet.
Otherwise, use a saga: a sequence of local transactions with explicit recovery. Backward recovery runs compensating transactions, which are semantic rollbacks; you cannot unsend the welcome email, only send a correction. Forward recovery retries from the failed step. Putting the steps most likely to fail first, and irreversible effects last, removes many compensations. A minimal orchestrated version:
type SagaStep<Context> = {
name: string;
run: (context: Context) => Promise<void>;
compensate?: (context: Context) => Promise<void>;
};
type SagaResult =
| { status: "completed" }
| { status: "compensated"; failedStep: string };
export async function runSaga<Context>(
steps: SagaStep<Context>[],
context: Context,
): Promise<SagaResult> {
const completed: SagaStep<Context>[] = [];
for (const step of steps) {
try {
await step.run(context);
completed.push(step);
} catch {
for (const done of [...completed].reverse()) {
await done.compensate?.(context);
}
return { status: "compensated", failedStep: step.name };
}
}
return { status: "completed" };
}
A real implementation must persist saga state and handle failing compensations; this only shows the shape.
An orchestrated saga, like this one, keeps the process explicit in one place, but the orchestrator knows every participant and business logic tends to drift into it. A choreographed saga lets services react to events with no coordinator; coupling is looser, but you need a correlation ID on every event and a projected view to know what state a saga is in. Newman is relaxed about orchestration when one team owns the whole saga and prefers choreography when several teams share it, and he admits a personal leaning towards choreography.
Reading it in 2026
The patterns have aged well; the tooling around them has changed.
- The outbox pattern. The book does not name it, but it is now the usual way to publish events reliably during change data ownership or a choreographed saga. You write the business change and an event row in one local transaction, and a relay or CDC tool publishes the row afterwards.
BEGIN;
UPDATE enrollment
SET status = 'active', activated_at = now()
WHERE id = 'enr_42';
INSERT INTO outbox_event (id, aggregatetype, aggregateid, type, payload)
VALUES (gen_random_uuid(), 'enrollment', 'enr_42', 'EnrollmentActivated',
'{"enrollmentId": "enr_42"}');
COMMIT;
- CDC. Newman already names Debezium as his choice of mapping engine. It is still the common open source option, it moved to the Commonhaus Foundation in late 2024, and its outbox event router consumes exactly this kind of table.
- Workflow engines. Durable execution engines such as Temporal are a common way to build orchestrated sagas without writing your own persistence and retries. The book's design questions (ordering, compensations, who owns the flow) remain.
Putting it to work
- List every service, job and tool that connects to the monolith's database, give each its own credentials, and put a view or wrapping service in front of readers you cannot remove yet.
- For each extraction, write down who owns each table, choose aggregate exposing monolith or change data ownership accordingly, and put brand-new data in the new service's schema.
- Decide schema first or code first per extraction, record why, and set a date for finishing the data split.
- When moving a source of truth, keep writes one-way, and build a reconciliation check before switching any reader.
- Before removing a foreign key, check you are not splitting an aggregate, and decide how consumers handle a missing record.
- For data that changes atomically today, try leaving it together; if you must split it, model a saga with compensations and publish events through an outbox.
Check yourself
When does Newman accept a shared database? For static read-only reference data, and for a database deliberately published as an endpoint for consumers.
Why does the book warn against "seed writes to either source" in a tracer write? It needs two-way synchronisation, which is very hard to get right.
What do you lose when you move a foreign key into code? Database joins (now a slower service call) and referential integrity enforced by the database.
Which reference data option does Newman lean towards for small, stable data? A shared library, accepting that services may run different versions for a while.
When does he consider orchestration acceptable? When one team owns the whole saga; with several teams he prefers choreography.
Further reading
- Debezium: outbox event router: how outbox rows become Kafka events.
- Temporal documentation: durable workflows for implementing orchestrated sagas.
- PostgreSQL: CREATE VIEW: what views can and cannot do, including updatable views.