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:

  1. Microservices Architecture Patterns: What Are They and What Benefits Do They Offer?
  2. Architecture Patterns: Organization and Structure of Microservices.
  3. Architecture Patterns: Microservices Communication and Coordination.
  4. Microservices Architecture Patterns: SAGA, API Gateway, and Service Discovery.
  5. Microservices Architecture Patterns: Event Sourcing and Event-Driven Architecture (EDA).
  6. Microservices Architecture Patterns: Communication and Coordination with CQRS, BFF, and Outbox.
  7. 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

Benefits

Challenges

Additional Considerations

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:

customer –> api gateway –> user microservices (migrated) / monolite (no migrated)

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

Advantages

Challenges

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 → API Gateway → Inventory Microservice → ACL → Legacy Inventory System

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

Advantages

Challenges

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:

  1. 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);
    }
}
  1. 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");
        }
    }
}
  1. 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:

legacy ecommere → users microservice, inventory microservice, orders microservice, payments microservice

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

Evolution approach

Migration order

Compatibility with the monolith

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.

Tell us what you think.

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.

Subscribe

This page uses proprietary and third-party cookies to improve the user experience. You can activate all of them or configure their use. Set up.