Apply Strangler Pattern To Decompose Legacy System Into Microservices: Part 1
Explore domain-specific examples of how to break down a legacy system into microservices architecture for a POS/e-commerce system.
Join the DZone community and get the full member experience.
Join For FreeMany sources provide explanations of microservices in a general context, but there is a lack of domain-specific examples. Newcomers or those unsure of where to begin may find it challenging to grasp how to transition their legacy systems into a microservices architecture. This guide is primarily intended for individuals who are struggling to initiate their migration efforts, and it offers business-specific examples to aid in understanding the process.
There is another pattern I wanted to talk about - the Strangler Pattern - which is a migration pattern used to transition from a legacy system to a new system incrementally while minimizing risk.
Let's take an example of a legacy grocery billing system. Now it is time to upgrade to microservice architecture to leverage its benefits.
Strangler is a pattern that is for gradually decommissioning the old system while incrementally developing the new system. That way, users can start using the newer system sooner rather than waiting for the whole system migration to be complete.
In this first article, I am going to focus on microservices needed by a grocery store. For example, consider a scenario where you currently have a legacy system for a grocery store, and you're interested in upgrading it to a microservices architecture and migrating it to the cloud.
Overview of Grocery Store Legacy System
First, the modules a grocery store online might have are:
- Shopping cart service
- Payment processing service with refund
- Inventory management service: The quantity of the product should be subtracted when the item is sold, and added back when the order is refunded.
As per the Strangler Pattern, you should be able to replace one module with a new microservice while you continue using the other modules until newer services are ready.
Here, you can first replace the shopping cart with a newer service. Since the shopping cart service is dependent on a payment processing service, you need to develop that one, too.
Assume that we are going to develop these services incrementally. For demonstration purposes, I will just focus on the above three services only. But in a real-world scenario, you might need the other services as illustrated below to complete the whole e-commerce site for the grocery store:
Now let's consider the essential model classes and operations required for each service.
For the shopping cart service, you'll need the following model classes and operations: product, product category, items added to the shopping cart, shopping cart, and order. It can be structured as follows:
Shopping Cart Service
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public Category ProductCategory { get; set; }
}
public class Category
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class ShoppingCartItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
public class ShoppingCart
{
public Guid Id { get; set; }
public List<ShoppingCartItem> Items { get; set; }
public Customer Customer { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Order
{
public Guid Id { get; set; }
public List<ShoppingCartItem> Items { get; set; }
public Customer Customer { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
Ideally, you should create a shared project to house all models and interfaces. It's essential to begin by identifying the necessary models and operations first.
When considering the operations that a customer can perform in the shopping cart, it typically involves just one primary action, CreateOrder
, when adding items to the cart. However, other operations, such as payment processing, refunds, and inventory adjustments, should be implemented as separate microservices. This modular approach allows for more flexibility and scalability in managing different aspects of the business process.
public class BillingService : IBillingService
{
public Order CreateOrder(Customer customer, List<ShoppingCartItem> items)
{
return new Order
{
Id = Guid.NewGuid(), //Create a new order id
Items = items,
Customer = customer,
TotalAmount = CalculateTotalAmount(items),
CreatedAt = DateTime.Now
};
}
private decimal CalculateTotalAmount(List<ShoppingCartItem> items)
{
decimal totalAmount = 0;
foreach (var item in items)
{
totalAmount += item.Product.Price * item.Quantity;
}
return totalAmount;
}
}
Ideally in the shared project, you have to create an interface for IBillingService
. It should look as below:
public interface IBillingService
{
public Order CreateOrder(Customer customer, List<ShoppingCartItem> items);
}
Now you can unit-test the CreateOrder
operation.
In the real world, it's common practice to create an IBillingRepository
to save orders in the database. This repository should encompass methods for storing orders in a database, or you can opt to use a downstream service to handle the order creation process.
I won't be addressing user authentication, security, hosting, monitoring, proxy, and other related topics in this discussion, as they are distinct subjects. My primary focus remains on the design aspects of microservices tailored to your specific requirements.
After creating the shopping cart, the next step involves customer payment. Let's proceed by creating the Payment Service project and its associated models.
Payment Processing Service
public class Payment
{
public Guid Id { get; set; }
public decimal Amount { get; set; }
public PaymentStatus Status { get; set; }
public DateTime PaymentDate { get; set; }
public PaymentMethod PaymentMethod { get; set; }
}
public enum PaymentStatus
{
Pending,
Approved,
Declined,
}
public enum PaymentMethod
{
CreditCard,
DebitCard,
PayPal,
}
public class Receipt
{
public Guid Id { get; set; }
public Order Order { get; set; }
public decimal TotalAmount { get; set; }
public DateTime IssuedDate { get; set; }
}
public class PaymentService : IPaymentService
{
private PaymentGateway paymentGateway;
public PaymentService()
{
this.paymentGateway = new PaymentGateway();
}
public Payment MakePayment(decimal amount, PaymentMethod paymentMethod, string paymentDetails)
{
// In a real system, you would handle the payment details and validation before calling the payment gateway.
return paymentGateway.ProcessPayment(amount, paymentMethod, paymentDetails);
}
}
public class ReceiptService : IReceiptService
{
public Receipt GenerateReceipt(Order order)
{
var receipt = new Receipt
{
Id = Guid.NewGuid(),
Order = order,
TotalAmount = order.TotalAmount,
IssuedDate = DateTime.Now
};
return receipt;
}
}
In this Service project, you have to create and implement the following interfaces:
public Interface IPaymentService
{
public Payment MakePayment(decimal amount, PaymentMethod paymentMethod, string paymentDetails);
}
public Interface IReceiptService
{
public Receipt GenerateReceipt(Order order);
}
public Interface IPaymentRepository
{
public Payment ProcessPayment(decimal amount, PaymentMethod paymentMethod, string paymentDetails)
}
public class PaymentGateway : IPaymentRepository
{
public Payment ProcessPayment(decimal amount, PaymentMethod paymentMethod, string paymentDetails)
{
// Simplified payment processing logic for demonstration
var payment = new Payment
{
Id = Guid.NewGuid(),
Amount = amount,
Status = PaymentStatus.Pending,
PaymentDate = DateTime.Now,
PaymentMethod = paymentMethod
};
// In a real system, you would connect to a payment gateway and process the payment, updating the payment status accordingly.
// For example, you might use an external payment processing library or API to handle the transaction.
// Simulating a successful payment here for demonstration purposes.
payment.Status = PaymentStatus.Approved;
return payment;
}
}
With all of those services created, we can easily decommission the shopping cart with the new system (assuming that you have a new user interface also done in parallel).
Next, we must address inventory management following the placement of an order. The Inventory Management Service is responsible for restocking when a purchase order is created. The structure of this service project will appear as follows:
Inventory Management Service
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int QuantityInStock { get; set; }
public Category ProductCategory { get; set; }
}
public class Category
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Supplier
{
public Guid Id { get; set; }
public string Name { get; set; }
public string ContactEmail { get; set; }
}
public class PurchaseOrder
{
public Guid Id { get; set; }
public Supplier Supplier { get; set; }
public List<PurchaseOrderItem> Items { get; set; }
public DateTime OrderDate { get; set; }
public bool IsReceived { get; set; }
}
public class PurchaseOrderItem
{
public Product Product { get; set; }
public int QuantityOrdered { get; set; }
public decimal UnitPrice { get; set; }
}
public interface IInventoryManagementService
{
void ReceivePurchaseOrder(PurchaseOrder purchaseOrder);
void SellProduct(Product product, int quantitySold);
}
public class InventoryManagementService : IInventoryManagementService
{
public void ReceivePurchaseOrder(PurchaseOrder purchaseOrder)
{
if (purchaseOrder.IsReceived)
{
throw new InvalidOperationException("The order is already placed.");
}
foreach (var item in purchaseOrder.Items)
{
item.Product.QuantityInStock += item.QuantityOrdered;
}
purchaseOrder.IsReceived = true;
}
public void SellProduct(Product product, int quantitySold)
{
if (product.QuantityInStock < quantitySold)
{
throw new InvalidOperationException("Item not in stock.");
}
product.QuantityInStock -= quantitySold;
}
}
As I mentioned, this guide is primarily intended for individuals who are struggling to initiate their migration efforts, and it offers business-specific examples to aid in understanding the process.
I trust that this article has provided valuable insights on how to initiate your migration project within a microservices architecture. If you are working on a grocery or any online shopping cart system, this information should be particularly useful to you. I hope you can take it from here. In my next article, I will present another domain-specific example, as you can always explore more general information on microservices elsewhere.
Opinions expressed by DZone contributors are their own.
Comments