In this series of post, we will build an example for Saga Pattern. We will be using the Choreography Pattern for the example. If you are interested to read more on Saga, please check out my article on Introduction to Saga Pattern. This example would use RabbitMq as the message broker.
To demonstrate the pattern, let us consider the following scenario. We have a online shopping platform, which is run on microservice architecture, involving 3 services (simplifying for demonstrative purposes) – Order Service, Inventory Service, Payment Service. We will build the example step by step, developing the core API end points first, before building the Saga related components.
Developing Individual Services (and thier Endpoints)
Order Service
The Order Service is responsible for handling orders. It exposes folloing end points.
Service APIs
1. Create_Order(Customer_Id, Item_Qty_Mapping)
Description
Creates a new Order in Pending State
Parameters
Customer_Id (Guid) : Unique Id of the Customer for whom Order is being generated
Item_Qty_Mapping (Dictionary(Guid,Int)) : A dictionary collection mapping unique Id of Item to the quantity
Returns
200 : Order Id of newly created order along with order details
2. Get_All
Description
Retrieve list of all orders in the system along with their current state
Returns
List of Order Details
3. Get_By_Id
Description
Retrieves details of given Order Id
Parameters
OrderId (Guid) : Unique Id of the Order to be retrieved
Returns
200: Details of Order
Now that we have defined the requirements of the Order Service’s end points, let us go ahead and create them. Create a Web API and create end points as follows
app.MapPost("/createorder", (CreateOrderRequest orderRequest,
[FromServices]ILogger<Program> logger,
[FromServices]IOrderService orderService,
[FromServices]IPublishEndpoint publishEndPoint) =>
{
logger.LogInformation($"OrderService.CreateOrder started with ");
var currentOrder = orderService.CreateOrder(new()
{
CustomerId = orderRequest.CustomerId,
OrderItems = orderRequest.Items,
});
return Results.Ok(new CreateOrderResponse
{
OrderId = currentOrder.Id,
CustomerId = currentOrder.CustomerId,
State = currentOrder.State,
});
});
app.MapGet("/getall", ([FromServices]IOrderService orderService, [FromServices]ILogger<Program> logger) =>
{
logger.LogInformation($"Retrieving all order items");
var result = orderService.GetAll();
var response = result.Select(x => new GetAllOrderResponse
{
OrderId = x.Id,
CustomerId = x.CustomerId,
State = x.State
});
return response;
});
app.MapGet("/getbyid", (Guid orderId, [FromServices] IOrderService orderService, [FromServices] ILogger<Program> logger) =>
{
logger.LogInformation($"Retrieving order info for #{orderId}");
try
{
var result = orderService.GetById(orderId);
return Results.Ok(new GetOrderByIdResponse
{
OrderdId = result.Id,
CustomerId = result.CustomerId,
State = result.State
});
}
catch(Exception ex)
{
logger.LogError($"Error Retrieving Order #{orderId} - [{ex.Message}]");
return Results.NotFound("OrderId not found !!");
}
});
The OrderService
is defined as
publicinterfaceIOrderService
{
Order GetById(Guid orderId);
Order CreateOrder(Order order);
IEnumerable<Order> GetAll();
}
publicclassOrderService : IOrderService
{
// Implementing Order service
}
I will leave out the implementation details of Service and Repository out of this example, but if anyone is interested, please do check out the entire source code in my Github. We will breifly revist the Service implementation later as we build our Saga bits around it.
Similarly, let us go ahead and define our endpoints for Inventory Service and Payment Service.
Inventory Service
Inventory Service, as name suggests, handles the inventory. It maintains a central table for all available inventory. Additionally, it maintains a separate table for reserved inventory for handling items reserved for orders which are still not completed.
Endpoint required are as follows
Service APIs
1. GetStock
Description
Retrieves information on all available inventory
Returns
200 : Collection of all inventory along with their stock details
2. GetReservedStock
Description
Retrieves information all reserved stock
Returns
200 : Colleciton of all reserved inventory, along with their stock and corresponding order details.
Let us go ahead and define the code for the same.
app.MapGet("/getstock", ([FromServices] IInventoryService inventoryService,
[FromServices]ILogger<Program> logger) =>{
logger.LogInformation($"Retrieving current stock status");
var stock = inventoryService.GetStock();
return Results.Ok(stock.Select(x => new GetStockStatusResponse
{
ItemId = x.Id,
Name = x.Name,
Quantity = x.Quantity
}));
});
app.MapGet("/getreservedstock", ([FromServices] IInventoryService inventory,
[FromServices]ILogger<Program> logger) =>
{
logger.LogInformation($"Retrieving reserved stock status");
var stock = inventory.GetReservedStock();
return Results.Ok(stock.GroupBy(x => x.OrderId).Select(x =>
new GetReservedStockStatusResponse
{
OrderId = x.Key,
ReservedStockItems = x.Select(c=> new ReservedStockItemResponse
{
ItemId= c.Id,
Quantity= c.Quantity,
State = c.State
})
}));
});
Inventory Service can be defined as follows
publicinterfaceIInventoryService
{
IEnumerable<Inventory> GetStock();
IEnumerable<OrderItem> GetReservedStock();
}
publicclassInventoryService : IInventoryService
{
// Implementing Inventory Service
}
Payment Service
Finally, Payment service can be defined with following end points.
Service APIs
1. MakePayment
Description
Creates a new Payment Entry detail
Parameters
OrderId (Guid) : Order Id against which payment is being done.
CustomerId (Guid) : Customer who makes the payment
Amount (double) : Amount to be paid
Returns
200 : On sucess returns Payment details
2. CancelPayment
Description
Cancels a particular (pending) payment
Parameters
OrderId (Guid) : Order Id against which Payment has to be cancelled.
CustomerId (Guid) : Customer whose's payment has to be marked as cancelled
Returns
200 : Returns details of cancelled payment
3. ConfirmPayment
Description
Confirms a particular (pending) payment
Parameters
OrderId (Guid) : Order Id against which Payment has to be Confirmed.
CustomerId (Guid) : Customer whose's payment has to be marked as Confirmed
Returns
200 : Returns details of confirmed payment
4. GetAllPayments
Description
Retrieves information on all payments in the system
Returns
200: Returns collection of all payments along with thier details including status
In the above case, it can be observed the payment is a 2-step process. First the payment is done, and then it has to be confirmed (by the bank probably) or Cancelled in a separate step.
The API end points can be defined as
app.MapPost("/makepayment", (MakePaymentRequest paymentRequest,
[FromServices] ILogger<Program> logger,
[FromServices] IPaymentService paymentService) =>
{
logger.LogInformation($"PaymentService.MakePayment started with ");
var payment = new CustomerPayment
{
CustomerId = paymentRequest.CustomerId,
Amount = paymentRequest.Amount,
OrderId = paymentRequest.OrderId,
State = PaymentState.Pending,
};
paymentService.MakePayment(payment);
});
app.MapPost("/confirmpayment", (ConfirmPaymentRequest paymentRequest,
[FromServices] ILogger<Program> logger,
[FromServices] IPaymentService paymentService) =>
{
logger.LogInformation($"Confirm Payment for OrderId #{paymentRequest.OrderId}");
paymentService.ConfirmPayment(paymentRequest.OrderId);
});
app.MapPost("/cancelpayment", (CancelPaymentRequest paymentRequest,
[FromServices] ILogger<Program> logger,
[FromServices] IPaymentService paymentService) =>
{
logger.LogInformation($"Cancel Payment for OrderId #{paymentRequest.OrderId}");
paymentService.CancelPayment(paymentRequest.OrderId);
});
app.MapGet("/getallpayment", ([FromServices] IPaymentService paymentService,
[FromServices] ILogger<Program> logger) =>
{
logger.LogInformation($"Retrieving all payment information");
var payments = paymentService.GetAll();
return Results.Ok(payments.Select(x=> new GetAllPaymentResponse
{
Amount = x.Amount,
OrderId = x.OrderId,
State = x.State,
CustomerId = x.CustomerId,
Id = x.Id
}));
});
The PaymentService can be defined as
publicinterfaceIPaymentService
{
void MakePayment(CustomerPayment customerPayment);
void ConfirmPayment(Guid orderId);
void CancelPayment(Guid orderId);
IEnumerable<CustomerPayment> GetAll();
}
publicclassPaymentService : IPaymentService
{
// Payment Service Implmentation
}
So far we have created the building blocks for demonstrating our Saga pattern implemention. In this part of series, we created 3 services which would be used in our example.
In the next part of this series, we will add our message broker to the implementation and how we can use it to implement the Saga Pattern.