Saga, compensate for failures in a terse and composable way

August 10, 2018

Motivation for a Saga

Imagine you want to book a trip which includes a car, hotel and a flight. If something cannot be booked, why would you bother going? If we would build this in Scala it would probably involve calling some external API’s. These API’s can go down, what happens if one of these API’s is down?

Saga: example of process which can fail, but takes failure in to account

Saga: example of process which can fail, but takes failure in to account

You could use recover to execute a compensating action (a action which reverses the side-effect), but it will only work with one action. Of course you could apply this to all your actions, but this will get messy because you need to keep track of your compensating actions.

How to describe that in a nice way?

To describe a program which can handle failure you need to couple the outcome of a succesful action to it’s compensating action.

A short example of a Saga program

import cats.effect.IO
import cats.implicits._
import cats.effect.concurrent.Ref
import goedverhaal._
import scala.util.control.NonFatal

def prg(ref: Ref[IO, Int]): Saga[IO, Unit] = for {
  _ <- Saga.recoverable(ref.tryUpdate(_ + 1))(_ => ref.tryUpdate(_ - 1) *> IO.unit).replicateA(500)
  _ <- Saga.recoverable(ref.tryUpdate(_ + 1))(_ => ref.tryUpdate(_ - 1) *> IO.unit).replicateA(500)
  _ <- Saga.nonRecoverable[IO, Nothing](IO.raiseError(new Throwable("Error")))
} yield ()

def main: IO[Int] = for {
  ref <- Ref.of[IO, Int](0)
  _ <- prg(ref).run.recoverWith { case NonFatal(_) => IO.unit }
  current <- ref.get
} yield current

The outcome of main will be zero, as the prg will crash at the end. The first action will increase the Ref[IO, Int] to 500 and the second action by another 500, but since it crashes the compensating actions will roll it back to 0.

The importance of lazy evaluation

The compensating action needs to be description of a action. A description means, that’s it is not executed immediately (lazy evaluation) and it may be a side-effect. This is called a computation in functional programming. The opposite of lazy evaluation is eager evaluation. An example of a type which is eager, is Future and Try.

Couple success with compensation

In my Saga the signature of the a recoverable combinator is defined as:

def recoverable[F[_] : Sync, A](comp: F[A])(rollback: A => F[Unit]): Saga[F, A]

The type class Sync constrain on F[_] is to enforce a type which supports lazy evaluation. The thing we need for our description of a Saga. The function it self takes two arguments, an actual comp (short for computation) which is the do action, and the rollback which uses the outcome of the do action to construct a rollback/compensating action.

Saga, a specialised Free Monad

As you can it returns a Saga[F, A]. A Saga itself is a description of several computations. In fact it’s a slightly altered variant of a Free Monad:

case class Pure[F[_], A](action: A) extends Saga[F, A]
case class Next[F[_], A](action: F[A], compensate: A => F[Unit]) extends Saga[F, A]
case class Bind[F[_], A, B](fa: Saga[F, A], f: A => Saga[F, B]) extends Saga[F, B]

The Pure and Bind are descriptions of operations which you’ll find on a Monad as well. The Next case however is not. This will store the parameters of the recoverable method as is for later evaluation.

This data is interpreted by the decide method on Saga, which looks like this:

def decide[B](f: (A, List[F[Unit]]) => F[B]): F[B]

It will fold the description of computations as described in the Saga data type to a F[B]. If anything fails (due a Sync.onError) it will execute the compensating actions accumulated so far. If it succeeds, it will execute the f: (A, List[F[Unit]] => F[B] function. This function let you decide what to do with the out come of the computation. This is useful when you work with a EitherT or OptionT. The outcome may be None or Left. In that case you might want to rollback all the actions.

You can also use the run variant on Saga which uses the decide

def run: F[A] = decide { case (a, _) => F.pure(a) }

Conclusion

As you can see Saga is useful tool when interacting with multiple API’s which are crossing an asynchronous boundary and might not offer transactional guarantees. It might not be the best solution, but in a lot of cases you don’t have a better choice I guess (welcome to the microservice/API era) !

If you want to have a closer look how that’s done or have feedback. Have a look at the source code on Github

Happy hacking!

Written by

Mark de Jong

Mark de Jong

Scala/Haskell Software Developer - Specialised in web-apps and high performance backend systems.