Skip to main content
Version: 0.7.0

Distributed Transactions (SAGA)

Sharkable provides a SAGA-pattern distributed transaction system: a sequence of steps where each step has a forward action and a compensating (rollback) action. If any step fails, previously-completed steps are compensated in reverse order.

Quick Start

1. Define Steps

public class CreateOrderStep : ISagaStep
{
public async Task<SagaResult> ExecuteAsync(CancellationToken ct)
{
// create order in database
return new SagaResult(true);
}

public async Task CompensateAsync(CancellationToken ct)
{
// delete order (rollback)
}
}

2. Assemble Saga

public class CreateOrderSaga : Saga
{
public CreateOrderSaga()
{
AddStep(new CreateOrderStep());
AddStep(new DeductInventoryStep());
AddStep(new ChargePaymentStep());
AddStep(new SendNotificationStep());
}
}

3. Execute

app.MapPost("/orders", async (CreateOrderSaga saga, SagaExecutor executor) =>
{
var result = await executor.ExecuteAsync($"order:{orderId}", saga);
return result.Success ? Results.Ok() : Results.Problem(result.Error);
});

How It Works

Execute step 0 → OK → save progress → step 1 → OK → save progress → step 2 → FAIL

compensate step 1 ← compensate step 0
  1. Acquire distributed lock (prevent concurrent saga execution)
  2. Load progress (crash recovery — resume from last completed step)
  3. Execute steps sequentially, saving progress after each success
  4. On failure: compensate completed steps in reverse order
  5. Release lock

Crash Recovery

Progress is saved to ISagaStore after each successful step. If the process crashes mid-saga, the next execution loads the last completed step and resumes from the next one. Already-completed steps are NOT re-executed.

Distributed Store

The default MemorySagaStore is in-process only — suitable for development. For production, install Redis store:

dotnet add package Sharkable.Cache.Redis
services.AddSharkableRedis("localhost:6379");
// ISagaStore auto-swaps to RedisSagaStore with distributed locking

With custom key prefixes:

services.AddSharkableRedis("localhost:6379", opt =>
{
opt.SagaLockPrefix = "myapp:saga:lock:";
opt.SagaProgressPrefix = "myapp:saga:progress:";
});

Or implement ISagaStore with your own database:

builder.Services.AddShark(opt =>
{
opt.SagaStoreFactory = sp => new MyDbSagaStore();
});

Data Sharing Between Steps

Steps share state via Saga.State.Data dictionary:

public class CreateOrderStep : ISagaStep
{
public async Task<SagaResult> ExecuteAsync(CancellationToken ct)
{
var orderId = Guid.NewGuid().ToString();
State.Data["orderId"] = orderId; // available to subsequent steps
return new SagaResult(true);
}
}

AOT Support

Fully AOT-compatible. No reflection, no dynamic.

Configuration

The SagaExecutor exposes configurable timeouts:

// Configure via DI registration or instance property
var executor = new SagaExecutor(store, logger)
{
LockTtl = TimeSpan.FromMinutes(5), // Distributed lock duration
LockRenewalInterval = TimeSpan.FromMinutes(1), // Background renewal cadence
CompensationTimeout = TimeSpan.FromSeconds(60), // Compensation step timeout
};
PropertyDefaultPurpose
LockTtl5 minHow long the distributed lock is held
LockRenewalIntervalLockTtl/3How often the lock is renewed during execution
CompensationTimeout60 sTimeout for the entire compensation (rollback) phase — uses a dedicated CTS not linked to the execution token, ensuring the saga always has a chance to roll back even after cancellation