Persistence (Serialization)

Page contents

The library provides two built-in strategies for persisting and restoring a state machine, both serializable to JSON via kotlinx.serialization:

Strategy API Size Listeners on restore Best for
State snapshot captureSavedStateConfig() / restoreBySavedStateConfig() Constant (active states only) Suppressed by default Long-running machines, frequent checkpoints
Event recording machine.eventRecorder / restoreByRecordedEvents() Grows with event count Suppressed by default Audit trails, full replay, debugging

Both strategies require that the restored machine is built from identical code — only the active configuration is persisted, not the machine structure itself.


There are several kinds or levels of StateMachine persistence (serialization). Let’s look at sample use cases:

  1. Structure + configuration - Create StateMachine on some process/host and send its structure and active configuration by network to another process/host. The receiver can dynamically create the same StateMachine instance in the same state as original one. This case currently lacks built-in support by the library (you can open an issue if you need something like that).
  2. Configuration only - Both original and restored StateMachine instances are created by identical static code (in a single or multiple different processes/hosts). Only active configuration can be saved and restored. This case in turn may be reached in three different ways:

    1. Persisting state - (not supported) deeply serializing all internal data, active states, history stack, state variables etc. from original StateMachine and applying them to restored one.
    2. Active state configuration - (supported) lightweight snapshot of currently active states and DataState data values. Constant size regardless of machine lifetime. Listeners fire normally during restore. See Active state configuration persistence.
    3. Event recording - (supported) serializing all incoming events and replaying them on a new machine instance, which leads it into the same state as the original. Listeners are suppressed during replay by default. See Event recording.

Event recording

The library supports event recording out of the box. To enable it you should use EventRecordingArguments in CreationArguments when creating a machine instance by createStateMachine() functions family. The recording process can be configured with EventRecordingArguments properties.

val machine = createStateMachine(
    creationArguments = buildCreationArguments { eventRecordingArguments = buildEventRecordingArguments {} }
) {
    // ...
}

buildEventRecordingArguments {} accepts two optional properties:

  • clearRecordsOnMachineRestart: Boolean (default true) — clears recorded events when the machine is stopped and started again. Disable if you want to preserve the full history across restarts.
  • skipIgnoredEvents: Boolean (default true) — ignored events are not recorded, since they do not affect machine state and replaying them is unnecessary.

When the machine had processed your business logic events, and you want to save its state configuration, first you have to get the recorded events:

val recordedEvents = machine.eventRecorder.getRecordedEvents()

RecordedEvents object now is ready to be serialized. The library provides an implementation of serialization process using kotlinx.serialization library starting from KStateMachine version v0.32.0.

Initialize serialization format (JSON for instance) using KStateMachineSerializersModule. This module contains serialization routines for library classes.

    val jsonFormat = Json {
    // use special, library provided SerializersModule for RecordedEvents and its internals
    // from kstatemachine-serialization artifact
    serializersModule = KStateMachineSerializersModule + SerializersModule { /* ... */ }
}

And encode (serialize) RecordedEvents object:

val recordedEventsJson = jsonFormat.encodeToString(recordedEvents)

Custom serialization library

Alternatively you can use some other serialization library to serialize RecordedEvents class by your own (you will also need to serialize SerializableGeneratedEvent class for internal events generated by the library itself).

Restoring StateMachine

When a user wants to restore the StateMachine, they deserialize the RecordedEvents object and create a StateMachine instance having exactly the same structure as the original one. Typically, both instances are created by the same code.

val restoredRecordedEvents = jsonFormat.decodeFromString<RecordedEvents>(recordedEventsJson)

Calling restoreByRecordedEvents() or its blocking analog restoreByRecordedEventsBlocking() will process recorded events over just created StateMachine instance.

machine2.restoreByRecordedEvents(restoredRecordedEvents)

