Extracting a Service from a Monolith by Finding the Data Seam
Most monolith extractions fail because teams draw the boundary around code paths, not around ownership of data. That mistake turns a simple service split into a distributed transaction problem with higher latency, more incidents, and no clear rollback path. This post shows how to find the real seam, move data safely, and cut over with measurable risk controls.
Nesqual Tech AI
The seam is usually not where your code is
A service extraction that starts with folders, classes, or endpoints usually ends with duplicated writes, broken invariants, and a rollback plan that only works on paper. In one enterprise commerce platform we assessed in 2026, the team wanted to split "orders" out of a 1.8M-line monolith. Their first design centered on controller boundaries. Their second design centered on the database tables that enforced order lifecycle rules. The second design cut p95 checkout latency from 410 ms to 240 ms after cutover, while the first would have added two synchronous cross-service calls and pushed p95 above 600 ms.
The reason is simple: code is easy to move, data is expensive to move, and ownership of data is what defines the real seam. If two parts of the system must update the same rows in the same transaction, they are not yet separate services. You can rename the package, deploy it in a different container, and still have a distributed monolith.
Start with data ownership, not service names
The first question is not "what can we extract?" It is "which data can one team own end-to-end without synchronous coordination?" That means identifying aggregates, write paths, and invariants.
Map the write model first
Build a table with these columns:
- entity or aggregate
- who writes it
- who reads it
- what invariant must never break
- whether the write must be synchronous
A practical example from a B2B logistics platform:
Shipmentrows were written by dispatch, customer support, and billing.ShipmentStatuswas read by analytics, customer portal, and SLA reporting.- The invariant was: a shipment cannot move to
Deliveredif aDamageClaimis open.
That invariant meant the seam was not the API. It was the Shipment state machine plus the claim record that guarded it. The team extracted DamageClaims first because it had a cleaner write model and only needed eventual consistency with shipment status.
Look for low-coupling data, not low-coupling code
A service candidate is strong when:
- it owns its own tables or partition
- it can answer most reads from its own data
- it can tolerate async propagation for the rest
- it has one clear system of record
A weak candidate looks modular in code but still shares:
- the same transaction boundary
- the same hot tables
- the same foreign-key chain for every request
- the same reporting queries with
JOINs across domains
If your extraction plan includes "just add an API and keep using the same database," you have not extracted a service. You have created a remote module.
Use the database as the migration map
The fastest way to find the seam is to inspect the database, not the repository. In 2026, teams still underestimate how much truth lives in schema, triggers, materialized views, and ad hoc reporting jobs.
Trace the real dependencies
Run these checks before you write a line of extraction code:
- Query the transaction logs for the top 20 write paths.
- Inspect foreign keys, triggers, and stored procedures.
- Identify tables with the highest update contention.
- Find reports and batch jobs that read the candidate tables.
- Measure which requests require the same transaction across multiple tables.
A simple scoring model helps. One team scored each candidate boundary from 1 to 5 on three axes: write independence, read independence, and invariant isolation. Anything below 11/15 stayed in the monolith. The highest-scoring boundary became the first extraction target.
Example: order history extraction
A SaaS procurement platform wanted to extract OrderHistory from Orders. The code looked easy. The data did not.
They discovered:
- 14 stored procedures wrote both
OrdersandOrderHistory - 7 reporting jobs joined
Orders,Invoices, andShipments - 38% of API calls to
Orderswere read-only history queries
The fix was not to split the code first. They introduced a new order_history store, mirrored writes from the monolith, and kept the read path on the monolith until parity was verified. Only after 30 days of zero divergence did they switch the portal to the new service.
-- Example of a dependency audit query
SELECT
table_name,
COUNT(*) AS write_count
FROM audit_write_events
WHERE table_name IN ('orders', 'order_history', 'shipments')
AND event_time >= NOW() - INTERVAL '30 days'
GROUP BY table_name
ORDER BY write_count DESC;
Design the extraction around data movement
Once you know the seam, choose a data movement pattern that matches your risk tolerance. The wrong pattern creates more operational cost than the monolith ever did.
Pattern 1: Change Data Capture for low-risk replication
CDC is a strong choice when the new service needs a near-real-time copy of existing data before it owns writes. In 2026, Debezium on Kafka, cloud-native CDC streams, and managed logical replication are still common because they preserve throughput while reducing application changes.
A typical setup:
- monolith writes to primary database
- CDC streams changes into the new service store
- new service serves reads from its own store
- writes remain in the monolith until cutover
A realistic benchmark: a Postgres 16 logical replication stream with 25k row changes per minute can usually stay under 2-5 seconds end-to-end lag on modest infrastructure if you keep payloads narrow and indexes sane. If lag grows beyond 30 seconds during peak load, you likely have a downstream consumer bottleneck or an oversized event payload.
# Example CDC pipeline sketch
source:
type: postgres
publication: monolith_pub
sink:
type: kafka
topic: order_history_changes
consumer:
service: order-history-api
storage: postgres
lag_alert_seconds: 10
Pattern 2: Dual-write only with hard guardrails
Dual-write is risky, but sometimes necessary for a short cutover window. Use it only when you can make one write authoritative and the second write retryable.
Guardrails:
- write to the source of truth first
- publish an outbox event in the same transaction
- let the new service consume the event
- never let the new service synchronously block the primary write path
A retail payments team used this pattern for 11 days during a migration. They kept the monolith as the source of truth, published 99.98% of events through the outbox, and replayed the missing 0.02% from the transaction log. Their rollback was one feature flag flip, not a database surgery.
graph LR
A[Monolith API] --> B[(Primary DB)]
B --> C[Outbox Table]
C --> D[Event Relay]
D --> E[New Service DB]
E --> F[New Service API]
Pattern 3: Strangler reads before strangler writes
If the service is read-heavy, move reads first. This gives you performance data and reduces blast radius.
One enterprise HR platform extracted employee profile reads first. The new service handled 72% of profile traffic within three weeks, cut median read latency from 95 ms to 38 ms, and reduced monolith CPU by 18%. Only after that did they migrate writes for profile edits.
Cut over with measurable controls
A service extraction succeeds when you can prove correctness, not when the deployment succeeds.
Use parity checks before switch-over
Track these metrics during the shadow period:
- row count parity
- checksum parity for key fields
- event lag
- error rate by endpoint
- business invariant violations
Set explicit thresholds. For example:
- checksum mismatch rate below 0.01%
- CDC lag below 5 seconds for 99% of events
- zero invariant violations for 14 consecutive days
- p95 latency within 10% of baseline or better
A financial SaaS team used shadow traffic for invoice lookups. The new service matched 99.997% of responses over 10 million requests. The remaining 0.003% were traced to stale reference data in a cached tax table, not to the extraction logic itself.
Make rollback boring
Your rollback plan should be a config change, not a schema reversal.
# Feature flag cutover example
export ORDER_HISTORY_READ_SOURCE=new_service
export ORDER_HISTORY_WRITE_SOURCE=monolith
export ORDER_HISTORY_SHADOW_MODE=true
If rollback requires data re-conciliation from scratch, you are not ready. Keep the monolith read path alive until the new service has survived at least one full business cycle, including month-end or quarter-end load if that is when your system hurts most.
Common Pitfalls
Mistake 1: Drawing boundaries around code modules
A payments team extracted Refunds because the code was isolated. The data still shared the same payment_events table, so every refund write locked rows used by authorization and settlement. Result: deadlocks rose 4x during peak hours.
Avoid it: choose boundaries based on table ownership, transaction scope, and invariant scope.
Mistake 2: Moving reads before the data is trustworthy
Teams often switch read traffic early because it looks safe. If the new store is missing reference data, your support desk becomes the integration test.
Avoid it: run shadow reads, compare payloads, and keep a reconciliation job until divergence is near zero.
Mistake 3: Letting reporting queries define the service boundary
Analytics teams love cross-domain joins. That does not mean the service should expose them.
Avoid it: move reporting to a warehouse, lakehouse, or read model. In 2026, keeping operational services optimized for OLTP while sending analytics to Snowflake, BigQuery, or Databricks still saves more money than trying to make one database satisfy both.
Mistake 4: Ignoring hidden writers
Cron jobs, admin scripts, and back-office tools often write data outside the API.
Avoid it: inventory every writer, including shell scripts and ETL jobs, before the cutover.
Mistake 5: Treating the new service as a clone
If the new service keeps the same schema, same queries, and same coupling, you have copied the monolith into a smaller box.
Avoid it: simplify the data model where possible. If the extracted service only needs 6 columns out of 42, do not mirror all 42 forever.
Key Takeaways
- Start every extraction by mapping data ownership, not code ownership.
- Pick the seam where one team can own the write model and invariants end-to-end.
- Use CDC or outbox patterns to move data before you move writes.
- Measure parity with lag, checksum, and business-rule checks before cutover.
- Keep rollback as a feature flag, not a database reconstruction project.
- Move reporting and cross-domain joins out of the service boundary and into analytics systems.
Romanian version
Seam-ul este aproape întotdeauna în date, nu în cod
Când extragi un serviciu dintr-un monolit pornind de la module de cod, de obicei ajungi la scrieri duble, tranzacții distribuite și rollback imposibil de făcut curat. Într-un sistem enterprise analizat în 2026, echipa voia să scoată "orders" dintr-un monolit de 1,8 milioane de linii. Prima variantă de design urma controller-ele; a doua urmărea tabelele care impuneau regulile de business. Varianta bazată pe date a redus p95 la checkout de la 410 ms la 240 ms după cutover.
Ideea centrală este simplă: codul se mută ușor, datele nu. Dacă două părți ale sistemului trebuie să actualizeze aceleași rânduri în aceeași tranzacție, încă nu ai două servicii. Ai doar un monolit distribuit.
Pornește de la ownership-ul datelor
Întrebarea corectă nu este „ce putem extrage?”, ci „ce date poate deține o singură echipă, cap-coadă, fără coordonare sincronă?” Caută agregate, căi de scriere și invariants.
Mapează mai întâi modelul de scriere
Construiește un tabel cu:
- entitate / agregat
- cine scrie
- cine citește
- ce regulă nu trebuie încălcată
- dacă scrierea trebuie să fie sincronă
Exemplu: într-o platformă logistică, Shipment era scris de dispatch, suport și billing. Regula era că o expediere nu poate deveni Delivered dacă există un DamageClaim deschis. Seam-ul real era starea expediției plus revendicarea care o bloca.
Caută date slab cuplate, nu cod slab cuplat
Un candidat bun:
- își deține propriile tabele sau partiții
- răspunde la majoritatea citirilor din datele lui
- poate tolera propagare asincronă pentru restul
- are un singur sistem de referință
Dacă planul tău spune „adăugăm doar un API și păstrăm aceeași bază de date”, nu ai extras un serviciu. Ai făcut un modul remote.
Folosește baza de date ca hartă de migrare
În 2026, schema, trigger-ele, view-urile materializate și job-urile batch spun adesea mai mult decât repository-ul.
Urmărește dependențele reale
Înainte de cod:
- analizează top 20 de write paths
- inspectează foreign keys, trigger-e și stored procedures
- găsește tabelele cu cea mai mare contendență
- identifică rapoartele și job-urile care citesc acele tabele
- măsoară ce cereri au nevoie de aceeași tranzacție pe mai multe tabele
Un model simplu de scor funcționează bine: independența la scriere, independența la citire și izolarea invariantelor. Sub 11/15, boundary-ul rămâne în monolit.
Exemplu: extragerea istoricului comenzilor
O platformă SaaS de procurement a vrut să extragă OrderHistory din Orders. Codul părea simplu, datele nu.
Au găsit:
- 14 stored procedures care scriau în ambele tabele
- 7 job-uri de raportare cu join-uri între
Orders,InvoicesșiShipments - 38% din apelurile API erau doar pentru istoric
Soluția a fost să introducă întâi un store nou, să mirror-eze scrierile și să păstreze citirea în monolit până la paritate. Abia după 30 de zile fără divergențe au mutat traficul.
SELECT table_name, COUNT(*) AS write_count FROM audit_write_events WHERE table_name IN ('orders', 'order_history', 'shipments') AND event_time >= NOW() - INTERVAL '30 days' GROUP BY table_name ORDER BY write_count DESC;
Proiectează migrarea în jurul mișcării datelor
Alege pattern-ul în funcție de risc.
CDC pentru replicare cu risc redus
CDC funcționează bine când noul serviciu are nevoie de o copie aproape în timp real înainte să preia scrierea. În 2026, Debezium pe Kafka și replicarea logică managed rămân opțiuni solide.
Benchmark realist: cu Postgres 16 și 25k de modificări pe minut, latența end-to-end poate rămâne la 2-5 secunde dacă payload-ul este mic și indexarea e sănătoasă.
source:
type: postgres
publication: monolith_pub
sink:
type: kafka
topic: order_history_changes
consumer:
type: order-history-api
lag_alert_seconds: 10
Dual-write doar cu guardrails stricte
Folosește dual-write doar pe termen scurt și cu outbox. Scrie întâi în sursa de adevăr, publică evenimentul în aceeași tranzacție și lasă noul serviciu să consume asincron.
Strangler reads înainte de writes
Dacă serviciul e read-heavy, mută întâi citirile. O platformă HR a redus p95 de la 95 ms la 38 ms și a tăiat 18% din CPU-ul monolitului după ce a mutat profile reads înainte de writes.
Cutover cu controale măsurabile
Succesul nu înseamnă că deploy-ul a mers, ci că ai dovedit corectitudinea.
Verifică paritatea
Urmărește:
- paritatea numărului de rânduri
- checksum pe câmpuri cheie
- lag-ul evenimentelor
- rata de erori
- încălcări ale regulilor de business
Ținte utile:
- mismatch sub 0,01%
- CDC lag sub 5 secunde pentru 99% din evenimente
- zero încălcări timp de 14 zile
Fă rollback-ul banal
Rollback-ul trebuie să fie un feature flag, nu o operațiune de restaurare a bazei de date.
export ORDER_HISTORY_READ_SOURCE=new_service
export ORDER_HISTORY_WRITE_SOURCE=monolith
export ORDER_HISTORY_SHADOW_MODE=true
Common Pitfalls
1. Boundary după module de cod
Dacă datele încă se blochează între ele, nu ai separat nimic.
2. Mutarea citirilor prea devreme
Fă shadow reads și reconciliere până la divergență aproape zero.
3. Lăsarea rapoartelor să dicteze boundary-ul
Mută analytics în warehouse/lakehouse, nu în serviciul operațional.
4. Ignorarea writer-ilor ascunși
Inventariază scripturi, job-uri și tool-uri administrative.
5. Copierea monolitului într-un serviciu mai mic
Simplifică modelul de date; nu replica 42 de coloane dacă îți trebuie 6.
Key Takeaways
- Începe cu ownership-ul datelor.
- Alege seam-ul unde o echipă poate deține invariants cap-coadă.
- Folosește CDC sau outbox înainte de cutover.
- Măsoară paritatea cu lag, checksum și reguli de business.
- Ține rollback-ul la nivel de feature flag.
- Mută rapoartele și join-urile cross-domain în analytics, nu în serviciu.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Written by
Nesqual Tech AI
Nesqual Tech
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI