Persistent FSM

Dependency

Persistent FSMs are part of Akka persistence, you must add the following dependency in your project:

sbt
libraryDependencies += "com.typesafe.akka" %% "akka-persistence" % "2.5.16"
Maven
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-persistence_2.11</artifactId>
  <version>2.5.16</version>
</dependency>
Gradle
dependencies {
  compile group: 'com.typesafe.akka', name: 'akka-persistence_2.11', version: '2.5.16'
}

Persistent FSM

Warning

Persistent FSM is no longer actively developed and will be replaced by Akka Typed Persistence. It is not advised to build new applications with Persistent FSM.

PersistentFSMAbstractPersistentFSM handles the incoming messages in an FSM like fashion. Its internal state is persisted as a sequence of changes, later referred to as domain events. Relationship between incoming messages, FSM’s states and transitions, persistence of domain events is defined by a DSL.

A Simple Example

To demonstrate the features of the PersistentFSM traitAbstractPersistentFSM, consider an actor which represents a Web store customer. The contract of our “WebStoreCustomerFSMActor” is that it accepts the following commands:

Scala
sealed trait Command
case class AddItem(item: Item) extends Command
case object Buy extends Command
case object Leave extends Command
case object GetCurrentCart extends Command
Full source at GitHub
Java
public static final class AddItem implements Command {
    private final Item item;

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

    public Item getItem() {
        return item;
    }
}

public enum Buy implements Command {INSTANCE}

public enum Leave implements Command {INSTANCE}

public enum GetCurrentCart implements Command {INSTANCE}
Full source at GitHub

AddItem sent when the customer adds an item to a shopping cart Buy - when the customer finishes the purchase Leave - when the customer leaves the store without purchasing anything GetCurrentCart allows to query the current state of customer’s shopping cart

The customer can be in one of the following states:

Scala
sealed trait UserState extends FSMState
case object LookingAround extends UserState {
  override def identifier: String = "Looking Around"
}
case object Shopping extends UserState {
  override def identifier: String = "Shopping"
}
case object Inactive extends UserState {
  override def identifier: String = "Inactive"
}
case object Paid extends UserState {
  override def identifier: String = "Paid"
}
Full source at GitHub
Java
enum UserState implements PersistentFSM.FSMState {
    LOOKING_AROUND("Looking Around"),
    SHOPPING("Shopping"),
    INACTIVE("Inactive"),
    PAID("Paid");

    private final String stateIdentifier;

    UserState(String stateIdentifier) {
        this.stateIdentifier = stateIdentifier;
    }

    @Override
    public String identifier() {
        return stateIdentifier;
    }
}
Full source at GitHub

LookingAround customer is browsing the site, but hasn’t added anything to the shopping cart Shopping customer has recently added items to the shopping cart Inactive customer has items in the shopping cart, but hasn’t added anything recently Paid customer has purchased the items

Note

PersistentFSMAbstractPersistentFSM states must inherit from traitimplement interface PersistentFSM.FSMState and implement the def identifier: StringString identifier() method. This is required in order to simplify the serialization of FSM states. String identifiers should be unique!

Customer’s actions are “recorded” as a sequence of “domain events” which are persisted. Those events are replayed on an actor’s start in order to restore the latest customer’s state:

Scala
sealed trait DomainEvent
case class ItemAdded(item: Item) extends DomainEvent
case object OrderExecuted extends DomainEvent
case object OrderDiscarded extends DomainEvent
Full source at GitHub
Java
public static final class ItemAdded implements DomainEvent {
    private final Item item;

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

    public Item getItem() {
        return item;
    }
}

public enum OrderExecuted implements DomainEvent {INSTANCE}

public enum OrderDiscarded implements DomainEvent {INSTANCE}
Full source at GitHub

Customer state data represents the items in a customer’s shopping cart:

Scala
case class Item(id: String, name: String, price: Float)

sealed trait ShoppingCart {
  def addItem(item: Item): ShoppingCart
  def empty(): ShoppingCart
}
case object EmptyShoppingCart extends ShoppingCart {
  def addItem(item: Item) = NonEmptyShoppingCart(item :: Nil)
  def empty() = this
}
case class NonEmptyShoppingCart(items: Seq[Item]) extends ShoppingCart {
  def addItem(item: Item) = NonEmptyShoppingCart(items :+ item)
  def empty() = EmptyShoppingCart
}
Full source at GitHub
Java
static class ShoppingCart {
    private final List<Item> items = new ArrayList<>();

    public List<Item> getItems() {
        return Collections.unmodifiableList(items);
    }

    void addItem(Item item) {
        items.add(item);
    }

    void empty() {
        items.clear();
    }
}

static class Item implements Serializable {
    private final String id;
    private final String name;
    private final float price;