restoreByRecordedEvents() method will start the machine if necessary. You can configure restoration process by restoreByRecordedEvents() arguments. The machine should not process any events before its restoration (in such case exception will be thrown) as it can possibly lead to incorrect restoration result.

Configuring restoration

restoreByRecordedEvents() accepts the following parameters:

  • muteListeners (default: true) — when true, listener callbacks are suppressed during replay, since the application reactions were already executed when the original events were processed.
  • disableStructureHashCodeCheck (default: false) — skip the machine structure integrity check. Useful when you intentionally restore on a structurally different machine, though results may differ.
  • validator (default: StrictValidator) — called after replay to validate the RestorationResult. The library provides two built-in implementations:
    • StrictValidator — throws RestorationResultValidationException if any warnings or failed processing results are found. This is the default and recommended choice.
    • EmptyValidator — skips validation entirely, useful when you expect and accept warnings.
    • Custom RestorationResultValidator — implement the fun interface to apply your own logic.

Example using EmptyValidator to allow warnings:

machine2.restoreByRecordedEvents(restoredRecordedEvents, validator = EmptyValidator)

See Event recording sample

Active state configuration persistence

An alternative to event recording is to take a snapshot of the currently active states (and their DataState data values). The snapshot has constant size regardless of how many events the machine has processed, making it ideal for long-running machines.

Restoration via restoreBySavedStateConfig() genuinely enters states using the same mechanism as Testing.startFrom(), so listener callbacks fire normally during restore — unlike restoreByRecordedEvents() which suppresses them by default.

How it works

Step 1 — capture (synchronous, no suspend needed):

val snapshot: SavedStateConfig = machine.captureSavedStateConfig()

Step 2 — restore on a freshly constructed machine with identical structure:

// suspending
machine2.restoreBySavedStateConfig(snapshot)

// or blocking
machine2.restoreBySavedStateConfigBlocking(snapshot)

restoreBySavedStateConfig() starts the machine if it has not been started yet.

Prerequisites

The following conditions are verified at capture time (an IllegalStateException is thrown if any fail):

  • The machine must be running.
  • isUndoEnabled must be false — the undo stack cannot be snapshotted and would be empty after restore. Pass disableUndoEnabledCheck = true to opt in: the restored machine starts with an empty undo stack but can record and undo events processed after restoration.
  • All states must have non-blank names. Use requireNonBlankNames with STATES or STATES_AND_TRANSITIONS value in buildCreationArguments {} to enforce this at machine start, or assign names to every state manually.

Serialization

SavedStateConfig can be serialized using kotlinx.serialization. The library provides SavedStateConfigSerializer via KStateMachineSerializersModule (from kstatemachine-serialization artifact).

If your machine has DataState instances, you must register serializers for their value types under Any::class in the SerializersModule:

val jsonFormat = Json {
    serializersModule = KStateMachineSerializersModule + SerializersModule {
        polymorphic(Any::class) {
            subclass(MyData::class)   // your DataState value type(s)
        }
    }
}

val snapshotJson = jsonFormat.encodeToString(snapshot)
// later:
val restoredSnapshot = jsonFormat.decodeFromString<SavedStateConfig>(snapshotJson)
machine2.restoreBySavedStateConfig(restoredSnapshot)

Configuring restoration

restoreBySavedStateConfig() accepts one optional parameter:

  • disableStructureHashCodeCheck (default: false) — skip the machine structure integrity check. Useful when intentionally restoring on a structurally different machine, though results may differ.

Limitations

Limitation Details
History states not restored HistoryState recorded history is not part of the snapshot; after restore it defaults to defaultState or the parent’s initial state
All states must have names Identification relies on state names; capture throws if any state has a null or blank name
isUndoEnabled must be false The undo stack cannot be captured; capture throws if undo is enabled unless disableUndoEnabledCheck = true is passed
Listeners fire during restore Unlike restoreByRecordedEvents, there is no muteListeners option — state entry is genuine

See Saved state config sample


This site uses Just the Docs, a documentation theme for Jekyll.