All contributions
Engineeringmicroservicesarchitecturedevops

From monolith to microservices: a practical guide for growing companies

Technical guide for migrating from a monolithic architecture to microservices. Patterns, anti-patterns, domain decisions, and lessons learned in production.

Numoru EngineeringPublished on April 5, 202615 min read
Share
6 wk → daily
Release cycle after strangler migration
Client case at end of article
3 squads
From 1 blocked team to 3 autonomous
Team-scale unlock
$45-180k
Migration engagement ticket
By org size
9-18 mo
Typical program duration
Strangler fig

When NOT to migrate to microservices

Before discussing how, let's discuss when you shouldn't:

  • Your team has fewer than 10 engineers: the operational overhead of microservices (networking, observability, independent deploys) outweighs the benefits.
  • Your monolith works fine: "microservices" is not a synonym for "modern." A well-structured monolith is easier to maintain.
  • You don't have differentiated scaling problems: if every part of the system needs the same capacity, microservices add complexity without benefit.

The right reason to migrate is: you need to scale, deploy or evolve parts of the system independently, and your monolith has become an organizational bottleneck.

Signals that it's time to migrate

  1. Deploys are risky: a change to the payments module forces a redeploy of everything, including the product catalog.
  2. Teams block each other: the checkout team waits for the inventory team to finish a feature because they share the same codebase.
  3. Disproportionate scale: your search API handles 100× more traffic than your reporting module, but they run on the same servers.
  4. Long release cycles: releases happen monthly because coordinating every change in one deploy is complex.

Strategy: Strangler Fig, not Big Bang

Never rewrite everything from scratch. The Strangler Fig strategy works like this:

  1. Identify the most independent domain with the largest separation benefit
  2. Extract it as a service behind the same API
  3. Route traffic gradually (10% → 50% → 100%)
  4. Repeat with the next domain

Step 1: Bounded Contexts

Before touching code, map the bounded contexts with the team:

Current monolith:
├── Users & Auth        → Context: Identity
├── Product Catalog     → Context: Catalog
├── Shopping Cart       → Context: Cart
├── Checkout & Payments → Context: Payments
├── Order Fulfillment   → Context: Fulfillment
├── Notifications       → Context: Notifications
└── Reporting           → Context: Analytics

Each context has its own data, its own rules, and an owning team. If two contexts share tables in the database, you must resolve that dependency before separating them.

Step 2: The first extraction

Pick the service that:

  • Has the lowest coupling with the rest of the system
  • Has the highest benefit from independent scaling
  • The owning team is motivated and available

Most of the time, Notifications or Analytics are good initial candidates: few inbound dependencies, not on the critical transaction path, and the team can fail without impacting checkout.

Step 3: The communication pattern

For inter-service communication:

PatternUse it whenAvoid when
Synchronous HTTP/RESTSimple queries, low latency requiredOperations that may fail and need retry
Asynchronous events (Kafka/NATS)Notifications, analytics, eventual consistencyWhen you need an immediate response
gRPCHigh-volume internal communicationPublic APIs, browser compatibility

Rule of thumb: if service A needs to wait for B's response to continue, use synchronous. If A only needs B to eventually know, use events.

Anti-patterns we've seen

The distributed monolith

Service A → Service B → Service C → Service D → Shared database

If all your microservices share a database and call each other in a synchronous chain, you don't have microservices — you have a distributed monolith with extra latency and more failure points.

The nano-service

One service per database entity. UserService, AddressService, PhoneService… If a single domain change requires modifying 5 services, the granularity is excessive.

CRUD-as-a-service

If your "microservice" is a wrapper around a table with GET/POST/PUT/DELETE endpoints and zero business logic, it's not a service — it's an unnecessary data-access layer.

What you need from day 1

Don't extract the first service without these in place:

Observability

  • Distributed tracing (Jaeger/Tempo): every request must be traceable across services
  • Centralized logs (ELK/Loki): one place to look for errors
  • Metrics (Prometheus/Grafana): p50/p95/p99 latency per service, error rate, saturation

Per-service CI/CD

Each service has its own pipeline:

  • Independent build
  • Independent tests
  • Independent deploy
  • Independent rollback

If a Payments deploy fails, it should never affect Catalog.

Service mesh or API gateway

For routing, rate limiting, circuit breaking and mTLS between services. Options:

  • Istio/Linkerd for Kubernetes
  • Kong/Traefik as API gateway
  • NGINX with manual config (viable for < 10 services)

Real case: from 6-week releases to daily deploys

