We’re kicking off the eighth post in our architecture patterns series, where we’ll focus on common patterns used in software migration projects.
Here are the previous posts in the series in case you missed any:
- Microservices Architecture Patterns: What Are They and What Benefits Do They Offer?
- Architecture Patterns: Organization and Structure of Microservices.
- Architecture Patterns: Microservices Communication and Coordination.
- Microservices Architecture Patterns: SAGA, API Gateway, and Service Discovery.
- Microservices Architecture Patterns: Event Sourcing and Event-Driven Architecture (EDA).
- Microservices Architecture Patterns: Communication and Coordination with CQRS, BFF, and Outbox.
- Microservices Patterns: Scalability and Resource Management with Auto Scaling.
Today, we’ll focus on 3 migration patterns.
Introduction to Migration Patterns in Microservices
The migration from monolithic applications to microservices-based architectures has gained popularity over the past decade, especially among industries looking to scale more efficiently and increase development velocity. While monolithic architectures are often robust, they present challenges such as the inability to scale components independently, high complexity in maintenance and CI/CD processes, and lack of flexibility in large development teams. In contrast, microservices enable agile development, independent scalability, and simplify the maintenance and continuous deployment of new features.
However, migrating a monolithic system to microservices is complex and, if not carefully planned, can introduce a number of issues. This is where migration patterns come into play, offering a systematic approach to make the transition smoother and more controlled. Below, we’ll look at three key migration patterns: the strangler pattern, the anti-corruption layer, and domain-based decomposition.
Strangler Pattern
Background and Context
The strangler pattern was popularized by Martin Fowler as a strategy for modernizing monolithic applications. The term “strangler” comes from a type of fig tree that grows around a host tree and eventually "strangles" it by replacing its functions until the original tree disappears. In software, this analogy describes how new functionalities gradually replace parts of the monolith, until the legacy system is no longer needed.
Technical Overview
The strangler pattern allows you to incrementally migrate a monolithic system to a microservices architecture. Instead of rewriting the entire system at once, you migrate one functionality at a time. A proxy or API Gateway is used to redirect requests: if the functionality has already been migrated, the request goes to the microservice; if not, it’s still handled by the monolith.
Implementation Strategy
- Identify candidate components: the first step is to identify areas of the monolith that can be safely extracted and migrated. These are typically well-bounded modules like user management, orders, or inventory.
- Create microservices: once identified, develop independent microservices using frameworks like Spring Boot in Java. These microservices replace the existing functionality in the monolith.
- Use a proxy or API Gateway: to ensure a seamless experience for users, requests are routed via a proxy or API Gateway that sends them to either the monolith or the microservice depending on the functionality.
Benefits
- Reduced risk: migrating small components minimizes impact on the system and allows for testing and iteration along the way.
- Faster iteration: you can release migrated microservices without having to pause or redesign the entire system.
- CI/CD compatibility: the incremental approach supports continuous integration and delivery (CI/CD), which aligns well with agile development teams.
Challenges
- Proxy coordination: as more functionalities are migrated, the logic within the proxy or API Gateway can become more complex.
- Temporary duplication: during migration, some features may exist both in the monolith and the microservice, leading to duplicated code.
Additional Considerations
- Monitoring: it’s crucial to implement observability tools like Prometheus or Grafana to track how traffic is being routed between the monolith and the microservices.
- Database management: during the transition, some data may need to be replicated across both systems or migrated gradually.
Let’s look at a simple example:
Let’s say you’re working on an eCommerce application that handles user management. In the monolithic system, the user management feature is tightly coupled with other parts of the platform. We’ll use the strangler pattern to migrate this functionality.
Original monolith: the original controller that manages user operations.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable String id) {
// Monolith logic to retrieve a user
return userService.getUserById(id);
}
}
Development of the user microservice: we create a microservice that is completely independent from the monolith. It is deployed as a separate service, performing the same task but in a decoupled manner.
@RestController
@RequestMapping("/api/users")
public class UserMicroserviceController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
// Retrieve the user from the decoupled database
return ResponseEntity.ok(userService.findById(id));
}
}
API Gateway configuration: the proxy or gateway decides whether to route the request to the monolith or the microservice. In this example, we use Spring Cloud Gateway.
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user_route", r -> r.path("/users/**")
.filters(f -> f.rewritePath("/users/(?<segment>.*)", "/api/users/${segment}"))
.uri("http://localhost:8081")) // URL of the user microservice
.build();
}
Example diagram:

Once the functionality is migrated to the microservice architecture, all calls are routed to the new endpoint and the migrated functionality in the monolith is disabled or removed.
Anti-Corruption Layer Pattern
History and Context
The Anti-Corruption Layer (ACL) pattern originates from the Domain-Driven Design (DDD) approach developed by Eric Evans. Its goal is to keep new systems free from poor design decisions or inconsistent models found in legacy systems. This is especially important when migrating from complex monoliths to microservices, where it’s crucial to prevent microservices from inheriting the legacy system’s complexity.
Technical Description
The anti-corruption layer introduces a translation layer between the new microservices and the legacy system. This layer acts as an “adapter” that transforms legacy data structures and logic into formats that are easier to manage and consistent for the microservices.
Implementation Strategy
- Create a translation interface: the goal of this layer is to translate requests coming from the new microservice into a format the legacy system can understand—and vice versa.
- Domain independence: business logic in the microservice should be completely independent from the legacy logic. The anti-corruption layer ensures that problems and inconsistencies in the old system do not affect the new design.
Advantages
- Protects microservices: isolates microservices from distorted designs caused by legacy system interactions.
- Smooth migration: allows microservices to integrate with legacy systems without modifying the new system’s logic.
- Modularity and flexibility: each microservice preserves its integrity and doesn’t have to adapt to the legacy system’s complexities.
Challenges
- Complexity in the translation layer: depending on how different the legacy system is from the new one, the anti-corruption layer can become complex and hard to maintain.
- Performance: the translation layer introduces latency, as it performs real-time conversions and translations.
- Feature development during migration affects the anti-corruption layer.
- Lack of transparency: if the translation logic becomes too opaque, it may be difficult to identify where errors occur.
Example: communicating with a legacy inventory system
Let’s say you have an old monolithic system that handles inventory, and you’re developing a new order management microservice that needs to access inventory data.
Order management microservice:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private InventoryAntiCorruptionLayer inventoryLayer;
@PostMapping
public ResponseEntity<String> placeOrder(@RequestBody Order order) {
if(inventoryLayer.checkAvailability(order.getProductId())) {
// Logic to process the order
return ResponseEntity.ok("Order placed successfully");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Product not available");
}
}
}
Anti-Corruption Layer:
The anti-corruption layer is responsible for translating requests from the order microservice to the legacy monolithic inventory system.
public class InventoryAntiCorruptionLayer {
public boolean checkAvailability(String productId) {
// Translate the query into the legacy system format
LegacyInventorySystem legacySystem = new LegacyInventorySystem();
return legacySystem.isProductAvailable(productId);
}
}
In this case, the InventoryAntiCorruptionLayer
class acts as an adapter, interacting with the legacy inventory system (represented by the LegacyInventorySystem
class), thus keeping the microservice isolated from the complexities of the legacy system.
Diagram of the example:

Client: the user who makes the requests.
API Gateway: routes the requests.
Inventory microservice: handles inventory operations.
ACL: translates the requests to the legacy system.
Legacy inventory system: monolithic system.
Domain Decomposition
History and Context
Domain decomposition is deeply tied to the Domain-Driven Design (DDD) methodology, which proposes that software design should align with the business model of the company.
Instead of building the architecture around technologies or technical layers (like databases, interfaces, or services), this approach suggests dividing the system based on business needs and processes, helping to avoid tightly coupled dependencies between components.
Technical Description
Domain decomposition involves breaking down the monolithic system into microservices that each correspond to specific business domains. A domain represents a logical part of the business with a cohesive set of rules and processes. This technique allows each microservice to own its own business logic and data persistence, eliminating unnecessary dependencies between components.
Implementation Strategy
- Identify bounded contexts: A bounded context is a DDD term that defines the limits within which a domain makes sense. For example, in an eCommerce system, possible domains could include “product management,” “order processing,” or “payments.”
- Model independent microservices: Once domains are identified, each one is implemented as a separate microservice. Each service should have full control over its data model, business rules, and APIs.
- Microservice communication: Often, domains need to communicate with one another. This can be done using asynchronous messaging (e.g., Kafka or RabbitMQ) or via REST APIs.
Advantages
- Business alignment: Microservices mirror the business structure, making it easier to evolve the application as business requirements change.
- Independent scalability: Each domain can scale independently according to business needs. For instance, the product service can scale separately from the payment service.
- Modular development: Teams can work on separate microservices without needing heavy coordination.
Challenges
- Defining boundaries correctly: Identifying the proper domains and bounded contexts can be difficult and may require multiple iterations.
- Microservice coordination: If microservices are too tightly coupled, communication between them can become a bottleneck.
- Eventual consistency: Since microservices manage their own databases, synchronizing data can be a challenge. This is where eventual consistency becomes essential.
Example: Splitting an eCommerce System
Let's consider a monolithic eCommerce system that handles orders, payments, and products.
Using domain decomposition, we can divide it into three microservices:
- Product management microservice:
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{id}")
public Product getProduct(@PathVariable String id) {
return productService.findById(id);
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productService.save(product);
}
}
- Order management microservice:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public Order placeOrder(@RequestBody Order order) {
// Check product availability
Product product = productClient.getProduct(order.getProductId());
if(product != null) {
return orderService.save(order);
} else {
throw new ProductNotFoundException("Producto no disponible");
}
}
}
- Payment microservice:
@RestController
@RequestMapping("/payments")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping
public Payment processPayment(@RequestBody Payment payment) {
return paymentService.processPayment(payment);
}
}
Example diagram:

Wait… aren’t the strangler and domain decomposition patterns quite similar?
Yes, they are—and here’s what they have in common
Goal of gradual migration: Both patterns aim for a gradual transition from a monolithic architecture to microservices. They allow for transformation without disrupting ongoing operations, which is critical for systems already in production.
Preservation of existing functionality: Both the strangler and domain decomposition patterns seek to preserve the original system’s functionality during the migration. This helps reduce the risk of a big-bang rewrite.
Componentization strategies: Both approaches encourage the creation of independently deployable and manageable components. This makes them flexible strategies for teams looking to manage and release application features incrementally.
Scalability and reduced coupling: As specific services or domains are migrated, both patterns improve scalability and reduce architectural coupling, making it easier to scale only the necessary parts.
But they’re not the same—here’s how they differ
Primary purpose and initial architecture
- The strangler pattern focuses on separating and replacing specific functionalities from the monolithic system in a controlled and gradual way. Each feature is extracted and replaced over time.
- The domain decomposition pattern is centered on splitting the application into microservices based on logical business domains, allowing each service to be autonomous within its context. It’s more of a business-oriented architectural refactoring.
Evolution approach
- Strangler aims to "strangle" or encapsulate monolithic components, moving functionality into new services until the original system is fully replaced.
- Domain decomposition organizes the system around defined bounded contexts, common in domain-driven design (DDD), where microservices represent specific business entities.
Migration order
- Strangler progresses in stages: it first surrounds the monolith, then gradually moves individual features to the new system.
- Domain decomposition starts with well-defined business domains, typically those with fewer dependencies or that can deliver standalone value.
Compatibility with the monolith
- In the strangler pattern, some functionality remains in the monolith during the transition, and integration with the monolith continues until full replacement is achieved.
- In domain decomposition, microservices are generally autonomous in their domain logic and don’t rely on the monolith. They interact with each other via APIs.
Conclusion
When implemented correctly, these migration patterns can make the transition from a monolithic system to a microservices architecture more efficient, scalable, and modular. However, each pattern has its own pros and cons, so choosing the right migration strategy must be aligned with your organization’s specific needs and capabilities.
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.