---
title: "Migrating a Monolith to Kubernetes Without a Big-Bang Cutover"
description: "Using the strangler-fig pattern to move a large monolith onto EKS service by service, with a routing facade, gradual traffic shifting, and a rollback at every step."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/case-studies/post/monolith-to-kubernetes-strangler-migration
---

# Migrating a Monolith to Kubernetes Without a Big-Bang Cutover

Almost every failed "let's move off the monolith" project shares one detail: the plan was a big-bang cutover. Rewrite in parallel, pick a weekend, flip the switch, and pray. This is the opposite of that. A large application moved onto EKS one service at a time using the strangler-fig pattern, with a routing facade in front, traffic shifting gradually per route, and a working rollback at every single step. No freeze, no weekend gamble, and no moment where the whole thing was in the air.

## Impact

The application reached Kubernetes with no big-bang moment. Every route moved gradually behind a facade with a working rollback, so no single step was ever high stakes and delivery never froze.

The gains were structural rather than a one-time event. Each extracted service got independent deploys and its own scaling, so teams stopped blocking each other and the hot paths no longer forced the whole application to scale with them.

<div class="not-prose my-8 flex flex-wrap gap-x-10 gap-y-6">

</div>

## The problem

The monolith itself was not the enemy. It ran fine, the team knew it, and it paid the bills. The problem was that it had become the bottleneck for everything else. Deploys were all-or-nothing, so one risky change held up every other team's work. Scaling meant scaling the entire application even when only one part was hot. And onboarding a new engineer meant handing them the whole thing at once.

The tempting fix, a full rewrite with a cutover, is where teams get hurt. You freeze features to build the replacement, the replacement drifts from the original as the original keeps changing, and the cutover becomes a single high-stakes event with no safe rollback. If anything goes wrong at 2am on migration night, the only option is a panicked revert of everything.

The goal was to get the benefits of independent services without ever betting the business on one cutover. That means the old and new systems have to run side by side, in production, for as long as it takes.

## Constraints

- **No big-bang cutover.** At no point could correctness depend on a single switch-flip.
- **No feature freeze.** The monolith kept shipping features throughout the migration.
- **A rollback at every step.** Each increment had to be revertible in minutes, not hours.
- **No shared-database free-for-all.** Extracted services own their data; the goal was decoupling, not a distributed monolith on one schema.
- **Prove parity before deleting anything.** Old code stayed until the new path was verified against it.

## Architecture

Before the migration, the shape was familiar: an Application Load Balancer in front of a monolith running across an Auto Scaling group, all talking to one shared relational database.

_Before: monolith on EC2_

The target keeps the monolith running, containerized, inside an EKS cluster, and puts a routing facade in front of everything. The facade is the heart of the pattern. It looks at each request and decides whether that path has been migrated to a new service or still belongs to the monolith. Extracted services get their own data stores; the monolith keeps its shared database until its remaining parts are small.

_After: strangler facade on EKS_

The name comes from the strangler fig, a plant that grows around a tree and gradually replaces it. The new system grows around the monolith, taking over one responsibility at a time, until the original is either gone or small enough to leave alone. Nothing about it requires a dramatic finish.

## The routing facade

The facade is where the safety comes from. Every request enters through it, and a route table decides the destination. A path that has been migrated goes to the new service; everything else defaults to the monolith. Crucially, migration of a single route is itself gradual: you shift a small percentage of that route's traffic to the new service, watch it, and widen only when it holds. If the new service misbehaves, the facade falls straight back to the monolith, which is still running and still correct.

_Strangler routing_

In practice the facade can be an ingress with weighted routing, an API gateway, or a service mesh. The mechanism matters less than the property: per-path routing plus per-path traffic weight plus instant fallback.

```yaml title="facade-route.yaml (illustrative weighted routing)"
# /users is being migrated: 10% to the new service, 90% still to the monolith.
http:
  - match:
      - uri:
          prefix: /users
    route:
      - destination: {host: users-service}
        weight: 10
      - destination: {host: monolith}
        weight: 90
  - route: # default: everything else stays on the monolith
      - destination: {host: monolith}
        weight: 100
```

## Implementation

The migration ran as a loop, not a project plan with an end date. Each pass picked one seam, extracted it, shifted traffic, verified, and cleaned up.

_Extraction sequence_

1. **Containerize the monolith first.** Before extracting anything, get the monolith itself running in EKS behind the facade. Now old and new live in the same place, and the facade is the only thing in front.

2. **Pick a loosely-coupled seam.** Choose a capability with a clear boundary and a data set it mostly owns, for example users or billing. Avoid the tangled core on the first pass; early wins build trust.