A marketplace we worked with had:

  • 4-year-old Python/Django monolith
  • 15 engineers across 3 teams
  • 6-week releases with a 4-hour deploy window
  • 3 major incidents in the previous 6 months from failed deploys

What we did

  1. Month 1: mapped bounded contexts, identified 6 domains, prioritized Notifications and Analytics as first extractions
  2. Months 2-3: extracted Notifications as a Go service with NATS for events. Stood up observability (Grafana + Jaeger + Loki)
  3. Months 3-4: extracted Payments as a Go service with its own PostgreSQL. Implemented the saga pattern for coordination with the monolith
  4. Months 4-5: independent CI/CD per service in GitHub Actions. Feature flags with LaunchDarkly
  5. Months 5-6: extracted Catalog. By now the team had mastered the pattern

Results

MetricBeforeAfter
Deploy frequencyEvery 6 weeksDaily
Deploy window4 hours< 5 minutes
Incidents per deploy~0.5< 0.05
Onboarding time3 weeks1 week (per service)
Team scale15 → blocked15 → 3 autonomous squads

Business & commercial impact

Business & commercial impact

How Numoru sells a migration

The productized offer is an Architecture Fitness Assessment ($9,500, 3 weeks) that produces a yes/no on whether microservices make sense and, if yes, the prioritized seam list. From there, a phased retainer drives the strangler program — typically 9-18 months — with clear exit criteria instead of an open-ended rebuild.

Migration engagement ticket by buyer (Numoru, USD)

Scale-up SaaS (30-100 eng)
Unblock deploy cycle + onboard new squads.
$80,000 – 180,000
12-18 mo + $6k/mo MSP
E-commerce high-traffic
Split checkout + catalog + search.
$55,000 – 140,000
9-12 mo
Fintech post-IPO-prep
Compliance boundary + blast-radius reduction.
$120,000 – 280,000
18 mo + audit
Corporate IT modernization
Break legacy on-prem monolith for cloud + team autonomy.
$180,000 – 450,000
RFP
Assessment only
CTO wants an outside opinion before committing.
$9,500
3 wk
Public case studySoftware consulting · Global · 2015-2024

Martin Fowler — Strangler Fig / MonolithFirst

Challenge
Codify why teams burn themselves with big-bang migrations and how to avoid it.
Solution
Fowler's blog and ThoughtWorks case studies popularized the strangler fig pattern. The recommendation stands: most microservice wins come from team-autonomy, not code quality.
Results
Public case migrations
100+
Catalogued in ThoughtWorks library
Cycle-time improvement
3-10×
Typical multi-quarter
Incident blast radius
-60 to -90%
Post-decomposition
Public case studyIndustry research · Global · 2024

DORA / State of DevOps

Challenge
Measure delivery performance against architecture choices and org design.
Solution
Google's DORA team publishes the annual State of DevOps report correlating architecture, team structure, and deploy cadence.
Results
Elite performers
18%
Deploy multiple times per day
Low performers
31%
Release less than monthly
Team independence predictor
Strong
Of elite status

30-engineer SaaS strangler migration (12 months)

Payback: 6 months
Assumptions
Team size30 eng
Current release cycle6 weeks
Post-migration releaseDaily
Revenue at risk from slow cycle$1.2M / yr
Infra overhead added$40,000 / yr
Migration engagement$140,000
MSP ongoing$6,000 / mo
Migration (one-time)−$140,000
MSP (12 mo × $6k)−$72,000
Infra overhead−$40,000
Revenue velocity captured+$780,000
Hiring runway extended (slower ramp needed)+$240,000
Net year-1 contribution+$768,000
Assessment
$9,500one-time
3 weeks. Yes/no + plan.
  • DDD boundary review
  • Deploy-graph mapping
  • Seam prioritization
  • Exit criteria + scorecard
Strangler retainer
$80,000 – 220,000engagement
9-18 mo program with checkpoints.
  • Seam-by-seam extraction
  • Observability-first setup
  • Platform team enablement
  • Quarterly exec review
  • Rollback plans per phase
MSP
$4,000 – 12,000/ mo
Keep the platform healthy post-migration.
  • CI/CD + infra hardening
  • Cost review + right-sizing
  • Quarterly architecture audit
  • On-call for incidents

Conclusion

Migrating to microservices is not a technical project — it's an organizational project that uses technical techniques. If your team structure doesn't change, your services will end up replicating the monolith's dependencies (Conway's Law).

Start with the most independent domain, invest in observability before extracting anything, and remember: the goal is not to have microservices — it's to have teams that can deliver value independently.

Want results like these for your company?

Start a conversation
Share