Skip to main content
SEMastery

Implementing the Saga Pattern With Wolverine in .NET

Learn the saga pattern in .NET with Wolverine: stateful sagas, Start and Handle methods, timeouts, and compensation. Simple words, examples, and diagrams.

13 min readUpdated April 10, 2026

A wedding planner for your code

Think about a big Indian wedding. There are many small jobs that must happen in order. Book the hall. Order the food. Hire the band. Send the cards. Arrange the cars. No single person does all of this at once. Instead, there is a wedding planner.

The planner keeps a notebook. After the hall is booked, the planner ticks that off and calls the caterer. After the food is confirmed, the planner ticks that off too and calls the band. The planner waits patiently between calls. Some jobs take days. The planner does not forget where things stand, because everything is written in the notebook.

And here is the important part. If the band suddenly cancels, the planner does not panic. The planner undoes the earlier bookings if needed, or finds a backup, so the family is never left half-arranged.

A saga is exactly this wedding planner for your software. It runs a long job in small steps across many services. It keeps a notebook (the saga state). It waits between steps. And if one step fails, it undoes the earlier work so nothing is left broken.

Wolverine is a .NET library that makes writing these planners very easy. Let us learn how, step by step, in plain words.

Why a normal transaction is not enough

Imagine an online food order. Placing an order touches several services:

  • Payment service charges the card.
  • Inventory service reserves the food items.
  • Delivery service assigns a rider.

On one single database, you could wrap all of this in one transaction. Everything succeeds together, or everything rolls back together. Nice and safe.

But in real systems, these are separate services with separate databases, often on separate machines. You cannot put one big transaction across all of them. The network is slow. Services go down. A rider might take ten minutes to accept. You need a way to coordinate these steps that can survive waiting, failure, and restarts.

That is the job a saga does.

One order spread across three separate services, each with its own database

What a saga really is

A saga has two simple ideas inside it.

First, state. This is the saga's notebook. It remembers what has happened so far. For an order saga, the state might hold the order id, whether payment is done, and whether stock is reserved.

Second, steps driven by messages. The saga does not run all at once. Each message that arrives moves the workflow forward by one step. A message comes in, the saga updates its notebook, and then it sends out the next command.

Here is the flow of a happy order, where everything works:

Happy path of an order saga

Start
Pay
Reserve
Deliver
Done

Steps

1

Start

Order placed, saga created

2

Pay

Charge the card

3

Reserve

Hold the food items

4

Deliver

Assign a rider

5

Done

Mark saga complete

Each step finishes and triggers the next one

Two kinds of sagas

People use the word saga for two different styles. It helps to know both.

StyleHow it worksWho is in charge
ChoreographyEach service listens for events and reacts on its ownNo central boss; services just react
OrchestrationOne central saga sends commands and tracks progressThe saga is the boss

Wolverine's Saga base class is the orchestration style. There is one clear coordinator that holds the state and tells each service what to do next. This is usually easier to follow and easier to debug, because all the workflow logic lives in one place.

Choreography versus orchestration at a glance

Setting up Wolverine

First, add Wolverine to your project. You will also pick a persistence option. For learning, lightweight storage on PostgreSQL is the simplest. Here is a basic host setup.

using Wolverine;
using Wolverine.Postgresql;
 
var builder = Host.CreateApplicationBuilder(args);
 
builder.UseWolverine(opts =>
{
    // Tell Wolverine to store messages and saga state in PostgreSQL
    var connectionString = builder.Configuration.GetConnectionString("orders")!;
    opts.PersistMessagesWithPostgresql(connectionString);
 
    // Turn on durable, reliable messaging so steps survive restarts
    opts.Policies.UseDurableLocalQueues();
});
 
using var host = builder.Build();
await host.RunAsync();

The key idea is that Wolverine now has a safe place to keep both the in-flight messages and the saga notebooks. If your app restarts in the middle of a workflow, nothing is lost.

Writing your first saga

A Wolverine saga is just a class that inherits from Wolverine.Saga. Let us model the order workflow.

First, the messages. In messaging we separate commands (do this) from events (this happened). Commands are instructions. Events are announcements in the past tense.

// Commands: instructions to a service
public record ChargePayment(Guid OrderId, decimal Amount);
public record ReserveStock(Guid OrderId);
public record AssignRider(Guid OrderId);
 
// Events: announcements that something already happened
public record OrderPlaced(Guid OrderId, decimal Amount);
public record PaymentCharged(Guid OrderId);
public record StockReserved(Guid OrderId);
public record RiderAssigned(Guid OrderId);

