Architecture
When to migrate from a monolith to microservices (and when not to)
Stay monolithic until you have 20+ engineers and hit deploy conflicts twice a week. Microservices cost $500-$5,000/month in infrastructure vs. $50-$200 for a monolith, and teams spend 20-30% of capacity on operations. Use the strangler fig pattern to extract one service at a time when specific pain signals appear.
Most startups that adopt microservices too early regret it. Most enterprises that stick with monoliths too long regret it. The question isn't which is better. It's when to switch.
This article covers the real tradeoffs between monolithic and microservices architectures, the signals that tell you it's time to migrate, the signals that tell you it's not, and the step-by-step approach that avoids the most common (and most expensive) mistakes.
What a monolith is, and why it works
A monolith is a single deployable unit. One codebase, one database, one build pipeline, one deployment. Your authentication module, your billing system, your notification service, your API layer; they all live in the same repository and deploy together.
This sounds limiting, but for most teams it's the opposite. A monolith gives you speed. You refactor across module boundaries in a single pull request. You run one test suite. You deploy one artifact. You debug by reading one set of logs. There's no network latency between services because there are no network calls; it's all in-process function calls.
Every successful tech company started as a monolith. Shopify runs a monolithic Rails app serving billions of dollars in transactions. Stack Overflow serves 100 million monthly visitors from a monolith. These aren't companies that couldn't figure out microservices. They're companies that understood the monolith was the right tool for their problem.
If you have fewer than 20 engineers working on the same codebase, a monolith is almost certainly the right choice. Full stop.
What microservices are, and what they cost you
A microservices architecture splits your application into independent services. Each service owns its own database, deploys independently, and communicates with other services through APIs (typically REST or gRPC) or message queues.
The promise: teams can deploy independently, scale individual components, and use different technologies where they make sense. The billing service can scale to handle month-end invoice processing without scaling the entire application. The search service can use Elasticsearch while the rest of the app runs on PostgreSQL.
The cost: you're now running a distributed system. Distributed systems are harder to build, harder to test, and harder to debug. Here's what you're signing up for:
- Network failures between services. In a monolith, a function call either works or throws an exception. In microservices, a request can time out, return a partial response, or silently fail. You need retry logic, circuit breakers, and fallback strategies for every inter-service call.
- Distributed transactions. When a single operation spans two services (charge the customer, then update inventory), you need sagas or eventual consistency patterns. These are complex to build and painful to debug.
- Observability overhead. A single request might touch 6 services. Without distributed tracing (Jaeger, Zipkin, or Datadog APM), debugging a failed request means searching through 6 separate log streams hoping to correlate timestamps.
- Deployment complexity. You need container orchestration (Kubernetes or ECS), service discovery, API gateways, and CI/CD pipelines for each service. One pipeline becomes 15.
- Data consistency challenges. Each service owns its database. Joining data across services means API calls, not SQL joins. Reporting becomes an engineering project, not a query.
None of these problems are unsolvable. But each one adds engineering hours, infrastructure cost, and operational complexity that a monolith doesn't carry.
The monolith is fine until it isn't
Four specific signals tell you your monolith has outgrown itself:
1. Deploy frequency conflicts
Team A needs to ship a billing fix today. Team B's half-finished feature is in the same deploy pipeline, and it breaks staging. Team A waits. Team B rushes to fix their code. Both ship later than planned. When this happens once a month, it's a minor annoyance. When it happens twice a week, you're losing days of engineering time to coordination overhead.
2. Teams stepping on each other
Merge conflicts in shared modules. Two teams changing the same database schema in the same sprint. Code reviews that require context from three different feature areas. These are signs that your codebase has grown beyond what a single team can own. When engineers spend more time coordinating than coding, the monolith is slowing you down.
3. One module's bug takes down everything
A memory leak in the image processing code crashes the entire application, including checkout. A slow database query in the reporting module degrades API response times for all users. In a monolith, modules share resources. A failure in one component cascades to every other component. If your most critical revenue path (checkout, payments, core API) is getting taken down by failures in less critical modules, that's a clear signal.
4. Scaling one feature means scaling the whole app
Your video transcoding module needs 8x the CPU of your API layer. But they deploy as one unit, so you're running 8x CPU for your entire application to satisfy one component's needs. Your infrastructure bill is 5-10x higher than it needs to be because you can't scale components independently.
If you're experiencing two or more of these signals consistently, it's time to start thinking about extraction. Not a full rewrite. Extraction.
Signs you're NOT ready for microservices
Before you start planning a migration, check these five conditions. If any of them apply, stay with your monolith.
- You have fewer than 5 engineers. Microservices solve team coordination problems. A team of 3 doesn't have coordination problems; it has a Slack channel and a whiteboard. The overhead of running distributed infrastructure will consume 30-40% of your small team's capacity.
- You serve fewer than 10,000 users. At low traffic, a single server handles everything comfortably. Microservices add latency (network hops between services) and complexity without providing meaningful scaling benefits at this volume.
- You have no DevOps experience on the team. Microservices require container orchestration, service mesh configuration, distributed logging, and automated deployment pipelines. Without someone who has operated these systems in production, you'll spend months building infrastructure instead of features.
- You have no monitoring or observability in place. If you can't trace a request through your monolith today, you definitely can't trace it across 10 services tomorrow. Set up structured logging, error tracking (Sentry), and application performance monitoring (Datadog, New Relic) before you split anything.
- You haven't defined clear module boundaries in your monolith. If your code is a tangled web where the billing module directly reads from the user session table and the notification system writes to the order database, extracting a service will be a nightmare. Clean up the boundaries first.
The migration path: the strangler fig pattern
The biggest mistake teams make is attempting a Big Bang rewrite. They spend 6-12 months rebuilding the entire system as microservices while the monolith keeps running in production. By the time the rewrite is "done," the monolith has 6-12 months of new features the rewrite doesn't have. The rewrite never catches up. The project gets canceled. The team is demoralized.
The strangler fig pattern avoids this entirely. Named after the strangler fig tree that grows around a host tree and gradually replaces it, this approach works in three steps:
- Step 1: Identify one component to extract. Route its traffic through an API gateway or proxy.
- Step 2: Build the new service alongside the monolith. Both run in production simultaneously. The proxy routes traffic to the new service for the extracted component and to the monolith for everything else.
- Step 3: Once the new service handles 100% of its traffic reliably, remove the old code from the monolith. Repeat with the next component.
This approach carries low risk because you're never replacing the whole system at once. If the new service has problems, the proxy routes traffic back to the monolith. Users never notice.
What to extract first
You have two good options for your first extraction:
Option A: The component that changes most often. Look at your git log. Which directory has the most commits in the past 6 months? That's the component causing the most deploy conflicts. Extracting it gives your teams immediate deployment independence.
Option B: The component that needs independent scaling. If your video processing module consumes 10x the resources of your API layer, extracting it lets you scale (and pay for) each component independently. The cost savings alone can justify the migration effort.
Don't start with the most complex component. Don't start with the component that has the most dependencies on other modules. Pick something with a clean boundary, a clear API surface, and a team that's ready to own it end-to-end. Your first extraction is a learning exercise as much as an architectural improvement.
Common mistakes that kill microservices migrations
Extracting too many services too fast
Teams get excited after their first successful extraction and try to split everything at once. Suddenly they're running 12 services with 12 deployment pipelines, 12 sets of logs, and 12 potential points of failure. The operational burden overwhelms the team. Extract one service, run it in production for 4-6 weeks, learn from the operational challenges, then extract the next one.
The distributed monolith
This is the most common failure mode. You split your application into 8 services, but they all share the same database. They all deploy together because changes in one service require schema changes that affect the others. You have all the operational complexity of microservices with none of the benefits. Each service must own its data completely. If two services need the same data, they communicate through APIs or events, not shared tables.
No service mesh or observability
Running microservices without distributed tracing is like driving at night with your headlights off. You won't know a service is degrading until users complain. You won't know which service caused a failure without manually searching through multiple log streams. Before you extract your first service, set up distributed tracing, centralized logging, and health check endpoints. This isn't optional infrastructure. It's a prerequisite.
Cost comparison: monolith vs. microservices
The infrastructure cost difference between these architectures is significant, and teams underestimate it consistently.
| Cost category | Monolith | Microservices |
|---|---|---|
| Compute (servers/containers) | $50 - $200/mo | $300 - $2,000/mo |
| Container orchestration (K8s/ECS) | $0 (not needed) | $75 - $500/mo |
| Monitoring and observability | $0 - $50/mo | $100 - $1,000/mo |
| Databases (multiple vs. single) | $15 - $100/mo | $100 - $1,000/mo |
| Message queues / event bus | $0 (not needed) | $25 - $500/mo |
| Total monthly infrastructure | $50 - $200 | $500 - $5,000 |
These ranges reflect mid-stage startups and growth-stage companies. Enterprise-scale deployments can run higher on both ends.
The infrastructure cost is the visible part. The hidden cost is engineering time. A team running microservices spends 20-30% of its capacity on operational tasks: updating Kubernetes configs, debugging inter-service communication, managing database migrations across multiple services, and maintaining CI/CD pipelines. That's engineering time that isn't going toward features your users care about.
The "modular monolith" middle ground
There's a third option most architecture articles skip: the modular monolith. You keep a single deployable unit but enforce strict module boundaries inside it.
Each module owns its own database tables (or schema). Modules communicate through well-defined internal APIs, not by reaching into each other's database tables or calling private functions. The code is organized so that extracting a module into a standalone service requires changing the transport layer (from in-process function calls to HTTP/gRPC) without rewriting the business logic.
The modular monolith gives you most of the organizational benefits of microservices (clear ownership, independent development within modules, enforced boundaries) without the operational cost. You deploy one artifact. You run one database server. You search one log stream.
Shopify's architecture is the best-known example. They run a monolithic Rails app with strictly enforced module boundaries. When a module needs to become a service (because of scaling requirements or team independence needs), the extraction is straightforward because the boundaries are already clean.
For teams between 5 and 30 engineers, the modular monolith is often the right answer. You get the structure and discipline of microservices thinking without paying the infrastructure and operational tax.
What Savi recommends
We've built monoliths, modular monoliths, and microservices architectures for clients across fintech, ecommerce, and SaaS. Here's the playbook we follow:
- Start monolithic. Every new product starts as a monolith. The priority is shipping features, validating the market, and iterating fast. Architectural purity doesn't matter if nobody uses your product.
- Enforce module boundaries from day one. Even in a monolith, each module should own its data and expose a clear interface. This costs almost nothing upfront and saves months of refactoring later.
- Set up observability early. Structured logging, error tracking, and basic performance monitoring. You need this whether you stay monolithic or migrate. It's table stakes.
- Extract services only when specific pain appears. Deployment conflicts blocking releases. Resource contention between modules. Team coordination overhead eating into engineering capacity. These are real signals, not theoretical concerns.
- Use the strangler fig pattern for extraction. One service at a time. Run it alongside the monolith. Validate it works. Then move on to the next one. Never do a Big Bang rewrite.
- Consider the modular monolith as your long-term architecture. For many products, it's the best of both worlds. You only move to true microservices when the modular monolith's single-deployment constraint becomes the bottleneck.
The best architecture is the one that lets your team ship features your customers want. For most companies at most stages, that's a well-structured monolith. For some companies at certain inflection points, it's a targeted extraction of specific services. For very few companies, it's a full microservices architecture.
Don't pick your architecture based on what Netflix or Uber uses. Pick it based on your team size, your traffic patterns, your deployment frequency, and the specific bottlenecks you're hitting today. Not the bottlenecks you might hit in two years.
Frequently asked questions
When should I migrate from a monolith to microservices?
Migrate when you see two or more of these signals: deploy conflicts block releases twice a week, teams step on each other in shared modules, one module's bug crashes the entire app, or scaling one feature forces you to scale everything. If you have fewer than 20 engineers, a monolith is almost certainly the right choice.
How much do microservices cost compared to a monolith?
A monolith runs on $50-$200/month in infrastructure. Microservices cost $500-$5,000/month when you add container orchestration ($75-$500), multiple databases ($100-$1,000), observability tools ($100-$1,000), and message queues ($25-$500). Teams also spend 20-30% of engineering capacity on operational tasks.
What is the strangler fig pattern for microservices migration?
The strangler fig pattern extracts one service at a time from your monolith. Route traffic through an API gateway, build the new service alongside the monolith, and shift traffic gradually. If the new service fails, route traffic back. This avoids Big Bang rewrites, which fail because the rewrite never catches up to 6-12 months of new monolith features.
What is a modular monolith and should I use one?
A modular monolith is a single deployable unit with strict internal module boundaries. Each module owns its database tables and communicates through defined internal APIs. For teams of 5-30 engineers, it delivers the organizational benefits of microservices (clear ownership, enforced boundaries) without the $500-$5,000/month infrastructure cost.
What should I extract first when moving to microservices?
Extract the component that changes most often (check your git log for the directory with the most commits in 6 months) or the component needing independent scaling (like a video processing module using 10x the CPU of your API). Avoid extracting the most complex component first. Your first extraction is a learning exercise.
Related reading
Multi-tenant SaaS architecture: what CTOs need to know
Database-per-tenant vs shared schema vs hybrid. How to pick the right multi-tenancy model and avoid the mistakes we see in production.
Serverless vs containers: which architecture fits your SaaS?
Serverless costs $0 at launch but gets expensive at scale. Containers cost more upfront but stay predictable. Here's how to pick the right architecture for your SaaS product.
Technical debt is killing your startup. Here's how to fix it.
Your engineers spend 25% of their time servicing code shortcuts from six months ago. That's $125K/year for a five-person team. Here's a system to stop the bleeding.
Planning a migration?
We've moved monoliths to microservices and built modular monoliths from scratch. 30-minute call.
Talk to our team