// Stay in touch?
Products
Encore CloudEncore Cloud
Encore.tsEncore.ts
Encore.goEncore.go
PricingPricing
Book a DemoBook a Demo
Use Cases
AI-Powered DevelopmentAI-Powered Development
Event-Driven SystemsEvent-Driven Systems
Distributed SystemsDistributed Systems
Case StudiesCase Studies
ShowcaseShowcase
Resources
DocsDocs
InstallInstall
Example AppsExample Apps
Demo videoDemo video
ArticlesArticles
GitHub ReleasesGitHub Releases
Systems Operational
Company
About UsAbout Us
Swag ShopSwag Shop
ContactContact
JobsJobs
PressPress
SecuritySecurity
TermsTerms
Privacy PolicyPrivacy Policy
Data Processing AgreementData Processing Agreement
Enterprise SLAEnterprise SLA
Encore
© 2026 EncoreAll rights reserved
© 2026 Encore All Rights Reserved
GitHubDiscordYouTube

When to Break a Monolith into Microservices

The signals that it's time, and a strangler-fig playbook for extracting the first service without a rewrite

07/20/26
11 Min Read
Ivan Cernja
07/20/26

When to Break a Monolith into Microservices

The signals that it's time, and a strangler-fig playbook for extracting the first service without a rewrite

Ivan Cernja
11 Min Read

When Amazon Prime Video published its 2023 write-up about folding a monitoring tool back into one application, most of the attention went to the 90 percent cost drop. The detail that matters more for a migration is the direction they moved, from many small services toward fewer larger ones, because it shows that splitting is a decision you can get wrong in both directions. This is a playbook for the other direction, when you already have a monolith and a specific part of it has started to hurt.

Whether you should split at all is a separate question, and the monolith vs microservices guide covers that decision along with the modular monolith middle ground you should exhaust first. This article assumes you have decided a split is worth it and focuses on doing it without a rewrite: reading the signals, extracting the first service with the strangler-fig pattern, untangling the shared database, and avoiding the two mistakes that sink most migrations.

The strangler-fig extraction flow: a gateway sits in front of a monolith, one capability is carved into a new service, and the gateway routes that capability's traffic to the new service while everything else still hits the monolith.

Client trafficone entry point
routing layer
Gateway / proxy
during migration
still owns most routes
Monolith
first extraction
New service
data
shared for now
Monolith database
laterOPTIONAL
Service database
Route one capability to a new service, keep the rest in the monolith, split the data last.

The signals it's time

A migration should follow a named cost, the same test that governs whether to split at all. The difference at this stage is that the cost is concrete enough to point at, and you can usually name the exact part of the system it lives in. The signals that a specific capability has earned its own service:

  • One capability scales on a different curve. Media transcoding, search indexing, or a report generator spikes CPU or memory in a pattern the rest of the app never does, and scaling the whole monolith to absorb those spikes wastes capacity on everything else.
  • Teams are blocking each other on one deploy pipeline. Two teams touch unrelated parts of the codebase but share a release, so one team's failing test or held rollback stalls the other. When this happens around a clear boundary, that boundary is the seam to cut along.
  • A component needs isolation. A part of the system handles payment data under PCI scope, or has to stay up when the rest is being redeployed, and mixing it into the monolith spreads that requirement across the whole codebase.
  • A capability has a different runtime need. It wants a different language, a GPU, or a data store the monolith doesn't use, and bolting that onto the main app is more awkward than running it apart.

If you cannot map the pain to one of these, the honest move is to tighten the monolith's internal boundaries instead and revisit later. Splitting to relieve a cost you can't name gives you the operational bill of microservices with nothing on the other side of the ledger.

The strangler-fig approach

The name comes from a fig that grows around a host tree, gradually taking over until the original is gone. Applied to a migration, you grow new services around the monolith and let them take over its work one capability at a time, so the monolith is never rewritten and never stops serving traffic. Each extraction is small enough to ship, verify, and roll back on its own.

The mechanics rest on a routing layer in front of the monolith, usually an API gateway or a reverse proxy. Every request enters through it, and by default it forwards to the monolith. When a new service is ready to own a capability, you change the routing so the paths for that capability go to the service and everything else keeps hitting the monolith. Clients see one endpoint the whole time and never learn that a route moved.

