Skip to content

Commit 6a000ee

Browse files
feat: add NativeAotSaga sample demonstrating compensating transactions
Implements the Saga pattern (flight, hotel, car rental booking with LIFO compensation on failure) as a NativeAOT-compatible Restate service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a4cb19a commit 6a000ee

File tree

6 files changed

+197
-0
lines changed

6 files changed

+197
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace NativeAotSaga;
4+
5+
[JsonSerializable(typeof(TripBookingRequest))]
6+
[JsonSerializable(typeof(TripBookingResult))]
7+
[JsonSerializable(typeof(FlightRequest))]
8+
[JsonSerializable(typeof(HotelRequest))]
9+
[JsonSerializable(typeof(CarRentalRequest))]
10+
[JsonSourceGenerationOptions(
11+
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
12+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
13+
internal partial class AppJsonContext : JsonSerializerContext;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace NativeAotSaga;
2+
3+
/// <summary>
4+
/// Simulates external booking APIs (flight, hotel, car rental).
5+
/// Car rental has a 20% random failure rate to demonstrate saga compensation.
6+
/// </summary>
7+
public static class BookingApi
8+
{
9+
public static Task<string> BookFlight(FlightRequest request)
10+
{
11+
Console.WriteLine(
12+
$" [API] Booking flight {request.From} -> {request.To} on {request.Date}"
13+
);
14+
return Task.FromResult($"FL-{Guid.NewGuid().ToString()[..8].ToUpperInvariant()}");
15+
}
16+
17+
public static Task CancelFlight(string confirmationId)
18+
{
19+
Console.WriteLine($" [API] Cancelling flight {confirmationId}");
20+
return Task.CompletedTask;
21+
}
22+
23+
public static Task<string> BookHotel(HotelRequest request)
24+
{
25+
Console.WriteLine(
26+
$" [API] Booking hotel in {request.City} from {request.CheckIn} to {request.CheckOut}"
27+
);
28+
return Task.FromResult($"HT-{Guid.NewGuid().ToString()[..8].ToUpperInvariant()}");
29+
}
30+
31+
public static Task CancelHotel(string confirmationId)
32+
{
33+
Console.WriteLine($" [API] Cancelling hotel {confirmationId}");
34+
return Task.CompletedTask;
35+
}
36+
37+
public static Task<string> BookCarRental(CarRentalRequest request)
38+
{
39+
Console.WriteLine(
40+
$" [API] Booking car in {request.City} from {request.PickUp} to {request.DropOff}"
41+
);
42+
43+
// Simulate occasional failures to demonstrate compensation
44+
if (Random.Shared.Next(100) < 20)
45+
throw new InvalidOperationException("Car rental service temporarily unavailable");
46+
47+
return Task.FromResult($"CR-{Guid.NewGuid().ToString()[..8].ToUpperInvariant()}");
48+
}
49+
50+
public static Task CancelCarRental(string confirmationId)
51+
{
52+
Console.WriteLine($" [API] Cancelling car rental {confirmationId}");
53+
return Task.CompletedTask;
54+
}
55+
}

samples/NativeAotSaga/Models.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace NativeAotSaga;
2+
3+
/// <summary>Request to book a complete trip.</summary>
4+
public record TripBookingRequest(
5+
string TripId,
6+
string UserId,
7+
FlightRequest Flight,
8+
HotelRequest Hotel,
9+
CarRentalRequest CarRental
10+
);
11+
12+
/// <summary>Result of a completed trip booking.</summary>
13+
public record TripBookingResult(
14+
string TripId,
15+
string? FlightConfirmation,
16+
string? HotelConfirmation,
17+
string? CarRentalConfirmation
18+
);
19+
20+
public record FlightRequest(string From, string To, DateOnly Date);
21+
22+
public record HotelRequest(string City, DateOnly CheckIn, DateOnly CheckOut);
23+
24+
public record CarRentalRequest(string City, DateOnly PickUp, DateOnly DropOff);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<IsPackable>false</IsPackable>
6+
<PublishAot>true</PublishAot>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="../../src/Restate.Sdk/Restate.Sdk.csproj"/>
11+
<ProjectReference Include="../../src/Restate.Sdk.Generators/Restate.Sdk.Generators.csproj"
12+
OutputItemType="Analyzer"
13+
ReferenceOutputAssembly="false"/>
14+
</ItemGroup>
15+
16+
</Project>

samples/NativeAotSaga/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using NativeAotSaga;
2+
using Restate.Sdk.Generated;
3+
using Restate.Sdk.Hosting;
4+
5+
// NativeAOT-compatible Restate endpoint demonstrating the Saga pattern.
6+
// Publish with: dotnet publish -c Release
7+
await RestateHost
8+
.CreateBuilder()
9+
.WithPort(9087)
10+
.BuildAot(services => services.AddRestateGenerated(AppJsonContext.Default))
11+
.RunAsync();
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Restate.Sdk;
2+
3+
namespace NativeAotSaga;
4+
5+
/// <summary>
6+
/// Demonstrates the Saga pattern (compensating transactions) using Restate,
7+
/// compiled with NativeAOT.
8+
/// </summary>
9+
[Service]
10+
public sealed class TripBookingService
11+
{
12+
[Handler]
13+
public async Task<TripBookingResult> Book(Context ctx, TripBookingRequest request)
14+
{
15+
var compensations = new List<Func<Context, Task>>();
16+
17+
try
18+
{
19+
// Step 1: Book flight
20+
ctx.Console.Log($"Booking flight for trip {request.TripId}...");
21+
var flightConfirmation = await ctx.Run(
22+
"book-flight",
23+
() => BookingApi.BookFlight(request.Flight)
24+
);
25+
26+
compensations.Add(
27+
async (c) =>
28+
{
29+
c.Console.Log($"Compensating: cancelling flight {flightConfirmation}");
30+
await c.Run("cancel-flight", () => BookingApi.CancelFlight(flightConfirmation));
31+
}
32+
);
33+
34+
// Step 2: Book hotel
35+
ctx.Console.Log($"Booking hotel for trip {request.TripId}...");
36+
var hotelConfirmation = await ctx.Run(
37+
"book-hotel",
38+
() => BookingApi.BookHotel(request.Hotel)
39+
);
40+
41+
compensations.Add(
42+
async (c) =>
43+
{
44+
c.Console.Log($"Compensating: cancelling hotel {hotelConfirmation}");
45+
await c.Run("cancel-hotel", () => BookingApi.CancelHotel(hotelConfirmation));
46+
}
47+
);
48+
49+
// Step 3: Book car rental (may fail — demonstrates compensation)
50+
ctx.Console.Log($"Booking car rental for trip {request.TripId}...");
51+
var carConfirmation = await ctx.Run(
52+
"book-car-rental",
53+
() => BookingApi.BookCarRental(request.CarRental),
54+
RetryPolicy.FixedAttempts(3)
55+
);
56+
57+
ctx.Console.Log($"Trip {request.TripId} booked successfully!");
58+
return new TripBookingResult(
59+
request.TripId,
60+
flightConfirmation,
61+
hotelConfirmation,
62+
carConfirmation
63+
);
64+
}
65+
catch (TerminalException)
66+
{
67+
ctx.Console.Log(
68+
$"Trip {request.TripId} failed. Running {compensations.Count} compensation(s)..."
69+
);
70+
71+
for (var i = compensations.Count - 1; i >= 0; i--)
72+
await compensations[i](ctx);
73+
74+
ctx.Console.Log($"Trip {request.TripId} fully compensated.");
75+
throw;
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)