Migration Strategy — How Systems Actually Move from Monolith to Microservices
1. Why Migration Is Harder Than Building Microservices
By the time teams decide to move to microservices, they already understand the limitations of their monolith – performance issues, deployment risks, and scaling constraints make it clear that the current architecture cannot evolve much further. At this stage, teams naturally start thinking about a better architecture.
However, migration is very different from building a new system from scratch. When engineers design a completely new platform, they can freely define service boundaries, communication models, and deployment strategies. Migration happens under very different conditions because the existing system is already serving real users, handling live data, and supporting active business operations.
That changes the nature of the problem completely. The challenge is no longer just building microservices, but the real challenge is transforming the architecture while the system continues running in production.
2. Why Full Rewrites Usually Fail
Many organizations initially believe a complete rewrite will simplify the transition. The idea sounds attractive – build a fresh microservices platform, move traffic to it, and eventually retire the monolith entirely.
In practice, this approach creates significant risk. Large rewrites take a long time to complete. During that period, the existing monolith continues evolving, teams add new features, fix production bugs, and respond to changing business requirements.
As a result, the new system often becomes outdated before it is even fully deployed. A full rewrite also creates a dangerous dependency on one final cutover event. If major issues appear after the switch, rollback becomes extremely difficult.
Successful migrations avoid treating modernization as a replacement project, instead, they treat it as a gradual transformation of the existing system.
3. Understanding the Strangler Pattern
The Strangler Pattern provides the most practical approach for large-scale migrations. Instead of replacing the monolith in a single step, teams slowly extract functionality into independent services. New capabilities are built outside the monolith, while existing modules move out gradually over time.
The monolith continues running throughout the process, but its responsibilities become smaller as more services take ownership of business functionality.
Traffic shifts incrementally from the monolith to the new services. Because of this gradual transition, teams can isolate failures more effectively and reduce overall migration risk.
This approach changes migration from a high-risk replacement strategy into a controlled architectural evolution.
4. Choosing the Right Starting Point
One of the most important migration decisions involves selecting the first service to extract.
Not every component inside a monolith can be separated safely in the beginning. Some modules are deeply connected to core business logic and shared data structures. The safest starting point usually involves functionality with clear boundaries and fewer dependencies.
Capabilities such as notifications, reporting, or user profile management often work well as early candidates because they interact with fewer parts of the system.
Starting with smaller, well-defined areas allows teams to validate deployment pipelines, communication patterns, and operational behaviour before moving critical business functionality.
Over time, confidence grows, and more complex modules become easier to extract safely.
5. The Real Challenge: Separating Data
Code migration is difficult, but data migration is usually harder.
Inside a monolith, all modules share the same database. Once services are extracted, each service needs ownership over its own data.
During migration, both the monolith and the new services often depend on overlapping datasets. Managing this temporary shared state becomes one of the most sensitive parts of the transition.
Teams handle this challenge in different ways. Some replicate data into new services, while others allow services to access monolith data temporarily through APIs. Regardless of the strategy, the goal remains the same: move ownership gradually without disrupting existing behaviour.
Data migration succeeds only when teams treat it as an incremental transition rather than a one-time movement of tables.
6. Routing Traffic Between Old and New Systems
As migration progresses, the system enters a hybrid state where some requests belong to the monolith and others belong to microservices. Because of this, traffic routing becomes a critical architectural layer.
Gateways or routing components typically manage this transition. They decide whether requests should continue flowing into the monolith or move toward the newly extracted services.
This flexibility is extremely important during production rollout.
If a newly introduced service becomes unstable, teams can redirect traffic back to the monolith without affecting the entire system. That ability dramatically reduces operational risk during migration.
Instead of relying on one large production cutover, teams gradually shift traffic in controlled stages.
7. Living in a Hybrid Architecture
One misconception about migration is that the hybrid phase will be short. In reality, many organizations operate in mixed architectures for months or even years.
During this phase, part of the business logic still runs inside the monolith, while newer capabilities operate through microservices. Requests may pass through both environments before completing. This creates additional operational complexity.
Debugging becomes harder because execution paths span multiple systems. Data consistency becomes more difficult because ownership is partially distributed. Monitoring also becomes more important because failures may appear across architectural boundaries.
Stable migrations acknowledge this complexity early instead of assuming the transition period will disappear quickly.
8. Why Testing Becomes More Difficult During Migration
Testing strategies also change significantly during migration.
In a monolith, most testing focuses on one deployable system. During migration, teams must validate interactions between:
- the monolith
- newly extracted services
- routing layers
- distributed data flows
A change in one service can still affect workflows that partially depend on the monolith.
Because of this, migration requires stronger integration testing, better observability, and continuous production validation.
Testing no longer focuses only on correctness of logic. It must also validate correctness of communication and consistency across systems.
9. Operational Complexity Increases Before It Improves
One of the most important realities of migration is that complexity grows before benefits become visible.
For a period of time, teams must simultaneously operate:
- the monolith
- multiple microservices
- synchronization mechanisms
- distributed monitoring systems
This increases operational pressure significantly. Deployments multiply, debugging becomes distributed, and production monitoring becomes more demanding.
That temporary increase in complexity is normal. Over time, as more functionality moves into stable services, the operational model becomes cleaner and more scalable.
The benefits appear gradually, not immediately.
10. From Production Perspective
From a production perspective, successful migrations rarely look dramatic. Most of them progress quietly through incremental improvements over long periods of time. Stable migrations share several common characteristics.
Teams avoid large rewrites, move functionality in smaller steps, and continuously validate behavior in production environments. They focus more on reducing migration risk than on accelerating migration speed.
Failures still happen, but smaller incremental changes make those failures easier to isolate and recover from.
Over time, the monolith becomes smaller, services mature operationally, and the overall architecture evolves without major disruptions to users.
Summary
Migration from monolith to microservices is not a single architectural event. It is a long-running transformation process that reshapes the system gradually.
Successful migration requires:
- careful extraction of functionality
- controlled traffic routing
- incremental data ownership changes
- ability to operate reliably in hybrid environments
The Strangler Pattern works well because it supports this gradual evolution. Instead of replacing the system entirely, it allows the architecture to evolve safely over time.
That distinction is critical.
Real-world systems cannot stop operating while architecture changes underneath them. Migration strategies succeed when they respect that reality.
Check out the entire series on Understanding Micro Services, and start your journey from Part 1
Strangler Fig Routing in Java
The Strangler Fig pattern works by intercepting requests at a routing layer and directing them either to the legacy monolith or the new microservice, based on a feature flag or route prefix. The routing layer is where the migration state lives — it determines which version of each capability is active at any moment.
The code below shows a minimal routing proxy in Java. In production this is typically an API gateway or a sidecar, but implementing it as a Java component first clarifies the logic before it is externalised to infrastructure.
// Java 11+ -- feature-flag-based routing for Strangler Fig migration
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StranglerRouter {
private static final Logger log = LoggerFactory.getLogger(StranglerRouter.class);
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
private final FeatureFlags flags; // reads from config service or environment
public StranglerRouter(FeatureFlags flags) {
this.flags = flags;
}
// Route each capability independently -- migration can proceed feature by feature.
// The monolith handles everything until a flag is enabled for the new service.
public HttpResponse<String> route(String capability, String path, String body) throws Exception {
String targetHost = flags.isEnabled(capability)
? "http://new-" + capability + "-service"
: "http://legacy-monolith";
log.info("routing capability={} to={} path={}", capability, targetHost, path);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetHost + path))
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
log.info("capability={} status={}", capability, response.statusCode());
return response;
}
}Code language: Java (java)
The FeatureFlags dependency is what makes the migration controllable. Enabling a flag for 1% of traffic to the new service while the monolith handles the rest is how canary validation works during migration. Rolling back is a flag change, not a deployment. This is the key property that makes Strangler Fig safer than a big-bang cutover.
Interview Questions
What issues arise when the legacy monolith and the new microservice share the same database during a Strangler Fig migration?
Root Cause: Shared schema creates an implicit coupling that negates the independence that microservices are supposed to provide. Both systems read and write the same tables. Schema changes required by the new service can break the legacy system, and vice versa. Internal Behaviour: The new service cannot evolve its data model independently. Adding a NOT NULL column to a shared table that the monolith does not populate causes insert failures in the monolith. Dropping a column the monolith still reads causes query failures. Both systems must be coordinated for every schema change. Production Impact: Migration velocity slows to the rate of the slowest system. The database becomes a coordination point that was supposed to be eliminated. Fix: Use the Database-per-Service pattern from the start of extraction. During migration, introduce a synchronisation layer (dual-write or CDC via Debezium) that keeps both databases consistent while the migration completes. Accept eventual consistency between the two stores during the transition period.
How does the system behave if the legacy monolith is modified while the Strangler Fig routing layer is routing some traffic to the new service?
Root Cause: The monolith continues to evolve during migration. If it changes the API contract or data format for a capability that the new service is also handling, the two implementations diverge. Internal Behaviour: Requests routed to the new service may produce responses in a different format than the monolith for the same endpoint. If the routing layer does not apply response normalisation, clients receive different response shapes depending on which backend handled the request. This is non-deterministic from the client perspective. Production Impact: Clients that cache or parse responses start failing intermittently as routing shifts between backends. Integration tests pass against the monolith but fail in production. Fix: Freeze the monolith API contract for any capability under active extraction. Treat the routing layer as the source of truth for the request interface. Any changes to the migrated capability go into the new service only, not both. Track extraction progress explicitly so the team knows which capabilities are still shared.




