How to extract a service from a monolith safely
Learn how to extract a service from a monolith by choosing data independence, limiting deployment risk, and making rollback a routing change.

The first service you extract should prove that the monolith can lose responsibility without losing control. Choose it by data ownership and deployment risk, not by how neatly someone can draw a box around a module. A tidy package with shared tables, synchronous callers, and one large transaction is still part of the monolith in every way that matters.
I have seen teams extract the code that looked most isolated, celebrate a clean repository, then discover that every release required a coordinated database change and a midnight call with three owners. The service existed as a process, but not as an operational unit. The first real production fault sent it straight back into the monolith.
A sound first extraction has a narrow write surface, data that can gain one clear owner, callers that tolerate a network boundary, and a release path that can reverse without repairing data by hand. It may look boring on the architecture diagram. Boring is useful when the team is learning how its system actually behaves.
Start with data ownership, not a business noun
The best first service owns a coherent set of facts and can reject writes without consulting half the monolith. That criterion is stricter than having classes named Billing, Customer, or Inventory. A business noun often spans workflows, reports, permissions, and historical tables that different code paths update for different reasons.
Ask which component can become the sole authority for a small set of records. Ownership means it validates changes, commits them, assigns identifiers, and publishes the result. Other code may cache or copy those facts, but it no longer edits the authoritative rows. If both the monolith and the new service remain legitimate writers, you have created a distributed race rather than a boundary.
Trace writes before reads. Reads are usually easier to duplicate, cache, or serve through a compatibility view. Writes expose the rules that keep data valid. Search application code, stored procedures, scheduled jobs, database triggers, import scripts, support utilities, and direct operator commands. Legacy systems often hide decisive writes in places the main repository does not make obvious.
A useful candidate might own document rendering requests, notification preferences, exchange-rate snapshots, or completed export jobs. These examples have a bounded state transition and often tolerate asynchronous work. A poor candidate is usually customer, order, account, or entitlement when those records sit inside every important transaction. Those domains can become services later, but choosing them first makes the migration teach the hardest possible lesson at the highest possible cost.
Do not confuse a table group with a domain. If a table has twelve incoming foreign keys, two triggers that update other aggregates, and a nightly job that corrects it, moving that table does not create independence. It moves the center of a dependency net. PostgreSQL's documentation describes foreign keys as dependencies between referencing and referenced rows. That guarantee is local to the database transaction. Once the tables live behind separate services, your application must replace the guarantee deliberately or accept weaker consistency.
Measure coupling in runtime evidence
Static dependency graphs are a starting point, but production evidence tells you whether a candidate can survive a process boundary. Instrument the monolith long enough to observe callers, query shapes, write frequency, transaction duration, payload size, latency sensitivity, retry behavior, and traffic peaks. The important unit is not a source file. It is a request or job that crosses the proposed seam.
Build a candidate ledger with one row per proposed service. Record the tables it reads and writes, every caller, whether each call sits inside a user request, whether a failure can wait, and which invariants currently rely on one database transaction. Add the deployment owner and the rollback action. If either of those cells says “to be decided,” the candidate is not ready.
Repository search will miss dynamic SQL, reflection, generated queries, and code deployed outside the repository. Database statement logs and traces catch those paths. Sample long enough to include scheduled work such as settlement, billing, reconciliation, archival, and month-end close. The quiet endpoint you observe on Tuesday can be the center of the system on the last business day of the month.
Count cross-boundary operations rather than method calls. One chatty repository method may issue twenty queries but move cleanly with its data. One innocent getter may participate in a transaction that locks rows across four domains. PostgreSQL's explicit-locking documentation notes that SELECT FOR UPDATE blocks conflicting updates and locking requests until the transaction ends. Replacing that local wait with a remote call changes timing, failure modes, and deadlock behavior even if the returned fields stay identical.
Treat unknown traffic as risk, not as zero. When logs cannot identify who updates a table, add observation before extraction. A proxy, database audit trigger used temporarily, or trace tag at the data-access layer can expose the writer. Guessing is faster only until the unknown writer overwrites the new service's state.
The evidence should answer four awkward questions: Can the service be unavailable without blocking the main transaction? Can a caller retry without duplicating work? Can an operator explain which copy of a record is authoritative? Can the team disable the route while leaving both stores internally consistent? A first service need not earn a perfect answer to all four, but each weak answer needs a tested control.
A clean diagram can hide a terrible first cut
Reject candidates whose appeal depends mainly on organizational neatness. Teams often choose a module because one squad already owns it, its namespace is tidy, or a vendor diagram labels it as a bounded context. None of those facts says the code can deploy alone.
Authentication is a common trap. It looks horizontal and self-contained, yet every request depends on its latency and availability. Its data may also mix credentials, sessions, roles, tenant membership, audit history, and recovery workflows. An authentication extraction can be sound, but it is a poor rehearsal when one routing error locks out the whole company.
Shared reference data creates another trap. A country-code or product-category module appears read-only until administrators edit it, caches refresh at different times, and business rules expect a change to take effect inside the same transaction. The volume is low, but the fan-out is enormous. Low traffic does not imply low deployment risk.
Reporting code looks safer because reports rarely write core records. The hidden cost is query ownership. A report that joins thirty monolith tables does not become independent when an HTTP endpoint wraps the same SQL. It becomes a remote query service tied to every schema it reads. Extracting a reporting projection can work after you define its feed and tolerate delay. Extracting the existing joins merely adds a network hop.
The recommendation I argue against is “extract the easiest module first.” It is popular because a quick repository split creates visible progress and gives a team a new deployment pipeline. It is wrong when ease refers only to code movement. Choose the easiest responsibility to operate independently, including data, failure, deployment, observation, and reversal. That definition produces less theater and more learning.
Deployment risk matters more than service size
A small service can carry a huge blast radius, while a larger asynchronous worker can fail quietly and recover from its queue. Rank the first extraction by what happens during a bad release. Code volume is a weak proxy. The percentage of critical requests that cross the boundary is far more revealing.
Prefer a candidate outside the synchronous path for login, checkout, authorization, or the primary record update. Notification delivery, file conversion, export generation, and document indexing often make better first boundaries because the monolith can enqueue work and continue. This does not make them disposable. It gives the team time to observe, retry, and repair without turning every service fault into a site outage.
Check resource coupling too. A new process can overload the same database with a different connection pool, retry a slow query more aggressively, or exhaust a shared queue. Isolation on a deployment diagram does not isolate CPU, locks, connections, or downstream quotas. Put explicit limits around concurrency and retries before shifting production traffic.
Score each candidate against consequences that operators can recognize:
- A ten-minute outage is lower risk when work queues and catches up; it is a warning when core requests fail.
- One component should write authoritative data; several applications and jobs are a warning.
- A stable idempotency key makes retries safer; blind request replay is a warning.
- A route or consumer switch should reverse a release; data merging and schema rollback are a warning.
- Compared outputs and domain metrics should detect semantic faults; process health alone is a warning.
Do not turn the table into a fake numerical formula. A score of 17 does not cancel one answer that says “we cannot reconstruct the missing writes.” Use it to expose veto conditions and force explicit discussion. For the first extraction, irreversible data divergence is a veto. So is a dependency that requires every caller to deploy in the same window.
Define the contract before moving the implementation
The extraction contract must describe behavior under retries, stale input, duplicate input, timeouts, and partial failure. A list of endpoints and fields is not enough. Callers need to know which request identity remains stable, which state transition is allowed, and what response means the service has durably accepted responsibility.
Write the contract against the behavior you already have, including behavior nobody likes. If the monolith accepts duplicate submissions and returns the original job, the new service cannot start returning conflicts because that feels cleaner. Modernization comes after parity is visible, or behind an explicit versioned change. Otherwise every difference becomes an argument about whether the old or new result was intended.
For an export-job candidate, a minimal request can make retry behavior concrete:
{
"request_id": "01JEXAMPLE8M4Q2",
"account_id": "A1842",
"report_type": "ledger",
"cutoff": "2026-08-01T00:00:00Z"
}
The service stores request_id under a unique constraint before starting work. Repeating the request returns the existing job and its current state. A timeout after acceptance is then recoverable: the caller repeats the same identity instead of inventing another job. The contract also needs a terminal failure state and a policy for whether an operator may retry it under the same identity.
Specify errors by caller action. “Invalid cutoff, do not retry” is useful. “Dependency unavailable, retry with bounded backoff” is useful. A generic internal error pushes every caller toward the most dangerous response, immediate blind retry. Bound request sizes and deadlines as part of the contract; otherwise the largest historical input will discover those limits in production.
Keep the first interface smaller than the old internal API. Do not expose tables through generic create, read, update, and delete endpoints. Offer operations that preserve invariants, such as request_export, cancel_pending_export, and get_export_status. A narrow command surface makes it possible to move implementation details without coordinating every caller again.
One writer prevents split-brain data
The migration needs a declared moment when the new service becomes the only writer for its records. Dual writing from application code is not a safe bridge. One write can succeed while the other times out, and the caller cannot know whether a retry will repair or duplicate the state.
Use a staged ownership transfer. First, make hidden writers visible and route them through one monolith interface. Next, introduce stable request identities and record the old behavior. Then let the new service process copied traffic without publishing results. After parity checks pass, route authoritative writes to the service while the monolith reads through a compatibility path. Remove the old writer only after the fallback window closes. This is one integrated migration sequence, not five independent projects.
If the monolith must react to a committed service change, publish an event from the same database transaction that records the change. A transactional outbox is the usual mechanism: the service commits domain state and an outbox row together, then a relay publishes the row. Consumers must still handle duplicates because a relay can publish and fail before marking the row complete.
Avoid cross-service foreign keys. They cannot enforce integrity across separate databases, and retaining one shared database keeps deployment coupled. Carry the referenced identifier, validate it where the workflow requires, and decide how to handle deletion or stale copies. Some invariants need a synchronous authority check; others can tolerate a local projection. The decision belongs in the domain contract, not in an accidental join.
Data backfill needs a watermark and a reconciliation query. “Copy the table, then switch” leaves a race between the snapshot and live writes. Capture changes after a known position, load the snapshot, apply changes in order, and compare counts plus domain totals or hashes. A matching row count cannot detect swapped values, missing relationships, or a default that changed meaning.
The cutover plan should name the old and new authorities for every phase. If an incident commander has to infer ownership from dashboards while writes continue, the plan has failed before the incident starts.
Shadow traffic must compare meaning
A shadow deployment proves more when it compares domain outcomes than when it merely compares status codes. Send a copy of eligible production requests to the new service, isolate its side effects, and record normalized outputs from both implementations. Then classify differences by field and business rule.
Normalize nondeterministic values before comparison. Timestamps, generated identifiers, unordered collections, and formatting can differ without changing behavior. Do not normalize away fields that carry meaning, such as money rounding, permission decisions, item order promised to a caller, or state-transition timing. Every normalization rule should have an owner and a reason.
Recorded production traffic catches combinations that test fixtures miss, but it also carries sensitive data and historical accidents. Define retention, access, masking, and replay controls before collecting it. In regulated environments, keeping the harness inside the customer perimeter can matter more than the convenience of a hosted test system. Supporting that environment does not confer a compliance certification; those are separate claims.
Compare side effects through a sink, not by letting the shadow service send real email, charge an account, or publish authoritative events. Replace those adapters with recorders that capture intent. The useful comparison is “the new implementation would have emitted this event with these fields,” not “the handler returned 202.”
Set promotion gates around observed behavior. Cover normal volume, peak payloads, retries, invalid input, dependency timeouts, and the scheduled jobs found during coupling analysis. Require zero unexplained differences for invariants that affect money, permissions, or durable state. For cosmetic formatting, document accepted differences instead of pretending byte equality is the goal.
CodeHero uses a parity harness against recorded production traffic when it rewrites a legacy system, and that is the part of the approach I would insist on even if another team built the replacement. Process health tells you that the new code runs. Parity evidence tells you whether it still does the job.
Rollback should change routing, not repair history
A safe rollback stops new traffic from reaching the extracted service without asking operators to merge two conflicting histories. That property must be designed before cutover. If rollback requires reverse-transforming data, restoring a shared table, or redeploying every caller, it will be too slow when the fault is ambiguous.
Keep the old read path available during the observation window, but do not keep two unconstrained writers. When the new service owns writes, feed its committed changes back to the monolith's compatibility store or make the monolith read through the service. The first option can support fast read fallback if replication lag is visible. The second keeps one truth but makes service availability part of the old path. Choose based on the outage you can tolerate.
Use a route control that changes independently of application deployment. It may sit at a gateway, in a consumer assignment, or behind a server-side configuration flag. Protect it with access control and an audit trail. Test the exact reversal under load before cutover, including in-flight requests and queued messages.
Define rollback triggers in observable terms. Rising error rate alone misses semantic corruption. Include parity differences, queue age, duplicate rate, rejected state transitions, and domain totals. Assign who can call the rollback and who diagnoses after traffic moves. An emergency meeting is not a control plane.
Schema changes must remain compatible through the rollback window. Add fields before requiring them, tolerate old and new representations while both versions run, and remove old fields later. Database rollback scripts are comforting on paper, but reversing a destructive migration after new writes arrive is often impossible. Forward-compatible schema changes keep the routing option real.
Do not call every retreat a failure. If the route switch works, evidence stays intact, and the team finds a semantic difference before customers depend on it, the extraction machinery has done its job. The failure is discovering that the only way back is an improvised data edit.
Promotion needs owners, thresholds, and a clock
A cutover is ready when named people can decide, from current evidence, whether to continue, pause, or reverse it. A calendar invitation and a dashboard do not create that ability. The team needs an operating record that connects each signal to an action and each action to one accountable role.
Write a promotion sheet before shadow traffic starts. Name the candidate version, route control, data watermark, compatibility mode, expected queue depth, allowed latency change, parity rules, and the person who can move traffic. Record the exact commands or control-panel actions for each traffic increment and reversal. If the procedure depends on one engineer remembering an undocumented flag, rehearse it until that dependency disappears.
Move traffic in increments that reveal load effects without creating a new data authority at every stage. For stateless reads, percentage routing may be enough. For stateful work, route by a stable partition such as account identifier, job type, or tenant so that one unit does not bounce between implementations. Persist the assignment. Random percentage routing for related writes can split a workflow across two owners and make the resulting fault look intermittent.
Time matters in two different ways. The new service needs enough exposure to encounter representative work, and each promotion stage needs a maximum dwell time before someone explicitly chooses what happens next. An open-ended partial rollout can become permanent architecture, complete with two dashboards, two runbooks, and no clear owner. Set the decision time according to traffic patterns, not management patience. A service with an important daily batch must see that batch before promotion; one with a quarter-end path needs recorded replay if waiting for the live event is unreasonable.
Separate infrastructure thresholds from semantic thresholds. CPU saturation, connection exhaustion, queue growth, and timeout rate tell you whether the service can carry load. State totals, transition counts, rounding outcomes, permission decisions, and intended side effects tell you whether it carries the right behavior. A release that is fast and wrong should stop sooner than one that is temporarily slow and correct.
Use a compact decision record during the rollout:
- The release owner records the traffic partition and data watermark before changing the route.
- The observer confirms infrastructure signals and parity results for that partition.
- The domain owner classifies every new semantic difference as explained, blocking, or accepted with a written reason.
- The incident owner confirms that reversal remains possible with the current schema and queued work.
- The release owner promotes, holds, or reverses, then records the evidence used.
This is not ceremony for its own sake. It prevents a familiar incident pattern: infrastructure looks healthy, the rollout advances, and an unexplained business difference is left for the next shift because nobody has authority to block promotion. The decision record makes uncertainty visible while reversal is still cheap.
Keep one clock for technical recovery and another for data recovery. Routing back may take seconds, while replaying missed events or rebuilding a projection takes longer. State both objectives. Calling the rollback complete when requests reach the monolith again hides unfinished repair work and invites the team to delete evidence too early.
Promotion ends only after ownership cleanup. Remove temporary dual-read paths, revoke obsolete database permissions, archive comparison results under the agreed retention policy, and update the service catalog and on-call route. Leaving migration privileges in place turns a controlled bridge into an unofficial permanent interface. The next extraction should begin with fewer hidden paths than this one did.
The first extraction tests the migration system
The first service is successful when the organization can repeat the method with better evidence and less coordination. Its business value matters, but its second output is a tested migration system: dependency discovery, contract capture, traffic replay, data transfer, promotion gates, and reversible routing.
Record where the plan disagreed with production. Perhaps a stored procedure owned a write nobody found in code search. Perhaps retries reused no identifier. Perhaps the peak payload came from a quarterly job. Turn each surprise into an automated query, trace, or gate for the next candidate. A retrospective without a changed control only preserves the story.
Keep the new service operationally independent after launch. Give it its own deployable artifact, health and domain signals, resource limits, on-call ownership, and a runbook for stuck work. Do not require a monolith release to change its implementation. Do not let its database become a convenient reporting schema for new consumers. Every direct reader becomes another future extraction problem.
Architecture modernization should follow observed behavior rather than transliterate old modules into new processes. A whole-codebase analysis helps because legacy behavior often crosses language boundaries, stored procedures, batch files, and client code. CodeHero reads those sources together and delivers rewrites in under 30 days, but speed does not remove the need to name one data owner and test the reversal.
Choose the candidate whose failure is containable, whose writes can have one owner, and whose callers can cross a network boundary without joining a distributed transaction. Then prove it with recorded traffic and pull the route back once before the real cutover. A team that has never exercised reversal has a rollback document, not a rollback capability.
FAQ
What is the best first service to extract from a monolith?
Choose a responsibility with one clear data owner, a narrow write surface, and failure that does not block the system's main transaction. Asynchronous jobs such as exports or document processing often make better first candidates than customer, order, or authentication domains.
Should the first microservice be the easiest module in the codebase?
Only if it is also easy to operate independently. A neat module that shares tables and transactions with the monolith is easy to move but hard to release, observe, and roll back.
Can a new service share the monolith database at first?
It can share infrastructure temporarily, but it still needs exclusive ownership of the rows it writes. Two legitimate writers create ambiguity, and keeping cross-boundary foreign keys preserves deployment coupling.
How do you find hidden database writers before extraction?
Combine repository search with database statement logs, traces, trigger inspection, scheduled-job inventories, and operator scripts. Observe a full business cycle so infrequent reconciliation and month-end work do not escape the map.
Why is dual writing unsafe during a service migration?
The first write can commit while the second times out, leaving the caller unable to tell whether retrying will repair or duplicate the operation. Prefer one writer, stable idempotency keys, and a transactional outbox for changes other components must receive.
What does data independence mean for a microservice?
The service is the sole authority that validates and commits changes to its records. Other components may keep projections or caches, but they cannot edit the authoritative state directly.
How should teams test behavior parity after an extraction?
Replay representative recorded traffic into an isolated shadow deployment and compare normalized domain outputs and intended side effects. Status codes and process health are insufficient when money, permissions, or durable state can differ.
What makes a service extraction easy to roll back?
A route or consumer switch can stop new traffic while one authoritative history remains intact. Compatible schemas, visible replication lag, and a rehearsed treatment of in-flight work keep that switch usable during an incident.
Is the strangler fig pattern enough to split a monolith safely?
It gives you a useful routing strategy, but routing alone does not settle data ownership, transaction boundaries, or retry semantics. Pair the facade with an explicit write owner, parity evidence, and a rollback design.
How long should the old implementation remain available?
Keep it through a defined observation window that covers normal traffic, peak cases, retries, and important scheduled work. Remove it when promotion gates pass and rollback no longer depends on unexplained behavior, not on an arbitrary calendar date.