VladKopanev/cats-saga

Support for non-Throwable domain-level errors

benhutchison opened this issue · 3 comments

I've been trying Saga out and really appreciating it so far.

But I noticed one area where my design style doesn't fit with the assumptions in Saga.

Saga assumes that errors are Throwables. I tend towards:

  • Errors that can be reasonably anticipated (domain layer errors) are modeled in the payload F[Either[E, A]]
  • Unexpected errors are modeled via Throwable, such as network layer problems.

Right now, there seems no way to trigger compensating actions without raising a Throwable.

I would like to examine the payload data and trigger compensation based on that. In my specific example, I'm trying to fulfill an order, but if any of the order components is OutOfStock (a domain layer error) I want to cancel the whole order and put all items back in inventory (a compensation that should also happen on unexpected error).

I looked at the design and concluded it might need another case in the Saga algebra, eg:

private case class EitherStep[F[_], A, E](action: F[Either[E, A]], compensate: E => F[Unit]) extends Saga[F, Either[E, A]]

WDYT?

Actually, apparently I was able to build what I was seeking on top of Saga (compose FTW):

  case class LeftError[E](e: E) extends Throwable with NoStackTrace

  /** Compensate if the action succeeds but yields a Left. Don't compensate if the action fails. */
  def compensateE[F[_]: MonadError[*[_], Throwable], A, E](action: F[Either[E, A]], compensation: E => F[Unit]) = {
    Saga.noCompensate(action).flatMap[A] {
      case Right(a) => Saga.succeed(a)
      case Left(e) => LeftError(e).raiseError[F, A].compensateIfFail[Throwable](_ => compensation(e))
    }
  }

Slightly changed my thinking from above, in that it returns Saga[F, A] not Saga[F, Either[E, A]]. I realize that raising errors are key to triggering compensation so no point in trying to return them.

Hi Ben, I agree that it would be great to have Saga logic support not only Throwable as an error, but an arbitrary type. And actually zio-saga library supports that, but it was easy to implement because underlying ZIO type in it's turn has support for user-defined error types. Unfortunately not all IO-like types support bifunctor IO capabilities that ZIO has. I think of Saga as a thin wrapper around the underlying IO type that tracks errors and tries to compensate them with given actions. Saga is not meant to be some kind of BiFunctor lifter for any IO type that doesn't support that IMO. If you have ideas of how to overcome cats.effect.IO restrictions in error type that would be great, I would like then to see how we can change the library design to support desired behavior.

I'll need to learn more about Saga and do some further thinking to comment usefully on this problem. Hope to get back to it..