Class Saga<T>
- Type Parameters:
T- what the saga operates on
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.
-
Nested Class Summary
Nested Classes -
Method Summary
-
Method Details
-
named
- Type Parameters:
T- what it operates on- Parameters:
name- what this saga is called, in logs and results- Returns:
- a builder
-
run
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
- Returns:
- the step names, in order
-
toString
-