From Monolith to Microservices: A Pragmatic Approach
"Let's rewrite it as microservices" is the most expensive sentence in software engineering. After leading several decomposition projects — some successful, some painful — here's my pragmatic guide to when and how to break apart a monolith.
When Microservices Are the Wrong Answer
Before reaching for microservices, ask: what problem am I actually solving?
Microservices don't fix:
- **Bad code structure** — if your monolith is a mess, your microservices will be a distributed mess
- **Slow deployments** — often a CI/CD problem, not an architecture problem
- **Team communication issues** — Conway's Law cuts both ways
Microservices *do* help when:
- Different parts of the system need to scale independently
- Teams need to deploy and release independently
- Technology choices need to vary by domain
The Strangler Fig Approach
Instead of a big-bang rewrite, extract services one at a time. The pattern:
- **Identify a bounded context** with clear boundaries
- **Build the new service** alongside the monolith
- **Route traffic** through a facade/gateway
- **Migrate data** incrementally
- **Cut over** when the new service is proven
┌─────────────────────────────┐
│ API Gateway │
├──────────┬──────────────────┤
│ │ │
│ New │ Monolith │
│ Service │ (shrinking) │
│ │ │
└──────────┴──────────────────┘The crucial part: the monolith keeps working throughout. No big-bang cutover. No "we'll be down for maintenance" weekends.
Choosing What to Extract First
Pick a candidate that has:
- **Clear domain boundaries** — minimal shared state with other areas
- **Independent data** — its own tables, not joined to everything
- **High change frequency** — you'll see the benefit of independent deployment quickly
- **Low risk** — not your payment processing system on the first try
In practice, I've found notification services, reporting/analytics, and file processing make excellent first candidates.
Data: The Hard Part
The trickiest aspect of decomposition is data. A monolith typically has a single database with foreign keys everywhere.
Shared Database (Temporary)
As a transitional step, the new service can read from the monolith's database. This is explicitly temporary — it couples the service to the monolith's schema:
// temporary: direct database access
var orders = await _monolithDb.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();API Integration (Target State)
The target state is service-to-service communication via APIs or events:
// target: API call to order service
var orders = await _orderServiceClient
.GetOrdersByCustomerAsync(customerId);Event-Driven Data Sync
For data that multiple services need, publish change events:
// order service publishes events
await _serviceBus.PublishAsync(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Total = order.Total
});
// notification service subscribes and maintains its own projectionWhat I Wish I'd Known Earlier
- **Start with a modular monolith.** Extract clean modules within the monolith before splitting into services. This forces you to define boundaries without the operational overhead of distributed systems.
- **You need observability first.** Distributed tracing, centralized logging, and health checks should be in place *before* you split. Debugging a distributed system without observability is miserable.
- **Network is not free.** Every service boundary is a network call. Latency, partial failures, and serialization overhead add up. Measure before and after.
- **Team structure matters.** One team owning 12 microservices is worse than a monolith. Align service boundaries with team boundaries.
Key Takeaways
- Don't start with microservices — start with understanding your domain boundaries
- Use the Strangler Fig pattern for incremental migration
- Data separation is the hardest part — plan for a transitional period
- Invest in observability before decomposing
- A well-structured monolith beats poorly designed microservices every time
Share this article
About the Author
Georg is a senior solution architect specializing in .NET, Azure, and Dynamics 365. He helps organizations design and build scalable, maintainable enterprise systems. When he's not writing code, he's writing about it here.
Learn more about Georg