Skip to main content

Saga

In a distributed system, sometimes you need a concept similar to a transaction in a database. That is, several operations spanning different microservices must succeed or fail as a unit; otherwise, we may end up in an inconsistent state. A saga implements this concept by providing for each action a corresponding compensating action, which is executed if any of the following steps fail. The role of the compensating action is to undo any changes performed by the action, hence taking the system to the state before the entire operation beginning its execution.

Additional context for this pattern

Saga distributed transactions pattern in Cloud Design Patterns.

Arrow Resilience provides the saga function, which creates a new scope where compensating actions can be declared alongside the action to perform. This is done by the saga function in SagaScope. The resulting Saga<A> doesn't perform any actions, though; you need to call transact to keep the chain going.

Let's use a small counter as an example, which we implement using the Atomic type provided by Arrow.

import arrow.atomic.AtomicInt
import arrow.resilience.*

val INITIAL_VALUE = 1

object Counter {
val value = AtomicInt(INITIAL_VALUE)

fun increment() {
value.incrementAndGet()
}

fun decrement() {
value.decrementAndGet()
}
}

Now we create a saga with a couple of operations. The first one increments the counter, so the compensating action must be decrementing it. The second action simply fails; we include no compensation because we know that part is never reached.

val PROBLEM = Throwable("problem detected!")

// describe the transaction
val transaction: Saga<Int> = saga {
saga({
// action to perform
Counter.increment()
}) {
// inverse action for rolling back
Counter.decrement()
}
saga({
throw PROBLEM
}) {}
// final value of the saga
Counter.value.get()
}

Executing the transaction gives the expected results:

  • The exception raised in the second step bubbles up to the caller of transact. In this case, we use Either.catch to turn it into Either.
  • The counter has been correctly decremented as part of the compensation process in the saga.
suspend fun example() {
// perform the transaction
val result = Either.catch { transaction.transact() }
result shouldBe PROBLEM.left()
Counter.value.get() shouldBe INITIAL_VALUE
}
Saga and Resource

SagaScope has many parallels with ResourceScope: both ensure that some operations are performed at a certain point, and saga and install require an action that "undoes" something. The main difference is that ResourceScope always runs the release actions, whereas SagaScope only runs compensation if the entire action fails.

Saga and STM

If you need to perform several actions as a unit over local data, Software Transactional Memory is a better tool than Sagas and Atomic references.