Implementing Saga 001 : Building Services

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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s