From Monolith to Microservices: The Journey Nobody Warns You About

February 10, 2026 (1w ago)

Every conference talk about microservices follows the same script: "We had a monolith. It was slow. We broke it apart. Now everything is fast and our teams are autonomous."

They skip the middle part. The part where everything catches fire.

I've been through two monolith-to-microservices migrations. One succeeded. One was rolled back after eight months. Here's what I wish someone had told me before either one.

The Monolith Isn't Your Enemy

Let's start with heresy: most systems should stay as monoliths.

A well-structured monolith with clear module boundaries handles millions of users just fine. Shopify serves over $200 billion in GMV through a monolithic Rails application. Stack Overflow serves 1.3 billion monthly page views from a monolith.

The actual reasons to migrate to microservices are narrower than most people think:

  1. Independent deployment velocity — Different teams need to ship at different cadences, and the monolith's shared deployment pipeline is the bottleneck
  2. Independent scaling — One component needs 100x the resources of others
  3. Technology heterogeneity — Different components genuinely need different tech stacks
  4. Organizational scaling — You have 100+ engineers and Conway's Law is destroying productivity

Notice what's NOT on this list: "because microservices are modern" or "because our monolith is messy."

If your monolith is messy, microservices give you a distributed mess. That's strictly worse.

The Distributed Transactions Nightmare

Here's the problem nobody mentions in "Intro to Microservices" blog posts.

In a monolith, placing an order looks like this:

1@transaction
2def place_order(user_id, items):
3    order = create_order(user_id, items)
4    deduct_inventory(items)
5    charge_payment(user_id, order.total)
6    send_confirmation(user_id, order.id)
7    # If anything fails, everything rolls back

One database. One transaction. ACID guarantees. Beautiful.

In microservices, the same operation spans four services:

1Order Service → Inventory Service → Payment Service → Notification Service

Each service has its own database. There is no cross-database transaction. If the payment fails after inventory was deducted, how do you roll back?

The SAGA Pattern: Your New Best Friend (and Headache)

The industry's answer is the SAGA pattern — a sequence of local transactions where each step has a compensating action:

1Step 1: Create Order (compensate: Cancel Order)
2Step 2: Reserve Inventory (compensate: Release Inventory)
3Step 3: Process Payment (compensate: Refund Payment)
4Step 4: Send Notification (compensate: — )

If Step 3 fails:

1Execute compensating actions in reverse:
2  → Release Inventory (undo Step 2)
3  → Cancel Order (undo Step 1)

Sounds clean in theory. In practice:

I once spent three weeks debugging a SAGA where the payment service's compensating action (refund) was succeeding, but the inventory service's compensating action (release) was silently failing due to a network partition. Users were being refunded for items that stayed "reserved" forever.

The Transactional Outbox: Making Sagas Reliable

The only way I've seen SAGAs work reliably at scale is with the Transactional Outbox Pattern:

1-- In the Order Service
2BEGIN;
3  INSERT INTO orders (id, status) VALUES ('abc', 'CREATED');
4  INSERT INTO outbox_events (id, type, payload) VALUES (
5    'evt-1', 'ORDER_CREATED',
6    '{"order_id": "abc", "items": [...]}'
7  );
8COMMIT;

A separate process reads the outbox table and publishes events to a message broker. This guarantees that the database write and the event publication are atomic — you can't have one without the other.

This is how companies like Uber and Airbnb achieve reliable distributed transactions at scale.

Uber's Course Correction: Domain-Oriented Microservices

Here's the part that should give you pause.

Uber, one of the poster children for microservices, published a landmark blog post acknowledging that their microservices architecture had gone too far. They had thousands of services, and the operational complexity was crushing developer productivity.

Their solution? Domain-Oriented Microservice Architecture (DOMA) — essentially grouping fine-grained microservices back into larger, domain-aligned services with clear interfaces.

In other words: they moved back toward the monolith, just a more structured version of it.

The sweet spot isn't "as many services as possible" or "one big monolith." It's the minimum number of services that solve your actual organizational and scaling problems.

The Hidden Costs Nobody Mentions

When planning a microservices migration, teams budget for the obvious costs: new infrastructure, service communication, API design. They almost never budget for these:

1. Distributed Debugging

In a monolith, a stack trace shows you exactly what happened. In microservices, you need distributed tracing (Jaeger, Zipkin), log aggregation (ELK, Datadog), and the expertise to correlate events across services.

Uber built an entire internal platform called Argos just for real-time root cause analysis across their microservices. That's not a library — that's a product with a dedicated team.

2. Integration Testing Becomes Almost Impossible

You can't spin up 30 services in a test environment and expect reliable integration tests. Companies solve this with:

Each solution has its own learning curve and failure modes.

3. Data Joins Across Services

In a monolith:

1SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id;

In microservices: You call the User Service API, then the Order Service API, then join in application code. Latency multiplies. Error handling multiplies. And if you need this data for a reporting dashboard, you've just discovered why every microservices company eventually builds a data warehouse.

4. Shared Libraries Become Governance Nightmares

"We'll put common code in a shared library." Congratulations, you've created a distributed monolith. Every library update requires coordinating deployments across all consuming services. You've traded a deployment monolith for a dependency monolith.

The Migration Playbook That Actually Works

After two migrations, here's the approach I'd use for a third:

Step 1: Fix the Monolith First

Extract clear module boundaries within the monolith. If you can't create clean modules in one codebase, you definitely can't create clean services across a network boundary.

Step 2: Strangle Pattern

Don't rewrite. Route specific traffic to new services while keeping the monolith running. New features go in services; old features migrate gradually.

Step 3: Start with the Most Independent Component

Find the component with the fewest cross-cutting concerns. For most companies, this is notification services or analytics pipelines — things that consume events but don't produce data other services depend on.

Step 4: Build the Platform Before the Services

Before extracting your second service, invest in:

Step 5: Stop When You've Solved Your Problem

This is the hardest step. Once the migration momentum starts, it's tempting to keep going. Don't. If you needed microservices to let three teams deploy independently, and you've achieved that with five services, stop at five services.

The Question You Should Actually Be Asking

The question isn't "should we use microservices?" It's "what specific problem are we solving, and is the operational complexity of microservices cheaper than the alternatives?"

Sometimes the answer is yes. Often, the answer is: fix your module boundaries, invest in better CI/CD, and keep the monolith.

The best architecture is the simplest one that solves your actual problems. Everything else is resume-driven development.


This analysis synthesizes learnings from Uber's DOMA architecture, the Strangler Fig pattern, SAGA patterns for distributed transactions, and real-world migration case studies from companies that have been through this journey.

Up next

How Netflix Delivers Video to 230 Million Users Without a Single Buffering Icon

A deep dive into Netflix's video processing pipeline — from shot-based encoding to multi-region failover — and why their architecture decisions are brilliant.