3. **Build the service with its own data.** Give the extracted service its own database rather than pointing it at the monolith's schema. Backfill and keep it in sync during the transition, but the target is independent ownership.

4. **Route to it gradually.** Add the path to the facade and shift a small slice of traffic, then widen. Watch latency and error rates during each step, and keep the monolith path warm as a fallback.

5. **Verify parity, then delete.** Once the new service matches the monolith's behavior under real traffic, remove that code from the monolith. Deleting the old path is what makes the win permanent.

6. **Repeat, and know when to stop.** Move to the next seam. Stop when what remains is small and stable enough that extracting it would cost more than it returns.

  The most dangerous shortcut is pointing a new service at the monolith's
  database so you can "extract later." That gives you two services coupled
  through one schema, which is a distributed monolith: all of the network
  overhead, none of the independence. Give the service its own data, even if
  that means a sync period during the transition.

Data is the genuinely hard part, and it is worth being honest about that. Moving stateless request handling is straightforward; moving the data it owns without downtime is not. The workable approach is to give the new service its own store, backfill it, keep it in sync while both paths run, and cut the monolith's write path over only once the new service is authoritative and verified. Where strict consistency is required during the overlap, treat the monolith as the source of truth until the very last step.

  Measure the migration by how much of the monolith is gone, not by how many
  services exist. A useful signal is the share of production traffic served by
  extracted services and the amount of code deleted from the monolith. Services
  created without code removed from the original is motion without progress.

## Results

The application moved onto EKS without a single cutover event and without a feature freeze. Because each route shifted gradually with a live fallback, no migration step was a high-stakes moment; the riskiest change only ever touched a small slice of one path at a time. Independent deploys arrived for each extracted service, so teams stopped blocking each other, and the hot paths could scale on their own instead of forcing the whole application to scale with them.

The migration also did not finish in the storybook sense, and that was the right outcome. A stable, low-change remainder of the monolith stayed in place, containerized and behind the facade, because extracting it would have cost more than it returned. Treat "the monolith is gone" as a possible ending, not the goal.

## Lessons

The facade is the whole safety story. Because every request always had a valid destination and an instant fallback, no step was irreversible. That single property is what let the team move quickly instead of cautiously.

Extract the easy seams first. The instinct to start with the messy core is a trap. Early, low-risk extractions build the tooling and the team's confidence, so the hard ones later are routine instead of terrifying.

Data ownership is the real migration. The service boundary is easy; the data boundary is the work. Any plan that hand-waves the database is a plan to build a distributed monolith.

Give yourself permission to stop. The goal was never zero monolith. It was independent, deployable, scalable services for the parts that needed it, and a small stable remainder for the parts that did not.

## Frequently Asked Questions

> **What exactly is the strangler-fig pattern?**

It is an incremental migration approach where a new system grows around an old one and takes over its responsibilities one at a time, until the old system is replaced or reduced to a small remainder. A routing facade sits in front and directs each request to either the new component or the old one, so both run in production together and you never need a single cutover.

> **Why not just rewrite and cut over on a weekend?**

Because a cutover is a single high-stakes event with no safe rollback. You freeze features to build the replacement, it drifts from the original as the original keeps changing, and if anything breaks on migration night your only option is reverting everything at once. Strangler-fig keeps the old system live the whole time, so every step is small and reversible.

> **What makes a good first service to extract?**

Low coupling and a clear data owner. Pick a capability with a clean boundary that mostly owns its own data, like users or billing, so you are not untangling shared state on your first attempt. Early, low-risk wins build the tooling and the confidence you will need for the harder seams later.

> **How do you handle the shared database?**

Give each extracted service its own store rather than pointing it at the monolith's schema. Backfill it and keep it in sync while both paths run, then cut the monolith's write path over only once the new service is authoritative and verified. Sharing one database across services is a distributed monolith and defeats the point of the migration.

> **How do you know when the migration is done?**

When the remaining monolith is small and stable enough that extracting more would cost more than it returns. Track the share of production traffic served by extracted services and the amount of code deleted from the monolith. Done does not have to mean zero monolith; a low-change remainder behind the facade is a perfectly good ending.

## References

- [Martin Fowler: StranglerFigApplication](https://martinfowler.com/bliki/StranglerFigApplication.html)
- [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html)
- [AWS Prescriptive Guidance: strangler fig pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-decomposing-monoliths/strangler-fig.html)
- [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [Database decomposition patterns](https://microservices.io/patterns/data/database-per-service.html)