A single extraction runs through a few steps:

  1. Put the gateway in front, forwarding everything to the monolith. This changes nothing for users but gives you the switch you'll flip later, and lets you verify the routing layer under real traffic before it carries any weight.
  2. Build the new service alongside the monolith for one capability, wired to the data it needs (see the shared-database section below for how).
  3. Shift read traffic first. Route reads for that capability to the new service while writes still go to the monolith, or mirror traffic to both and compare responses. This surfaces behavior gaps while the monolith is still the source of truth.
  4. Shift writes once reads are proven, so the new service now owns the capability end to end.
  5. Delete the old code path from the monolith. Skipping this leaves two implementations that drift apart, which is worse than either one alone.

Then you repeat for the next capability, and you stop whenever the remaining monolith isn't causing pain. A migration doesn't have to end at zero monolith; most stop with a smaller core plus a handful of services around the parts that needed to move.

Picking and carving out the first service

The first extraction sets the pattern the rest of the migration copies, so pick a capability that will succeed rather than the one that hurts most. You want a boundary that is easy to reason about and cheap to reverse, which means favoring a capability that reads and writes a narrow, self-contained slice of data and rarely needs synchronous access to the rest of the system.

Good first candidates share a shape:

CandidateWhy it's a good first cut
Notifications (email, SMS, push)Consumes events, owns its own templates and delivery logs, almost no synchronous reads from the core.
Search / indexingDifferent scaling and its own datastore already, reads the core's data through a feed rather than live joins.
Media or PDF processingCPU-heavy on a separate curve, takes a job and returns a result, minimal shared state.
Payments / billingClear business boundary, benefits from isolation, well-defined interface.

Avoid making the first cut a capability that sits in the middle of every request path, like authentication or a core domain object that half the codebase reads. Those are entangled enough that a first extraction turns into a research project, and a stalled first attempt tends to end the whole migration.

Carving the capability out is mostly a boundary exercise before it is a networking one. Find every place the rest of the monolith calls into the capability and route those calls through a single interface, then find every place the capability reaches into the rest and do the same. Once both directions go through a narrow interface inside the monolith, moving the capability across a network boundary becomes a change of transport rather than a change of design. A modular monolith gives you these seams for free, which is why tightening the monolith first makes the eventual split so much cheaper.

Handling the shared database

The shared database is where most migrations stall, because a monolith usually has one schema with foreign keys and joins reaching across what will become service boundaries. The rule that keeps you out of trouble is one owner per table: exactly one service writes a given table, and everyone else reaches that data through the owner's API or through events, never with a direct connection.

Do the split in two stages rather than one. First extract the service but leave its tables in the monolith database, and have the service read and write that data by calling the monolith's API. The service is running and owns its logic, while the data hasn't moved, so you can prove the boundary is right before you touch the schema. Second, once the service is stable, move the tables it owns into its own database and replace any cross-boundary joins with API calls or events.

Cross-boundary joins are the specific thing to hunt for, because a query that joins the service's tables to the monolith's won't survive the data split. Each one becomes either an API call to fetch the related data or a local copy the service keeps up to date from events the owner publishes. When a service needs a slice of another service's data on its own read path, having the owner publish changes to a Pub/Sub topic and letting the consumer maintain a local read model is the standard fix, since it removes the synchronous dependency that a live join would create.

The two mistakes that sink migrations

The big-bang rewrite. The tempting version of a migration is to freeze the monolith, build the whole microservices system beside it, and cut over once it's done. It fails for reliable reasons: feature work stops for months while the business keeps needing features, the new system has to reproduce behavior in the old one that nobody fully remembers, and requirements drift the entire time so the target moves as you build. Strangler-fig extraction avoids all three, because the product keeps shipping, each step is small enough to understand, and you're always migrating against the current system rather than a frozen snapshot of it.

Extracting by layer instead of by capability. The other common failure is splitting along technical layers, so that one service handles data access, another the business logic above it, and another the API on top. It looks tidy on an architecture diagram and produces the worst of both worlds, because every request now hops across all the layer-services in sequence, so you've added network calls and separate deploys while the services stay so coupled they must change together. Splitting by business capability instead, where one service owns billing or search end to end including its data, gives each service a boundary that matches how the product changes and lets it deploy and scale on its own.

Where Encore fits

