Class Saga<T>

java.lang.Object
org.acemq.amqp.patterns.Saga<T>
Type Parameters:
T - what the saga operates on

public final class Saga<T> extends Object
A sequence of steps where each one knows how to undo itself.

 Saga<Order> booking = Saga.named("place-order")
         .step("take-payment", order -> payments.charge(order))
                 .compensateWith(order -> payments.refund(order))
         .step("reserve-stock", order -> inventory.reserve(order))
                 .compensateWith(order -> inventory.release(order))
         .step("book-courier", order -> couriers.book(order))
         .build();

 SagaResult result = booking.run(order);
 

If book-courier throws, the stock is released and the payment refunded, in that order, and SagaResult.compensated() says so.

What this is not

Not a distributed transaction. Nothing is isolated: after take-payment the customer's money really has moved, and anybody looking sees that it has. If book-courier then fails, the refund is a new fact rather than an erasure of the old one, and for a few seconds the world contained a charge that should not have happened. That is not a defect in this class; it is what compensating a real-world action means, and a saga is honest about it where a two-phase commit pretends otherwise.

So the steps must be things that can be undone by doing something else. Sending an email cannot be compensated — the apology is a second email, not an unsend — and a saga step that sends one should be the last step, after everything that can still fail.

Not durable. This runs in one process and its state is on the stack. A crash midway leaves the saga half-applied with nothing to resume it, which is the honest limitation of the in-process form and is why the compensations run in a finally-shaped path rather than after a restart. Where a saga must survive the process, the steps have to be messages and the state has to be in a database — a much larger thing, and it is not this.

For most systems the in-process form is the right one: it turns "remember to undo the three things you already did" from a comment into something the compiler can see.

When compensation itself fails

It is tried, it is logged, and the remaining compensations still run. The alternative — stopping — leaves more undone than continuing does. What comes back is a SagaResult listing what could not be undone, and that list is the thing to alert on: it is the set of facts a human now has to reconcile by hand.

  • Method Details

    • named

      public static <T> Saga.Builder<T> named(String name)
      Type Parameters:
      T - what it operates on
      Parameters:
      name - what this saga is called, in logs and results
      Returns:
      a builder
    • run

      public SagaResult run(T subject)
      Runs the steps, compensating in reverse if one fails.
      Parameters:
      subject - what to operate on
      Returns:
      what happened; never throws for a step failure, because a caller needs the compensation report more than it needs a stack trace
    • stepNames

      public List<String> stepNames()
      Returns:
      the step names, in order
    • toString

      public String toString()
      Overrides:
      toString in class Object