From Monolith to Microservices: When to Make the Switch
The tech industry has a pattern: a useful concept emerges, gets evangelized, becomes dogma, and then teams adopt it regardless of fit. Microservices followed this trajectory perfectly. For a while, building a monolith felt like admitting defeat before you started.
But here's the uncomfortable truth: most applications should start as monoliths. And many should stay that way.
This isn't anti-microservices propaganda. We've built both, and each architecture has its place. The real skill is knowing which one fits your situation, and recognizing when it's time to change.
The Case for Monoliths (Really)
Before discussing migration, let's defend the monolith. Done well, it's not a ball of mud. It's a deliberate architectural choice with real advantages.
Development Velocity
In a monolith, there's one codebase, one deployment pipeline, and one thing to understand. A new developer can clone the repo and have the entire system running locally in minutes.
# The dream
git clone myapp
cd myapp
npm install
npm run dev
# Everything works
Compare this to a microservices setup where spinning up the local environment means orchestrating 12 services, managing network connections, and debugging why the auth service isn't talking to the user service.
Simpler Operations
Monoliths have one deployment target. One set of logs. One place to monitor. One thing to scale (at least initially).
# Monolith deployment
deploy:
- build app
- push to production
- done
# Microservices deployment
deploy:
- build 12 services
- update service mesh config
- coordinate rolling deployments
- pray nothing breaks mid-rollout
For teams without dedicated DevOps, the operational overhead of microservices can consume more time than feature development.
Refactoring Confidence
In a monolith, your IDE knows about everything. Rename a function, and it updates every caller. Change a data model, and the compiler catches every broken reference.
Cross-service refactoring? Good luck. You're coordinating API version changes, handling backward compatibility, and hoping your integration tests catch the edge cases.
The Modular Monolith
Here's the secret: you can have the best of both worlds. A well-structured monolith uses the same boundaries microservices would, just without network calls between them.
src/
├── modules/
│ ├── auth/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── repositories/
│ │ └── index.ts # Public API
│ ├── billing/
│ │ ├── controllers/
│ │ ├── services/
│ │ └── index.ts
│ └── users/
│ └── ...
├── shared/
│ ├── database/
│ └── utils/
└── app.ts
Each module exposes a clean public interface. Internal implementation stays private. When you eventually need to extract a service, the boundaries are already defined.
When Microservices Actually Make Sense
So when should you consider microservices? Not when some architecture astronaut tells you it's "best practice." Consider them when you're hitting real problems that microservices actually solve.
Different Scaling Requirements
Your user-facing API handles 10,000 requests per second. Your report generation runs once daily but needs massive compute when it does. Your real-time notification system needs to be always available.
Scaling these together is wasteful. The report generator doesn't need 50 instances. The notification system shouldn't go down because the API is overloaded.
Monolith scaling: Scale everything together
├── API endpoints (10k rps) ─┐
├── Report generation ├── All scaled identically
└── Notifications ─┘
Microservices scaling: Independent scaling
├── API Service → 50 instances
├── Report Service → 2 instances (bursty)
└── Notification → 10 instances (always-on)
Team Independence
You have 40 engineers across 6 teams. In a monolith, they're constantly stepping on each other. Merge conflicts are daily. Deployments require cross-team coordination. A bug in billing blocks the marketing team's release.
Microservices let teams own their services end-to-end. They choose their release schedule, their technology stack (within reason), and their development practices.
But be honest: if you have 5 developers, you don't have this problem.
Technology Diversity
Your main application runs on Node.js, but you need a machine learning component. Python's ecosystem is vastly superior for ML. In a monolith, you're either embedding Python somehow (awkward) or building a separate service anyway.
Microservices embrace this: each service uses the best tool for its job.
Service Architecture:
├── API Gateway (Node.js/Express)
├── User Service (Node.js/TypeScript)
├── ML Pipeline (Python/FastAPI)
├── Search Service (Go/Elasticsearch)
└── Notification (Node.js)
Fault Isolation
When your monolith crashes, everything crashes. A memory leak in one module takes down the whole system. An infinite loop blocks all requests.
With microservices, a failing component doesn't necessarily take down the others. The billing service can be down while users still browse products (though you should probably fix that quickly).
Signs It's Time to Migrate
You're running a successful monolith, but things are getting painful. Here are concrete signals that migration might be worth the investment:
Deploy Fear
When deploying becomes an anxiety-inducing event requiring multiple people on standby, something's wrong. If you're spending more time coordinating releases than building features, the monolith is holding you back.
Scaling Walls
You've vertically scaled your servers to the maximum your cloud provider offers. Horizontal scaling doesn't help because the bottleneck is a single database or a single queue processor.
Team Gridlock
Pull requests sit for days because everyone's touching the same files. "Who owns this code?" is a common question. Teams are blocked by each other's unfinished work.
Release Coupling
Team A finished their feature two weeks ago but can't release because Team B's feature shares the deployment and isn't ready. Every release requires coordinating across multiple teams.
Performance Isolation
That batch job you run nightly is affecting real-time user requests. You can't scale the parts that need scaling without scaling everything else.
The Migration Playbook
Decided to migrate? Don't rewrite everything. That path is littered with failed projects and burned-out teams. Instead, extract incrementally.
Step 1: Identify Boundaries
Find the seams in your monolith. Look for:
- Distinct domains: Billing, users, inventory, notifications
- Different data ownership: What data belongs together?
- Team alignment: What could one team fully own?
- Scale differences: What needs independent scaling?
Map these boundaries before writing any new code.
Step 2: The Strangler Fig Pattern
Named after vines that gradually envelop trees, this pattern lets you extract services without a big bang rewrite.
Phase 1: Route through facade
┌─────────────────────────────────────────┐
│ Monolith │
│ ┌─────────────────────────────────┐ │
│ │ Billing Module │ │
│ │ ├── Calculate Invoice │ │
│ │ ├── Process Payment │ │
│ │ └── Generate Report │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Phase 2: Extract behind interface
┌──────────────────────────────────────────┐
│ Monolith │
│ ┌─────────────────────────────────┐ │
│ │ Billing Facade │ │
│ │ └── Calls billing service ──────────┼──► Billing Service
│ └─────────────────────────────────┘ │ ├── Calculate Invoice
│ │ ├── Process Payment
└──────────────────────────────────────────┘ └── Generate Report
Phase 3: Remove facade, direct communication
┌──────────────────────────────────────────┐
│ Monolith (smaller) │
│ └── Other modules ────────────────────────► Billing Service
└──────────────────────────────────────────┘
Step 3: Start with the Edges
Don't start with your core domain. Extract peripheral services first:
- Notifications: Usually self-contained, low risk
- Reporting: Read-only, can tolerate eventual consistency
- File processing: Independent, batch-oriented
Build your operational expertise on lower-stakes services before touching critical paths.
Step 4: Data Separation
The hardest part isn't extracting code. It's separating data. When your monolith shares one database, you need a migration strategy.
Option A: Shared database (temporary)
- Quick to implement
- Creates coupling you'll regret
- Only use as stepping stone
Option B: Database per service
- True independence
- Requires data synchronization strategy
- Event-driven updates or API calls
Option C: Gradual migration
- New service gets its own tables
- Read from both during transition
- Migrate writes, then reads
- Remove old tables when confident
Step 5: Invest in Infrastructure
Microservices require operational maturity. Before extracting your first service, ensure you have:
- Service discovery: How services find each other
- Centralized logging: Aggregate logs across services
- Distributed tracing: Follow requests across boundaries
- Health checks: Know when services are unhealthy
- Circuit breakers: Graceful degradation when services fail
// Example: Circuit breaker pattern
class BillingClient {
private circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000,
});
async calculateInvoice(userId: string) {
return this.circuitBreaker.execute(async () => {
const response = await fetch(`${BILLING_URL}/invoice/${userId}`);
if (!response.ok) throw new Error('Billing service error');
return response.json();
});
}
}
What to Avoid
The Big Bang Rewrite
"Let's rebuild everything as microservices!" This approach fails more often than it succeeds. You're rewriting working software while maintaining the old system. By the time you finish, requirements have changed.
Distributed Monolith
If your services all need to be deployed together, can't function independently, and share a database, you have a distributed monolith. All the complexity of microservices with none of the benefits.
Over-Extraction
Not everything needs to be a service. If two components always change together and are owned by the same team, they should probably stay together.
Ignoring Latency
Network calls are orders of magnitude slower than function calls. What was a 10ms operation in your monolith might become 200ms across services. Design for this.
Making the Decision
Here's a quick decision framework:
Stay with monolith if:
- Team is under 10-15 engineers
- Single clear scaling profile
- Rapid iteration more important than perfect architecture
- Limited DevOps expertise
- Early-stage product still finding fit
Consider microservices if:
- Multiple teams with clear domain ownership
- Parts of the system need independent scaling
- Deploy coupling is blocking teams
- Different technology requirements per component
- Organization is growing and needs team autonomy
The honest answer: If you're asking whether you need microservices, you probably don't. When you truly need them, you'll know. The pain will be undeniable.
Need Help with Your Architecture?
Architecture decisions have long-lasting consequences. Whether you're building a new application and wondering about the right approach, or you're feeling the pain of a monolith that's outgrown its structure, we can help.
At High Mountain Studio, we've guided teams through successful migrations and helped startups avoid premature complexity. Book a consultation to discuss your specific situation.

