How to migrate to microservices with the strangler pattern

Waqas Ahmed
7 min readJul 26, 2023

--

1. Monolithic Architecture:

Monolithic architecture is a traditional software design where an entire application is developed as a single, tightly integrated unit. In this approach, all components, features, and functionalities are tightly coupled within a single codebase, often leading to a large and complex application.

2. Microservices Architecture:

Microservices architecture is an innovative approach where an application is broken down into a collection of loosely coupled, independently deployable services. Each service is responsible for a specific business capability and communicates with others through well-defined APIs.

Introduction Strangler Pattern

The “Strangler Pattern” is a software design and migration strategy used in the context of legacy system modernization or application rewrites. It was introduced by Martin Fowler, a renowned software engineer and author. The pattern’s name is inspired by the way certain plants, called “strangler figs,” grow around their host trees, eventually overtaking and replacing them.

In the context of software development, the Strangler Pattern involves the gradual replacement of an existing system with a new one. Instead of building an entirely new application from scratch and replacing the old one in one big-bang release, the Strangler Pattern allows developers to incrementally and systematically migrate functionality from the old system to the new one.

Here’s how the Strangler Pattern typically works:

  1. Identify functional areas: First, you identify the distinct functional areas or features of the legacy system.
  2. Create parallel functionality: Develop equivalent functionality in the new system for one of the identified functional areas. This new functionality usually coexists with the old one.
  3. Gradual migration: Once the new functionality is tested and ready, you start routing a portion of the user traffic or data to the new system for that specific functionality. This can be done through load balancers or routing mechanisms.
  4. Monitor and iterate: As more users and data are migrated, you closely monitor the performance and ensure the new system behaves as expected. If any issues arise, you can quickly address them.
  5. Repeat: Continue the process of creating new functionality in the new system, gradually migrating users and data until the old system becomes obsolete and can be decommissioned entirely.

The benefits of the Strangler Pattern include reduced risk during migration, the ability to leverage modern technology in a phased manner, and minimal disruption to end-users. It also allows development teams to maintain essential business functionality while incrementally improving the overall system.

It’s worth noting that while the Strangler Pattern is a useful strategy, it might not be the best approach for every situation. The decision to adopt this pattern should be based on factors such as the size of the legacy system, the complexity of the migration, and the organization’s specific requirements and constraints.

Migrating to microservices using the Strangler Pattern involves breaking down a monolithic application into smaller, more manageable microservices incrementally.

Here’s a step-by-step guide to help you with the migration process:

  1. Analyze the Monolith:
  • Understand the existing monolithic application’s architecture, functionality, and dependencies.
  • Identify the distinct functional areas or modules within the monolith that can be separated into microservices.

2. Define Microservices Architecture:

  • Design the target microservices architecture. Decide on the technologies, communication protocols (e.g., REST, gRPC), and infrastructure (e.g., Kubernetes, Docker) that you’ll use.

3. Identify Low-Risk Areas:

  • Start the migration with low-risk and isolated functional areas. Choose components that have limited dependencies on other parts of the monolith.

4. Build New Microservices:

  • Develop the new microservices to replicate the functionality of the selected components from the monolith. Make sure to create clear APIs for communication.

5. Setup API Gateway or Proxy:

  • To route traffic between the monolith and microservices, use an API gateway or proxy. This will allow you to gradually direct requests from the monolith to the corresponding microservices.

6. Incremental Deployment:

  • Deploy the new microservices alongside the monolith, but ensure they don’t handle critical functionality initially. Begin routing some user requests to the new services via the API gateway.

7. Monitor and Test:

  • Monitor the performance of the new microservices and gather feedback from users. Conduct thorough testing to ensure the new components work as expected.

8. Progressive Expansion:

  • As confidence grows in the new microservices, expand their responsibilities and offload more functionality from the monolith.

9. Database and Data Migration:

  • Handle database changes carefully. If the monolith and microservices share a database, implement a strategy to manage data migration without disruption.

10. Retire Monolith Components:

  • Over time, as more functionality is moved to microservices, decommission the corresponding components from the monolith.

11. Rinse and Repeat:

  • Continue the process, identifying new functional areas for migration and creating additional microservices.

12. Continuous Improvement:

  • As you migrate and refine the system, continuously look for opportunities to optimize and improve the microservices architecture.

Remember, migrating to microservices is a complex undertaking, and it requires careful planning, testing, and monitoring. Each organization’s journey will be unique based on their specific requirements, existing architecture, and resources. It’s essential to have a clear understanding of the monolithic application’s structure and dependencies to execute a successful migration using the Strangler Pattern.

Let’s walk through an example of a simple monolithic bookstore application and then implement the Strangler Pattern to gradually migrate it to a microservices architecture. In this example, we’ll have functionalities related to books, such as listing books, adding books to the inventory, and handling book orders.

Step 1: Create the Monolithic Bookstore

// MonolithicBookstore.cs

public class MonolithicBookstore
{
public void ListBooks()
{
// Monolithic logic to list books
// ...
}

public void AddBook(string bookTitle, string author, double price)
{
// Monolithic logic to add a book to the inventory
// ...
}

public void PlaceOrder(string bookTitle, int quantity)
{
// Monolithic logic to handle book orders
// ...
}
}

