A declarative DSL for defining state transitions with compile-time type checking, composable guards, chainable actions, and full thread-safe immutability.
Every design decision prioritizes compile-time correctness and developer ergonomics.
Compile-time type checking for states, events, and context. Invalid transitions are caught before runtime.
Composable boolean predicates with and, or, not operators for expressive transition rules.
Side effects and context transformations with the then operator for clean, sequential processing.
Automatic state management through Stateful<S, C> for seamless integration with your domain models.
Hook into state changes for logging, event publishing, auditing, or any cross-cutting concerns.
All transitions produce new context instances. No mutation, no race conditions, full thread safety guaranteed.
Define complex state machines with a clean Kotlin DSL that reads like a specification.
1val machine = stateMachine<DocumentStatus, DocumentEvent, Document> { 2 from(DocumentStatus.DRAFT) { 3 on<DocumentEvent.Publish>() goto DocumentStatus.PUBLISHED 4 on<DocumentEvent.Archive>() goto DocumentStatus.ARCHIVED 5 } 6 from(DocumentStatus.PUBLISHED) { 7 on<DocumentEvent.Archive>() goto DocumentStatus.ARCHIVED 8 } 9} 10 11val doc = Document(DocumentStatus.DRAFT, "Hello World") 12val result = machine.fire(doc, DocumentEvent.Publish) 13// result.newState == DocumentStatus.PUBLISHED
1val machine = stateMachine<DocumentStatus, DocumentEvent, Document> { 2 from(DocumentStatus.DRAFT) { 3 on<DocumentEvent.Publish>() goto DocumentStatus.PUBLISHED guardedBy { 4 it.content.isNotBlank() 5 } action { doc, _ -> 6 doc.copy(publishedAt = Instant.now()) 7 } 8 } 9} 10 11// Guard prevents transition when content is blank 12val emptyDoc = Document(DocumentStatus.DRAFT, "") 13machine.canFire(emptyDoc, DocumentEvent.Publish) // false
1// Define reusable guard predicates 2val notBlank: Guard<Document> = Guard { it.content.isNotBlank() } 3val longEnough: Guard<Document> = Guard { it.content.length >= 10 } 4 5// Compose with boolean operators 6val publishable = notBlank and longEnough 7val archivable = notBlank or Guard { it.state == DocumentStatus.PUBLISHED } 8val notArchived = !Guard<Document> { it.state == DocumentStatus.ARCHIVED } 9 10// Chainable actions with 'then' 11val setTimestamp: Action<Document, DocumentEvent> = Action { doc, _ -> 12 doc.copy(publishedAt = Instant.now()) 13} 14val normalize: Action<Document, DocumentEvent> = Action { doc, _ -> 15 doc.copy(content = doc.content.trim().lowercase()) 16} 17val publishAction = setTimestamp then normalize
1val machine = stateMachine<DocumentStatus, DocumentEvent, Document> { 2 from(DocumentStatus.DRAFT) { 3 on<DocumentEvent.Publish>() goto DocumentStatus.PUBLISHED 4 on<DocumentEvent.Archive>() goto DocumentStatus.ARCHIVED 5 } 6 from(DocumentStatus.PUBLISHED) { 7 on<DocumentEvent.Archive>() goto DocumentStatus.ARCHIVED 8 } 9 10 // Hook into every state transition 11 onTransition { from, event, to -> 12 logger.info("Transition: $from -> $to via $event") 13 eventBus.publish(StateChanged(from, to)) 14 } 15}
Available via JitPack. Add the repository and dependency to your Gradle build file.
// settings.gradle.kts dependencyResolutionManagement { repositories { mavenCentral() maven { url = uri("https://jitpack.io") } } }
// build.gradle.kts dependencies { implementation("com.github.Tianea2160:statemachine:v1.0.0") }
A minimal surface area that covers complex state management needs.
| Component | Type | Description |
|---|---|---|
State |
interface | Marker interface for state types (typically enum classes) |
Event |
interface | Marker interface for event types (typically sealed interfaces) |
Stateful<S, C> |
interface | Context interface with state property and withState() method |
Guard<C> |
fun interface | Predicate (C) -> Boolean with and, or, not operators |
Action<C, E> |
fun interface | Function (C, E) -> C with chainable then operator |
fire(model, event) |
method | Execute a transition, returns TransitionResult<S, C> |
canFire(model, event) |
method | Check if a transition is valid without executing it |
availableEvents(model) |
method | Get all valid events for the current state and context |