EventSourced behaviors as finite state machines

An EventSourcedBehaviorEventSourcedBehavior can be used to represent a persistent FSM. If you’re migrating an existing classic persistent FSM to EventSourcedBehavior see the migration guide.

To demonstrate this consider an example of a shopping application. A customer can be in the following states:

  • Looking around
  • Shopping (has something in their basket)
  • Inactive
  • Paid
Scala
sourcesealed trait State
case class LookingAround(cart: ShoppingCart) extends State
case class Shopping(cart: ShoppingCart) extends State
case class Inactive(cart: ShoppingCart) extends State
case class Paid(cart: ShoppingCart) extends State
Java
sourceabstract static class State {
  public final ShoppingCart cart;

  protected State(ShoppingCart cart) {
    this.cart = cart;
  }
}

public static class LookingAround extends State {
  public LookingAround(ShoppingCart cart) {
    super(cart);
  }
}

public static class Shopping extends State {
  public Shopping(ShoppingCart cart) {
    super(cart);
  }
}

public static class Inactive extends State {
  public Inactive(ShoppingCart cart) {
    super(cart);
  }
}

public static class Paid extends State {
  public Paid(ShoppingCart cart) {
    super(cart);
  }
}

And the commands that can result in state changes:

  • Add item
  • Buy
  • Leave
  • Timeout (internal command to discard abandoned purchases)

And the following read only commands:

  • Get current cart
Scala
sourcesealed trait Command
case class AddItem(item: Item) extends Command
case object Buy extends Command
case object Leave extends Command
case class GetCurrentCart(replyTo: ActorRef[ShoppingCart]) extends Command
private case object Timeout extends Command
Java
sourceinterface Command {}

public static class AddItem implements Command {
  public final Item item;

  public AddItem(Item item) {
    this.item = item;
  }
}

public static class GetCurrentCart implements Command {
  public final ActorRef<ShoppingCart> replyTo;

  public GetCurrentCart(ActorRef<ShoppingCart> replyTo) {
    this.replyTo = replyTo;
  }
}

public enum Buy implements Command {
  INSTANCE
}

public enum Leave implements Command {
  INSTANCE
}

private enum Timeout implements Command {
  INSTANCE
}

The command handler of the EventSourcedBehavior is used to convert the commands that change the state of the FSM to events, and reply to commands.

The command handler:The forStateType command handler can be used:

Scala
sourcedef commandHandler(timers: TimerScheduler[Command])(state: State, command: Command): Effect[DomainEvent, State] =
  state match {
    case LookingAround(cart) =>
      command match {
        case AddItem(item) =>
          Effect.persist(ItemAdded(item)).thenRun(_ => timers.startSingleTimer(StateTimeout, Timeout, 1.second))
        case GetCurrentCart(replyTo) =>
          replyTo ! cart
          Effect.none
        case _ =>
          Effect.none
      }
    case Shopping(cart) =>
      command match {
        case AddItem(item) =>
          Effect.persist(ItemAdded(item)).thenRun(_ => timers.startSingleTimer(StateTimeout, Timeout, 1.second))
        case Buy =>
          Effect.persist(OrderExecuted).thenRun(_ => timers.cancel(StateTimeout))
        case Leave =>
          Effect.persist(OrderDiscarded).thenStop()
        case GetCurrentCart(replyTo) =>
          replyTo ! cart
          Effect.none
        case Timeout =>
          Effect.persist(CustomerInactive)
      }
    case Inactive(_) =>
      command match {
        case AddItem(item) =>
          Effect.persist(ItemAdded(item)).thenRun(_ => timers.startSingleTimer(StateTimeout, Timeout, 1.second))
        case Timeout =>
          Effect.persist(OrderDiscarded)
        case _ =>
          Effect.none
      }
    case Paid(cart) =>
      command match {
        case Leave =>
          Effect.stop()
        case GetCurrentCart(replyTo) =>
          replyTo ! cart
          Effect.none
        case _ =>
          Effect.none
      }
  }
Java
source  CommandHandlerBuilder<Command, DomainEvent, State> builder = newCommandHandlerBuilder();

  builder.forStateType(LookingAround.class).onCommand(AddItem.class, this::addItem);

  builder
      .forStateType(Shopping.class)
      .onCommand(AddItem.class, this::addItem)
      .onCommand(Buy.class, this::buy)
      .onCommand(Leave.class, this::discardShoppingCart)
      .onCommand(Timeout.class, this::timeoutShopping);

  builder
      .forStateType(Inactive.class)
      .onCommand(AddItem.class, this::addItem)
      .onCommand(Timeout.class, () -> Effect().persist(OrderDiscarded.INSTANCE).thenStop());

  builder.forStateType(Paid.class).onCommand(Leave.class, () -> Effect().stop());

  builder.forAnyState().onCommand(GetCurrentCart.class, this::getCurrentCart);
  return builder.build();
}

The event handler is used to change state once the events have been persisted. When the EventSourcedBehavior is restarted the events are replayed to get back into the correct state.

Scala
sourcedef eventHandler(state: State, event: DomainEvent): State = {
  state match {
    case la @ LookingAround(cart) =>
      event match {
        case ItemAdded(item) => Shopping(cart.addItem(item))
        case _               => la
      }
    case Shopping(cart) =>
      event match {
        case ItemAdded(item)  => Shopping(cart.addItem(item))
        case OrderExecuted    => Paid(cart)
        case OrderDiscarded   => state // will be stopped
        case CustomerInactive => Inactive(cart)
      }
    case i @ Inactive(cart) =>
      event match {
        case ItemAdded(item) => Shopping(cart.addItem(item))
        case OrderDiscarded  => i // will be stopped
        case _               => i
      }
    case Paid(_) => state // no events after paid
  }
}
Java
source@Override
public EventHandler<State, DomainEvent> eventHandler() {
  EventHandlerBuilder<State, DomainEvent> eventHandlerBuilder = newEventHandlerBuilder();

  eventHandlerBuilder
      .forStateType(LookingAround.class)
      .onEvent(ItemAdded.class, item -> new Shopping(new ShoppingCart(item.getItem())));

  eventHandlerBuilder
      .forStateType(Shopping.class)
      .onEvent(
          ItemAdded.class, (state, item) -> new Shopping(state.cart.addItem(item.getItem())))
      .onEvent(OrderExecuted.class, (state, item) -> new Paid(state.cart))
      .onEvent(OrderDiscarded.class, (state, item) -> state) // will be stopped
      .onEvent(CustomerInactive.class, (state, event) -> new Inactive(state.cart));

  eventHandlerBuilder
      .forStateType(Inactive.class)
      .onEvent(
          ItemAdded.class, (state, item) -> new Shopping(state.cart.addItem(item.getItem())))
      .onEvent(OrderDiscarded.class, (state, item) -> state); // will be stopped

  return eventHandlerBuilder.build();
}