Now the saga itself. Look closely at the method names. Wolverine uses conventions, so the names matter.

using Wolverine;
 
public class OrderSaga : Saga
{
    // This is the saga id. Wolverine matches messages to this.
    public Guid Id { get; set; }
 
    public bool PaymentDone { get; set; }
    public bool StockReserved { get; set; }
 
    // Start is called by the very first message of the workflow.
    // The return value is the next command Wolverine should send.
    public static (OrderSaga, ChargePayment) Start(OrderPlaced placed)
    {
        var saga = new OrderSaga { Id = placed.OrderId };
        var charge = new ChargePayment(placed.OrderId, placed.Amount);
        return (saga, charge);
    }
 
    // Handle methods move the workflow forward one step at a time.
    public ReserveStock Handle(PaymentCharged charged)
    {
        PaymentDone = true;
        return new ReserveStock(Id);
    }
 
    public AssignRider Handle(StockReserved reserved)
    {
        StockReserved = true;
        return new AssignRider(Id);
    }
 
    public void Handle(RiderAssigned assigned)
    {
        // The last step. The workflow is finished.
        MarkCompleted();
    }
}

Three things make this special.

The Start method creates the saga and returns the first command. Wolverine sees this static method and knows that an OrderPlaced message begins a new saga.

Each Handle method takes an event, updates the notebook, and returns the next command. Returning a message is called cascading. Wolverine sends it for you. You never call a bus directly.

MarkCompleted() tells Wolverine to delete the saga state. The wedding is over; the planner can throw away the notebook.

How messages and the saga state move together over time

How Wolverine finds the right saga

When a PaymentCharged event arrives, Wolverine must load the correct saga out of thousands. How does it know which one?

It looks at the message for the saga id, in this order:

Lookup orderWhat Wolverine checksExample
1A property marked with [SagaIdentity][SagaIdentity] Guid OrderRef
2A property named after the saga typeOrderSagaId
3A property simply named IdId

In our example, the events carry OrderId. If we want Wolverine to use that, we can mark it clearly:

public record PaymentCharged([property: SagaIdentity] Guid OrderId);

Once Wolverine reads the id, it loads that exact saga from storage, runs the matching Handle method, saves the updated state, and sends any cascaded command. All of this happens inside one safe unit of work.

Adding a timeout

Real workflows get stuck. The rider never accepts. The payment provider hangs. You do not want a saga waiting forever. So you add a timeout.

Wolverine has a special base type called TimeoutMessage. You create your own timeout and say how long to wait. When a saga returns it, Wolverine schedules it for the future without any external scheduler.

using Wolverine;
 
// This message will be delivered 30 minutes after it is returned.
public record OrderTimeout(Guid OrderId) : TimeoutMessage(30.Minutes());

Now we update the saga to start a timer when the order begins, and to react if the timer fires before we finish.

public class OrderSaga : Saga
{
    public Guid Id { get; set; }
    public bool Completed { get; set; }
 
    public static (OrderSaga, ChargePayment, OrderTimeout) Start(OrderPlaced placed)
    {
        var saga = new OrderSaga { Id = placed.OrderId };
        return (
            saga,
            new ChargePayment(placed.OrderId, placed.Amount),
            new OrderTimeout(placed.OrderId)   // start the 30 minute clock
        );
    }
 
    public void Handle(OrderTimeout timeout)
    {
        if (Completed)
            return; // already finished, ignore the timer
 
        // Still stuck after 30 minutes. Give up cleanly.
        CancelEverything();
        MarkCompleted();
    }
 
    private void CancelEverything()
    {
        // Send refund and release-stock commands here.
    }
}

Notice we return three things from Start: the saga, the first command, and the timeout. Wolverine is happy to send several messages at once. The timeout just rides along.

A timeout that fires only if the saga is still unfinished

Undoing work: compensation

This is the heart of why sagas exist. We cannot use one big database transaction across services, so we cannot simply roll back. Instead, when a later step fails, we run compensating actions that undo the earlier successful steps.

If payment succeeded but stock is out of stock, we must refund the payment. Refunding is the compensation for charging. Each forward step has a matching undo step.

Forward stepCompensating step (the undo)
Charge paymentRefund payment
Reserve stockRelease stock
Assign riderCancel rider assignment

Here is how that looks when a stock reservation fails.