Most of what makes an extraction expensive is the infrastructure the new service needs on day one: its own database, the queues it consumes, the secrets it reads, and the service discovery, tracing, and deploy pipeline that let it talk to what's left of the monolith. With Encore that infrastructure is declared as typed objects in the service's own code, so standing up the first service is closer to writing a module than to provisioning a new system.

// notifications/notifications.ts import { api } from "encore.dev/api"; import { Topic } from "encore.dev/pubsub"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { users } from "~encore/clients"; // The extracted service declares its own database... const db = new SQLDatabase("notifications", { migrations: "./migrations" }); // ...and subscribes to events instead of joining the monolith's tables. export const orderPlaced = new Topic<OrderPlaced>("order-placed", { deliveryGuarantee: "at-least-once", }); export const send = api( { method: "POST", path: "/notify" }, async (p: NotifyParams): Promise<void> => { // A type-checked call to another service, no HTTP client or service URL. const user = await users.get({ id: p.userId }); await deliver(user.email, p.template); }, );

The parts of the strangler-fig playbook that usually take the most setup come with the framework. Services call each other as type-checked functions through the ~encore/clients import, so the boundary between the extracted service and the rest is checked at compile time instead of discovered in production. Each service gets its own Postgres database provisioned into your own AWS or GCP account, which fits the one-owner-per-table rule directly. Pub/Sub topics with delivery guarantees give you the event-driven read models that replace cross-boundary joins, and distributed tracing spans the monolith-plus-services shape you have mid-migration, so a request that hops from the old code into a new service is one trace rather than two disconnected logs. Because the same codebase can hold several services, you can extract a capability into its own service while it still lives beside the monolith code, which is close to what the strangler-fig pattern asks for. When you'd rather run the result on your own platform, encore build docker produces a standard image.

Related reading

  • Monolith vs Microservices: How to Choose in 2026
  • Microservices vs Modular Monolith
  • Service-to-Service Communication
  • API Gateway for Microservices
  • Message Queues vs Pub/Sub
  • Microservices Best Practices

Frequently asked questions

When should you break a monolith into microservices?

Break out a service when you can point at a concrete cost the monolith is imposing, such as one part of the system needing to scale on its own, teams blocking each other on a shared deploy pipeline, or a component that needs isolation for compliance or reliability. If nothing in the monolith is hurting yet, splitting adds network calls and separate deploys without buying anything back.

What is the strangler-fig pattern?

The strangler-fig pattern extracts a monolith into services one piece at a time behind a routing layer. You stand up a new service for a single capability, point a proxy or gateway at it for the routes it now owns, and leave everything else in the monolith. Over successive extractions the new services take over more traffic while the old code shrinks, until the monolith is either gone or reduced to a small core.

How do you choose the first service to extract from a monolith?

Pick a capability with a clear business boundary, few dependencies on the rest of the code, and a real reason to be separate, such as different scaling or a team that owns it. Notifications, search indexing, PDF or media processing, and payments are common first candidates because they read or write a narrow slice of data and rarely need synchronous access to the rest of the system.

How do you handle the shared database when extracting a service?

Keep the tables in the monolith database at first and have the new service reach them through the monolith's API rather than connecting directly, which preserves one owner per table. Once the service is stable, move the tables it owns into its own database and replace cross-boundary joins with API calls or events. Splitting the schema before the service works is the step that most often forces a rollback.

Why do big-bang microservices rewrites fail?

A big-bang rewrite stops feature work for months, ships nothing until everything is done, and asks the team to reproduce behavior in the old system that nobody fully remembers. Requirements drift while the rewrite is in flight, so the new system chases a moving target. Incremental extraction keeps the product shipping the whole time and lets you stop once the painful parts are out.

Should you split a monolith by layer or by capability?

Split by capability, not by technical layer. Carving out a service for a business capability like billing or search gives it ownership of its own data and a boundary that maps to how the product changes. Splitting by layer, such as a separate service for all data access or all business logic, produces services that must talk to each other on every request and gives you distributed calls with none of the independence.

Contents
The signals it's time
The strangler-fig approach
Picking and carving out the first service
Handling the shared database
The two mistakes that sink migrations
Where Encore fits
Related reading
Encore

The backend platform for humans and agents

Build backends in Go or TypeScript and run them on your own AWS or GCP, with the guardrails your team and AI agents need to ship safely.

Get started

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.