Saga Orchestration Pattern in Microservices with a Travel Plan Example in C#
In a microservices architecture, long-running business processes often require transactions across multiple services. This presents challenges because each service manages its data and runs independently, meaning traditional ACID transactions don’t work across services.
The Saga Pattern provides a way to handle distributed transactions by dividing them into smaller, isolated transactions across services. If something goes wrong, compensating actions are triggered to maintain consistency.
In this blog, we’ll explore the Saga Orchestration Pattern using an example of booking a travel plan that includes flight, hotel, and car rental services in a microservices environment, written in C#.
Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.
Key Concepts of the Saga Pattern
- Orchestration: One central component (the orchestrator) coordinates the entire business process by invoking each service in the transaction and handling failures.
- Compensating Transactions: In case of a failure, compensating actions (i.e., undo operations) are triggered to roll back changes that were made by services in the transaction.
- Long-Running Transactions: Sagas are useful for processes that take time and involve multiple steps across services.
Travel Plan Example
Consider a travel plan where a user books:
- A flight
- A hotel
- A car rental
Each of these actions is handled by a different microservice, and the entire booking process should either succeed or be compensated in case of failure.
Services Involved:
- Flight Service: Responsible for booking a flight.
- Hotel Service: Responsible for booking a hotel.
- Car Rental Service: Responsible for renting a car.
Saga Orchestration Pattern Implementation in C#
We’ll implement this using:
- Orchestrator: Coordinates the entire saga.
- Service APIs: Individual microservices for flight, hotel, and car rental.
- Compensation Logic: Rollback actions to undo operations if one of the services fails.
Step 1: Define the Orchestrator
The Orchestrator is responsible for invoking the services in sequence and handling failures by calling compensating transactions.
public class TravelPlanOrchestrator
{
private readonly IFlightService _flightService;
private readonly IHotelService _hotelService;
private readonly ICarRentalService _carRentalService;
public TravelPlanOrchestrator(IFlightService flightService, IHotelService hotelService, ICarRentalService carRentalService)
{
_flightService = flightService;
_hotelService = hotelService;
_carRentalService = carRentalService;
}
public async Task<bool> BookTravelPlanAsync(TravelPlanRequest request)
{
try
{
// Step 1: Book flight
var flightBookingResult = await _flightService.BookFlightAsync(request.FlightDetails);
if (!flightBookingResult)
{
Console.WriteLine("Failed to book flight.");
return false;
}
// Step 2: Book hotel
var hotelBookingResult = await _hotelService.BookHotelAsync(request.HotelDetails);
if (!hotelBookingResult)
{
Console.WriteLine("Failed to book hotel. Compensating flight booking...");
await _flightService.CancelFlightAsync(request.FlightDetails);
return false;
}
// Step 3: Rent car
var carRentalResult = await _carRentalService.RentCarAsync(request.CarDetails);
if (!carRentalResult)
{
Console.WriteLine("Failed to rent car. Compensating hotel and flight bookings...");
await _hotelService.CancelHotelAsync(request.HotelDetails);
await _flightService.CancelFlightAsync(request.FlightDetails);
return false;
}
Console.WriteLine("Travel plan booked successfully!");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Exception occurred: {ex.Message}");
return false;
}
}
}
Here, the orchestrator coordinates the travel booking process:
- Book Flight: Calls the flight service to book a flight.
- Book Hotel: Calls the hotel service to book a hotel if the flight booking succeeds.
- Rent Car: Calls the car rental service if both the flight and hotel bookings succeed.
In case of failure:
- The orchestrator rolls back previous successful steps by calling compensating transactions (e.g., cancel the flight or hotel booking).
Step 2: Define the Microservices
Flight Service
public interface IFlightService
{
Task<bool> BookFlightAsync(FlightDetails flightDetails);
Task<bool> CancelFlightAsync(FlightDetails flightDetails);
}
public class FlightService : IFlightService
{
public async Task<bool> BookFlightAsync(FlightDetails flightDetails)
{
// Simulate booking a flight
Console.WriteLine($"Booking flight from {flightDetails.Origin} to {flightDetails.Destination}");
return await Task.FromResult(true); // Simulate success
}
public async Task<bool> CancelFlightAsync(FlightDetails flightDetails)
{
// Simulate flight booking cancellation
Console.WriteLine($"Cancelling flight from {flightDetails.Origin} to {flightDetails.Destination}");
return await Task.FromResult(true); // Simulate success
}
}
Hotel Service
public interface IHotelService
{
Task<bool> BookHotelAsync(HotelDetails hotelDetails);
Task<bool> CancelHotelAsync(HotelDetails hotelDetails);
}
public class HotelService : IHotelService
{
public async Task<bool> BookHotelAsync(HotelDetails hotelDetails)
{
// Simulate booking a hotel
Console.WriteLine($"Booking hotel in {hotelDetails.City}");
return await Task.FromResult(true); // Simulate success
}
public async Task<bool> CancelHotelAsync(HotelDetails hotelDetails)
{
// Simulate hotel booking cancellation
Console.WriteLine($"Cancelling hotel booking in {hotelDetails.City}");
return await Task.FromResult(true); // Simulate success
}
}
Car Rental Service
public interface ICarRentalService
{
Task<bool> RentCarAsync(CarDetails carDetails);
Task<bool> CancelCarRentalAsync(CarDetails carDetails);
}
public class CarRentalService : ICarRentalService
{
public async Task<bool> RentCarAsync(CarDetails carDetails)
{
// Simulate renting a car
Console.WriteLine($"Renting car: {carDetails.CarType} in {carDetails.Location}");
return await Task.FromResult(true); // Simulate success
}
public async Task<bool> CancelCarRentalAsync(CarDetails carDetails)
{
// Simulate car rental cancellation
Console.WriteLine($"Cancelling car rental in {carDetails.Location}");
return await Task.FromResult(true); // Simulate success
}
}
Step 3: Travel Plan Request Model
This is the request model that will be used to send all the necessary details for booking a travel plan.
public class TravelPlanRequest
{
public FlightDetails FlightDetails { get; set; }
public HotelDetails HotelDetails { get; set; }
public CarDetails CarDetails { get; set; }
}
public class FlightDetails
{
public string Origin { get; set; }
public string Destination { get; set; }
public DateTime DepartureDate { get; set; }
}
public class HotelDetails
{
public string City { get; set; }
public DateTime CheckInDate { get; set; }
public DateTime CheckOutDate { get; set; }
}
public class CarDetails
{
public string CarType { get; set; }
public string Location { get; set; }
public DateTime RentalDate { get; set; }
}
Step 4: Running the Orchestrator
Here’s how to run the travel plan orchestration with a travel request.
public static async Task Main(string[] args)
{
var flightService = new FlightService();
var hotelService = new HotelService();
var carRentalService = new CarRentalService();
var orchestrator = new TravelPlanOrchestrator(flightService, hotelService, carRentalService);
var travelRequest = new TravelPlanRequest
{
FlightDetails = new FlightDetails
{
Origin = "New York",
Destination = "London",
DepartureDate = DateTime.Now.AddDays(5)
},
HotelDetails = new HotelDetails
{
City = "London",
CheckInDate = DateTime.Now.AddDays(5),
CheckOutDate = DateTime.Now.AddDays(10)
},
CarDetails = new CarDetails
{
CarType = "SUV",
Location = "London",
RentalDate = DateTime.Now.AddDays(5)
}
};
var success = await orchestrator.BookTravelPlanAsync(travelRequest);
Console.WriteLine($"Travel booking {(success ? "succeeded" : "failed")}");
}
Conclusion
The Saga Orchestration Pattern is ideal for handling distributed transactions in microservices, especially when operations need to maintain consistency across services. In this example, we demonstrated how to implement a travel plan booking system using the Saga pattern with an orchestrator in C#.
- The Orchestrator coordinates calls to multiple services (flight, hotel, and car rental) and handles failures with compensating transactions.
- Each microservice (flight, hotel, car rental) performs its operations independently but is orchestrated as part of a larger workflow.