public record StockReservationFailed(Guid OrderId);
public record RefundPayment(Guid OrderId);
 
public class OrderSaga : Saga
{
    public Guid Id { get; set; }
    public bool PaymentDone { get; set; }
 
    public RefundPayment Handle(StockReservationFailed failed)
    {
        // Stock could not be reserved. Undo the payment we already took.
        if (PaymentDone)
        {
            return new RefundPayment(Id);
        }
 
        MarkCompleted();
        return null!;
    }
 
    public void Handle(PaymentRefunded refunded)
    {
        // The undo is finished. The workflow ends here.
        MarkCompleted();
    }
}

The flow below shows the rollback in action.

Compensation when stock runs out

Pay OK
Reserve fails
Refund
Done

Steps

1

Pay OK

Card charged

2

Reserve fails

No stock left

3

Refund

Money returned

4

Done

Saga marked complete

Payment is undone so the customer is not charged for nothing

Handling messages for a saga that is gone

Sometimes a message arrives for a saga that no longer exists. Maybe it already completed, or a duplicate message came in late. By default this is an error, but you can handle it gracefully with a NotFound method.

public class OrderSaga : Saga
{
    public Guid Id { get; set; }
 
    // Called when a PaymentCharged arrives but the saga is gone.
    public static void NotFound(PaymentCharged charged, ILogger<OrderSaga> logger)
    {
        logger.LogInformation(
            "Ignoring late PaymentCharged for order {OrderId}; saga already finished",
            charged.OrderId);
    }
}

This keeps your system calm. A late or duplicate message becomes a quiet log line instead of a crash.

Why testing is so easy

This is one of Wolverine's best gifts. Because a saga is just a plain class with state, you can test it with no mocks, no fakes, and no message broker. You set up a state, call a method, and check the state and the returned message.

[Fact]
public void Charging_payment_should_ask_to_reserve_stock()
{
    var saga = new OrderSaga { Id = Guid.NewGuid() };
 
    var next = saga.Handle(new PaymentCharged(saga.Id));
 
    Assert.True(saga.PaymentDone);
    Assert.IsType<ReserveStock>(next);
}

That is a real unit test of business logic, running in milliseconds. No other saga library in the .NET world makes this quite so clean.

Choosing where to store saga state

Wolverine gives three storage choices. Pick based on your project.

Storage optionSetup effortBest when
LightweightAlmost none; JSON in a tableYou want the simplest start
MartenLow; document in PostgreSQLYou already use Marten or want documents
EF CoreMedium; flat queryable tableYou want to save saga and business data in one transaction

For most teams starting out, lightweight storage is the friendliest. As your needs grow, you can move to Marten or EF Core without rewriting your saga logic.

A note on the wider .NET ecosystem

You may have heard of other saga tools like MassTransit, NServiceBus, and Rebus. They are all capable. A few honest points to help you choose:

  • MassTransit and NServiceBus now use commercial licenses. For paid production use this can mean real cost, so check their current terms.
  • MediatR also moved to a commercial license recently. It is an in-process tool and not a saga library, but people often mention it nearby, so it is worth knowing.
  • Wolverine is open source and uses convention-based sagas, which means less ceremony and no state machine DSL to memorise.

The right tool depends on your team, budget, and existing stack. Wolverine is an excellent choice when you want low ceremony and easy testing.

Putting the whole picture together

Here is the full life of an order saga, from the first message to the last.

Full saga lifecycle including a possible failure branch

Each box is just a method on your saga class. Wolverine handles loading state, saving state, sending the next message, and scheduling timeouts. You focus on the business rules.

Quick recap

  • A saga is a planner that runs a long job in small steps across many services and remembers where it is.
  • We use sagas because one big transaction cannot span separate services, so we coordinate with messages instead.
  • A Wolverine saga is a plain class that inherits from Saga, with a Start method and several Handle methods.
  • Handle methods cascade the next command by returning it. You never call a bus directly.
  • MarkCompleted() deletes the saga state when the workflow ends.
  • Wolverine finds the right saga using [SagaIdentity], then a {SagaType}Id property, then Id.
  • A TimeoutMessage schedules future work so stuck sagas do not wait forever.
  • Compensation undoes earlier steps when a later step fails, since you cannot roll back across services.
  • Sagas are very easy to test because they are plain classes with state in and state out.
  • Wolverine offers lightweight, Marten, and EF Core storage; remember that MassTransit, NServiceBus, and MediatR are now commercially licensed.

References and further reading

Related Patterns