EventSourced behaviors as finite state machines
An EventSourcedBehavior
EventSourcedBehavior
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
-
source
sealed 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
-
source
abstract 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
-
source
sealed 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
-
source
interface 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
-
source
def 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
-
source
def 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(); }