    Item(String id, String name, float price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public String getId() {
        return id;
    }

    public float getPrice() {
        return price;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return String.format("Item{id=%s, name=%s, price=%s}", id, price, name);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Item item = (Item) o;

        return item.price == price && id.equals(item.id) && name.equals(item.name);
    }
}
Full source at GitHub

Here is how everything is wired together:

Scala
startWith(LookingAround, EmptyShoppingCart)

when(LookingAround) {
  case Event(AddItem(item), _) ⇒
    goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
  case Event(GetCurrentCart, data) ⇒
    stay replying data
}

when(Shopping) {
  case Event(AddItem(item), _) ⇒
    stay applying ItemAdded(item) forMax (1 seconds)
  case Event(Buy, _) ⇒
    goto(Paid) applying OrderExecuted andThen {
      case NonEmptyShoppingCart(items) ⇒
        reportActor ! PurchaseWasMade(items)
        saveStateSnapshot()
      case EmptyShoppingCart ⇒ saveStateSnapshot()
    }
  case Event(Leave, _) ⇒
    stop applying OrderDiscarded andThen {
      case _ ⇒
        reportActor ! ShoppingCardDiscarded
        saveStateSnapshot()
    }
  case Event(GetCurrentCart, data) ⇒
    stay replying data
  case Event(StateTimeout, _) ⇒
    goto(Inactive) forMax (2 seconds)
}

when(Inactive) {
  case Event(AddItem(item), _) ⇒
    goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
  case Event(StateTimeout, _) ⇒
    stop applying OrderDiscarded andThen {
      case _ ⇒ reportActor ! ShoppingCardDiscarded
    }
}

when(Paid) {
  case Event(Leave, _) ⇒ stop()
  case Event(GetCurrentCart, data) ⇒
    stay replying data
}
Full source at GitHub
Java
startWith(UserState.LOOKING_AROUND, new ShoppingCart());

when(UserState.LOOKING_AROUND,
    matchEvent(AddItem.class,
        (event, data) ->
            goTo(UserState.SHOPPING).applying(new ItemAdded(event.getItem()))
                .forMax(Duration.ofSeconds(1)))
    .event(GetCurrentCart.class, (event, data) -> stay().replying(data))
);

when(UserState.SHOPPING,
    matchEvent(AddItem.class,
        (event, data) ->
            stay().applying(new ItemAdded(event.getItem()))
               .forMax(Duration.ofSeconds(1)))
    .event(Buy.class,
        (event, data) ->
            goTo(UserState.PAID).applying(OrderExecuted.INSTANCE)
                .andThen(exec(cart -> {
                    reportActor.tell(new PurchaseWasMade(cart.getItems()), self());
                    saveStateSnapshot();
                })))
    .event(Leave.class,
        (event, data) ->
            stop().applying(OrderDiscarded.INSTANCE)
                .andThen(exec(cart -> {
                    reportActor.tell(ShoppingCardDiscarded.INSTANCE, self());
                    saveStateSnapshot();
                })))
    .event(GetCurrentCart.class, (event, data) -> stay().replying(data))
    .event(StateTimeout$.class,
        (event, data) ->
            goTo(UserState.INACTIVE).forMax(Duration.ofSeconds(2)))
);


when(UserState.INACTIVE,
    matchEvent(AddItem.class,
        (event, data) ->
            goTo(UserState.SHOPPING).applying(new ItemAdded(event.getItem()))
                .forMax(Duration.ofSeconds(1)))
    .event(GetCurrentCart.class, (event, data) -> stay().replying(data))
    .event(StateTimeout$.class,
        (event, data) ->
            stop().applying(OrderDiscarded.INSTANCE)
                .andThen(exec(cart ->
                    reportActor.tell(ShoppingCardDiscarded.INSTANCE, self())
                )))
);

when(UserState.PAID,
    matchEvent(Leave.class, (event, data) -> stop())
    .event(GetCurrentCart.class, (event, data) -> stay().replying(data))
);
Full source at GitHub
Note

State data can only be modified directly on initialization. Later it’s modified only as a result of applying domain events. Override the applyEvent method to define how state data is affected by domain events, see the example below

Scala
override def applyEvent(event: DomainEvent, cartBeforeEvent: ShoppingCart): ShoppingCart = {
  event match {
    case ItemAdded(item) ⇒ cartBeforeEvent.addItem(item)
    case OrderExecuted   ⇒ cartBeforeEvent
    case OrderDiscarded  ⇒ cartBeforeEvent.empty()
  }
}
Full source at GitHub
Java
@Override
public ShoppingCart applyEvent(DomainEvent event, ShoppingCart currentData) {
    if (event instanceof ItemAdded) {
        currentData.addItem(((ItemAdded) event).getItem());
        return currentData;
    } else if (event instanceof OrderExecuted) {
        return currentData;
    } else if (event instanceof OrderDiscarded) {
        currentData.empty();
        return currentData;
    }
    throw new RuntimeException("Unhandled");
}
Full source at GitHub

andThen can be used to define actions which will be executed following event’s persistence - convenient for “side effects” like sending a message or logging. Notice that actions defined in andThen block are not executed on recovery:

Scala
goto(Paid) applying OrderExecuted andThen {
  case NonEmptyShoppingCart(items) ⇒
    reportActor ! PurchaseWasMade(items)
}
Full source at GitHub
Java
(event, data) ->
    goTo(UserState.PAID).applying(OrderExecuted.INSTANCE)
        .andThen(exec(cart -> {
            reportActor.tell(new PurchaseWasMade(cart.getItems()), self());
        })))
Full source at GitHub

A snapshot of state data can be persisted by calling the saveStateSnapshot() method:

Scala
stop applying OrderDiscarded andThen {
  case _ ⇒
    reportActor ! ShoppingCardDiscarded
    saveStateSnapshot()
}
Full source at GitHub
Java
(event, data) ->
    stop().applying(OrderDiscarded.INSTANCE)
        .andThen(exec(cart -> {
            reportActor.tell(ShoppingCardDiscarded.INSTANCE, self());
            saveStateSnapshot();
        })))
Full source at GitHub

On recovery state data is initialized according to the latest available snapshot, then the remaining domain events are replayed, triggering the applyEvent method.

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.