Step 2: Define Microservices Interfaces

// IBookListing.cs

public interface IBookListing
{
void ListBooks();
}

// IBookInventory.cs

public interface IBookInventory
{
void AddBook(string bookTitle, string author, double price);
}

// IOrderHandler.cs

public interface IOrderHandler
{
void PlaceOrder(string bookTitle, int quantity);
}

Step 3: Implement the Microservices

// BookListingMicroservice.cs

public class BookListingMicroservice : IBookListing
{
public void ListBooks()
{
// Microservice logic to list books
// ...
}
}

// BookInventoryMicroservice.cs

public class BookInventoryMicroservice : IBookInventory
{
public void AddBook(string bookTitle, string author, double price)
{
// Microservice logic to add a book to the inventory
// ...
}
}

// OrderHandlerMicroservice.cs

public class OrderHandlerMicroservice : IOrderHandler
{
public void PlaceOrder(string bookTitle, int quantity)
{
// Microservice logic to handle book orders
// ...
}
}

Step 4: Introduce the Strangler Proxy

// StranglerProxy.cs

public class StranglerProxy : IBookListing, IBookInventory, IOrderHandler
{
private MonolithicBookstore monolithicBookstore;
private IBookListing bookListingMicroservice;
private IBookInventory bookInventoryMicroservice;
private IOrderHandler orderHandlerMicroservice;

public StranglerProxy()
{
monolithicBookstore = new MonolithicBookstore();
bookListingMicroservice = new BookListingMicroservice();
bookInventoryMicroservice = new BookInventoryMicroservice();
orderHandlerMicroservice = new OrderHandlerMicroservice();
}

public void ListBooks()
{
// Route listing books request to the appropriate service
if (/* some condition to decide when to use microservice */)
{
bookListingMicroservice.ListBooks();
}
else
{
monolithicBookstore.ListBooks();
}
}

public void AddBook(string bookTitle, string author, double price)
{
// Route add book request to the appropriate service
if (/* some condition to decide when to use microservice */)
{
bookInventoryMicroservice.AddBook(bookTitle, author, price);
}
else
{
monolithicBookstore.AddBook(bookTitle, author, price);
}
}

public void PlaceOrder(string bookTitle, int quantity)
{
// Route order placement request to the appropriate service
if (/* some condition to decide when to use microservice */)
{
orderHandlerMicroservice.PlaceOrder(bookTitle, quantity);
}
else
{
monolithicBookstore.PlaceOrder(bookTitle, quantity);
}
}
}

Step 5: Gradual Migration in the Main Program

// MainProgram.cs

public class Program
{
static void Main(string[] args)
{
// Create an instance of the Strangler Proxy
IBookListing stranglerProxy = new StranglerProxy();
IBookInventory bookInventoryProxy = (IBookInventory)stranglerProxy;
IOrderHandler orderHandlerProxy = (IOrderHandler)stranglerProxy;

// Gradual migration - use the proxy for new requests
// (for simplicity, we assume that we switch to microservices after some time)
stranglerProxy.ListBooks();
bookInventoryProxy.AddBook("Sample Book", "John Doe", 29.99);
orderHandlerProxy.PlaceOrder("Sample Book", 5);
}
}

In this example, we have defined interfaces for each microservice representing the distinct functional areas (listing books, handling inventory, and processing orders). We then implement these interfaces with separate microservice classes.

The StranglerProxy acts as the API gateway or proxy that routes requests to either the monolithic bookstore or the microservices based on some condition (commented as /* some condition to decide when to use microservice */). In a real-world scenario, this condition would be more sophisticated, based on business rules, load, or other factors.

The MainProgram demonstrates the gradual migration by using the StranglerProxy for new requests, while some existing code still uses the monolithic bookstore. As you migrate more functionality, you can replace the conditions in the StranglerProxy to route all requests to the respective microservices.

Conclusion:

In conclusion, the Strangler Pattern is a powerful and gradual migration strategy used to transition from a monolithic architecture to a microservices architecture. It allows organizations to modernize their legacy systems in a controlled and iterative manner, reducing risks and minimizing disruptions to end-users.

By identifying distinct functional areas within the monolith, creating equivalent microservices for each area, and gradually routing traffic to the new services, the Strangler Pattern enables a smooth transition. It ensures that essential business functionality is maintained while incrementally improving the system with modern technology.

The Strangler Pattern’s step-by-step implementation involves creating a proxy or API gateway that intelligently routes requests to the monolith or microservices based on certain conditions. As more functionality is migrated to microservices and confidence in the new architecture grows, the conditions are adjusted, and eventually, all requests are directed to the microservices.

Remember, the success of implementing the Strangler Pattern depends on careful planning, continuous testing, monitoring, and iteration. It’s essential to consider factors like the size of the application, dependencies between components, and the organization’s specific requirements when deciding on the migration strategy. With proper execution, the Strangler Pattern can lead to a successful and efficient transformation from monolithic to microservices architecture.

--

--

Waqas Ahmed

Microsoft Azure Enthusiast ☁ | Principal Software Engineer | Angular | React.Js | .Net Core | .Net | Azure | Micro Services 👉 https://bit.ly/3AhEyOz