Microservices migration is one of the most requested — and most mishandled — engineering initiatives we see. Teams underestimate the organizational complexity, overestimate the immediate performance benefits, and often end up with a distributed monolith that's worse than what they started with. This guide shares what actually works.
Step 1 — Why shouldn't you start with decomposition?
The first instinct is to split the monolith into services. Resist it. Start by improving observability inside the monolith — structured logging, distributed tracing, dependency graphs. You can't decompose what you don't understand. Spend 4–6 weeks mapping the actual call graph, not the org chart.
Step 2 — What is the strangler fig pattern?
The strangler fig is the safest migration pattern: build new capabilities as services, redirect traffic to them, and gradually "strangle" the monolith. Never do a big-bang rewrite — the risk of total failure is too high, and you'll be maintaining two codebases in a broken state for months.
- Put a routing layer in front of the monolith and change nothing else. Every later step depends on this seam existing, and adding it while traffic still goes one place is the only time it is risk-free.
- Pick the first capability by write-coupling, not by how annoying it is: something that owns its data and is read far more than it is written. Notifications and search are common first extractions; anything sharing a table with billing is not.
- Dual-run it. Send reads to both the new service and the monolith, serve the monolith's response, and log the differences. Those diffs are the real test suite — they surface the undocumented behaviour no one remembered.
- Flip reads to the new service one endpoint at a time behind a flag, with a rollback that takes effect in seconds rather than a redeploy.
- Move writes last, then delete the monolith's copy of the code. An extraction that leaves the old path in place is not finished — it is two implementations to keep in sync, which is strictly worse than the monolith you started with.
# Each migrated capability is one line; everything unmatched still
# falls through to the monolith. Nothing here is a big-bang cutover.
location /api/notifications/ { proxy_pass http://notifications-svc; } # extracted
location /api/search/ { proxy_pass http://search-svc; } # extracted
location /api/billing/ { proxy_pass http://monolith; } # still coupled
location / { proxy_pass http://monolith; }
# If this file has not changed in two months, the migration has
# stalled — regardless of how many services exist in the repo.Step 3 — Why is database decomposition the hard part?
Service decomposition is easy. Database decomposition is where projects stall. You cannot have two services sharing a database — that creates invisible coupling. Use the Database-per-Service pattern, and accept the eventual consistency trade-off for non-critical data. For critical financial or inventory data, use the Saga pattern with compensating transactions.
Step 4 — Do you need an API gateway or a service mesh?
- API Gateway handles cross-cutting concerns: auth, rate limiting, request routing, SSL termination.
- Service Mesh (Istio or Linkerd) handles service-to-service security, circuit breaking, and observability.
- Don't implement both on day 1 — start with an API gateway, add service mesh when you have 10+ services.
Common Mistakes to Avoid
- Decomposing by technical layer instead of business capability.
- Not investing in a shared auth/identity service early.
- Ignoring local development complexity — invest in docker-compose or Tilt from week 1.
- Creating too many tiny services (nanoservice anti-pattern).
