Finite state machine utilities library for Kotlin.
This small library contains several implementations for common use cases.
io.github.ngirchev.fsm.impl.extended.ExFsm- a simple fsm that has a status, and it changes the status by events. Has state.io.github.ngirchev.fsm.impl.extended.ExDomainFsm- if you have some domain with a status, and you want to change this status using events. Has no own state.
You can also use the io.github.ngirchev.fsm.impl package with basic implementations.
dependencies {
implementation("io.github.ngirchev:fsm:1.0.2")
}dependencies {
implementation 'io.github.ngirchev:fsm:1.0.2'
}<dependency>
<groupId>io.github.ngirchev</groupId>
<artifactId>fsm</artifactId>
<version>1.0.2</version>
</dependency>data class Document(
val id: String = UUID.randomUUID().toString(),
override var state: DocumentState = DocumentState.NEW,
val signRequired: Boolean = false
) : StateContext<DocumentState>
enum class DocumentState {
NEW, READY_FOR_SIGN, SIGNED, AUTO_SENT, DONE, CANCELED
}
fun main() {
val fsm = FsmFactory.statesWithEvents<String, String>()
.add(from = "NEW", to = "READY_FOR_SIGN", onEvent = "TO_READY")
.add(from = "READY_FOR_SIGN", to = "SIGNED", onEvent = "USER_SIGN")
.add(from = "READY_FOR_SIGN", to = "CANCELED", onEvent = "FAILED_EVENT")
.add(from = "SIGNED", to = "AUTO_SENT")
.add(from = "AUTO_SENT", to = "DONE", onEvent = "SUCCESS_EVENT")
.add(from = "AUTO_SENT", to = "CANCELED", onEvent = "FAILED_EVENT")
.build()
.createFsm("NEW")
println("Initial state: ${fsm.getState()}")
try {
fsm.onEvent("FAILED_EVENT")
} catch (ex: Exception) {
println("$ex")
}
println("State still the same: ${fsm.getState()}")
fsm.onEvent("TO_READY")
fsm.onEvent("USER_SIGN")
fsm.onEvent("SUCCESS_EVENT")
println("Terminal state is DONE = ${fsm.getState()}")
}
There are two transitions from the status READY_FOR_SIGN:
SIGNEDif eventUSER_SIGNwill be thrown.CANCELEDif eventFAILED_EVENTwill be thrown. And two transitions from the statusAUTO_SENT:DONEif eventSUCCESS_EVENTwill be thrown.CANCELEDif eventFAILED_EVENTwill be thrown.
fun main() {
val document = Document(signRequired = true)
val fsm = FsmFactory
.statesWithEvents<DocumentState, String>()
.add(from = NEW, onEvent = "TO_READY", to = READY_FOR_SIGN)
.add(from = READY_FOR_SIGN, onEvent = "USER_SIGN", to = SIGNED)
.add(from = READY_FOR_SIGN, onEvent = "FAILED_EVENT", to = CANCELED)
.add(from = SIGNED, onEvent = "FAILED_EVENT", to = CANCELED)
.add(
from = SIGNED, onEvent = "TO_END", // switch case example
To(AUTO_SENT, condition = { document.signRequired }), // first
To(DONE, condition = { !document.signRequired }), // second
To(CANCELED) // else
)
.add(from = AUTO_SENT, onEvent = "TO_END", to = DONE)
.build()
.createDomainFsm<Document>()
try {
fsm.handle(document, "FAILED_EVENT")
} catch (ex: Exception) {
println("$ex")
}
println("State still the same - NEW = ${document.state}")
fsm.handle(document, "TO_READY")
println("READY_FOR_SIGN = ${document.state}")
fsm.handle(document, "USER_SIGN")
println("SIGNED = ${document.state}")
fsm.handle(document, "TO_END")
println("AUTO_SENT = ${document.state}")
fsm.handle(document, "TO_END")
println("Terminal state is DONE = ${document.state}")
}
There are we add new extra steps. From SIGNED we have 3 different transitions for only one event TO_END:
AUTO_SENTif conditiondocument.signRequiredwill betrue.DONEif condition!document.signRequiredwill betrue.CANCELEDif both previous conditions werefalse(definitely this case impossible, but you can change conditions forfalsein both cases).
We rewrite code with the same transitions
fun main() {
val document = Document(signRequired = true)
val fsm = FsmFactory.statesWithEvents<DocumentState, String>()
.from(NEW).to(READY_FOR_SIGN).onEvent("TO_READY").end()
.from(READY_FOR_SIGN).toMultiple()
.to(SIGNED).onEvent("USER_SIGN").end()
.to(CANCELED).onEvent("FAILED_EVENT").end()
.endMultiple()
.from(SIGNED).onEvent("TO_END").toMultiple()
.to(AUTO_SENT).condition { document.signRequired }.end()
.to(DONE).condition { !document.signRequired }.end()
.to(CANCELED).end()
.endMultiple()
.from(AUTO_SENT).onEvent("TO_END").to(DONE).end()
.build().createDomainFsm<Document>()
try {
fsm.handle(document, "FAILED_EVENT")
} catch (ex: Exception) {
println("$ex")
}
println("State still the same - NEW = ${document.state}")
fsm.handle(document, "TO_READY")
println("READY_FOR_SIGN = ${document.state}")
fsm.handle(document, "USER_SIGN")
println("SIGNED = ${document.state}")
fsm.handle(document, "TO_END")
println("AUTO_SENT = ${document.state}")
fsm.handle(document, "TO_END")
println("Terminal state is DONE = ${document.state}")
}
fun main() {
val fsm = ExFsm("INITIAL", ExTransitionTable.Builder<String, String>()
.add(ExTransition(from = "INITIAL", to = "GREEN", onEvent = "RUN"))
.add(ExTransition(from = "RED", to = To("GREEN", timeout = Timeout(3), action = { println(it) })))
.add(ExTransition(from = "GREEN", to = To("YELLOW", timeout = Timeout(3), action = { println(it) })))
.add(ExTransition(from = "YELLOW", to = To("RED", timeout = Timeout(3), action = { println(it) })))
.build())
fsm.onEvent("RUN")
}
OR
fun main() {
val fsm = FsmFactory.statesWithEvents<String, String>()
.from("INITIAL").to("GREEN").onEvent("RUN").end()
.from("RED").to("GREEN").timeout(Timeout(3)).action { println(it) }.end()
.from("GREEN").to("YELLOW").timeout(Timeout(3)).action { println(it) }.end()
.from("YELLOW").to("RED").timeout(Timeout(3)).action { println(it) }.end()
.build().createFsm("INITIAL")
fsm.onEvent("RUN")
}
The library supports diagram generation in PlantUML and Mermaid formats for visualizing finite state machines.
import io.github.ngirchev.fsm.diagram.*
// Create FSM
val transitionTable = ExTransitionTable.Builder<DocumentState, String>()
.from(NEW).to(READY_FOR_SIGN).onEvent("TO_READY").end()
.from(READY_FOR_SIGN).to(SIGNED).onEvent("USER_SIGN").timeout(Timeout(1)).end()
.from(SIGNED).to(DONE).onEvent("TO_END").end()
.build()
// Generate diagrams
println(transitionTable.toPlantUml())
println(transitionTable.toMermaid())For more readable diagrams, use NamedAction and NamedGuard:
// Define named actions and conditions
val chargeCard = NamedAction<Any>("ChargeCard") { /* ... */ }
val sendReceipt = NamedAction<Any>("SendReceipt") { /* ... */ }
val isPaymentValid = NamedGuard<Any>("IsPaymentValid") { true }
// Build FSM
val fsm = ExTransitionTable.Builder<OrderState, String>()
.from(NEW)
.onEvent("PAY")
.to(PAID)
.onCondition(isPaymentValid) // Displayed on arrow: [IsPaymentValid]
.action(chargeCard) // Displayed in PAID state: βΆ ChargeCard
.postAction(sendReceipt) // Displayed in PAID state: β SendReceipt
.timeout(Timeout(30))
.end()
.build()PlantUML:
@startuml
state "NEW" as NEW
state "PAID" as PAID {
PAID : βΆ ChargeCard
PAID : β SendReceipt
}
NEW --> PAID : [PAY] [IsPaymentValid] β±30SECONDS
@endumlMermaid:
stateDiagram-v2
state PAID {
PAID : βΆ ChargeCard
PAID : β SendReceipt
}
NEW --> PAID : PAY [IsPaymentValid] β±30s
Inside states:
βΆ ActionName- action executed when entering the stateβ PostActionName- action executed after entering the state
On transition arrows:
[EVENT]- transition event[ConditionName]- transition conditionβ±30s- timeout
- PlantUML: Online Editor | IntelliJ Plugin
- Mermaid: Live Editor | IntelliJ Plugin | GitHub automatically renders Mermaid in markdown
// Print to console
transitionTable.printPlantUml()
transitionTable.printMermaid()
// Get string
val plantUml: String = transitionTable.toPlantUml()
val mermaid: String = transitionTable.toMermaid()
// Save to file
transitionTable.toPlantUml(Path("diagram.plantuml"))
transitionTable.toMermaid(Path("diagram.mermaid"))- π Simple and lightweight finite state machine implementation
- π Support for state diagrams generation (PlantUML and Mermaid)
- π Multiple FSM implementations for different use cases
- β±οΈ Support for timeouts and actions
- π― Type-safe state transitions
- π§ͺ Fully tested with JUnit 5
- π Logging support via SLF4J
This project is buildable using only FLOSS (Free/Libre and Open Source Software) tools.
- Java 11 or higher (OpenJDK recommended - FLOSS)
- Gradle 7.0+ (Gradle - FLOSS)
- Kotlin 1.6.21+
All build tools, dependencies, and test frameworks used are FLOSS.
This project uses Semantic Versioning (MAJOR.MINOR.PATCH, e.g. 1.0.0).
Each release has a unique version identifier, is tagged in Git as v{version} (e.g. v1.0.0).
Development builds use the -SNAPSHOT suffix (e.g. 1.0.3-SNAPSHOT) and are not intended for production use.
This project uses an automated test suite that is publicly released as FLOSS.
The project uses JUnit 5 (JUnit Jupiter) as the test framework, which is:
- Publicly released as FLOSS (licensed under the Eclipse Public License 2.0)
- Maintained as a separate FLOSS project
- Standard testing framework for Java/Kotlin applications
The test suite includes comprehensive unit tests covering:
- FSM state transitions
- Event handling
- Timeout functionality
- Error handling and edge cases
- Diagram generation
The test suite is invocable in a standard way for Java/Kotlin projects using Gradle.
To run the automated test suite:
./gradlew testThis is the standard Gradle command for running tests. The command will:
- Compile the source code
- Compile the test code
- Execute all tests using JUnit 5
- Display test results
Alternative ways to run tests:
# Run tests with verbose output
./gradlew test --info
# Run a specific test class
./gradlew test --tests "io.github.ngirchev.fsm.impl.basic.BFsmTest"
# Run tests and generate coverage report
./gradlew test jacocoTestReportThe project uses JaCoCo for code coverage analysis.
JaCoCo is:
- A FLOSS Java code coverage library
- Integrated into the Gradle build process
- Automatically generates coverage reports during testing
- Enforces minimum coverage thresholds to maintain code quality
Coverage requirements:
- Minimum line coverage: 80%
- Minimum branch coverage: 70%
Coverage reports are generated automatically during the build and can be viewed at build/reports/jacoco/test/html/index.html after running ./gradlew test jacocoTestReport.
To check coverage thresholds:
./gradlew jacocoTestCoverageVerificationThe build will fail if coverage thresholds are not met.
The project uses the following tools for code quality:
- Detekt - Static code analysis for Kotlin
- Kotlinter - Kotlin linter and formatter
To run code quality checks:
# Run Detekt
./gradlew detekt
# Run Kotlinter
./gradlew lintKotlinMain lintKotlinTestThe project uses GitHub Actions for continuous integration:
- CI Pipeline (
.github/workflows/ci.yml):- Runs on every push and pull request
- Builds the project and runs all tests
- Executes code quality checks (Detekt, Kotlinter)
- Generates and verifies code coverage reports
- Uploads build artifacts and reports
CI Pipeline URL: https://github.com/NGirchev/fsm/actions
Before publishing to Maven Central, you need to:
-
Create a Sonatype OSSRH account:
- Sign up at https://s01.oss.sonatype.org/
- Create a JIRA ticket to request access for your groupId (
io.github.ngirchev) - Wait for approval (usually 1-2 business days)
-
Set up GPG signing:
- Install GPG (if not already installed)
- Generate a GPG key:
gpg --gen-key - Export your public key:
gpg --keyserver keyserver.ubuntu.com --send-keys <your-key-id> - Export your private key for use in Gradle (see below)
-
Configure credentials:
Gradle will automatically read credentials from Maven
~/.m2/settings.xml(same as Maven uses):<settings> <servers> <server> <id>central</id> <username>your-sonatype-username</username> <password>your-sonatype-password</password> </server> </servers> <profiles> <profile> <id>release</id> <properties> <gpg.executable>gpg</gpg.executable> <gpg.keyname>your-gpg-key-id</gpg.keyname> <gpg.passphrase>your-gpg-passphrase</gpg.passphrase> </properties> </profile> </profiles> </settings>
Alternatively, you can use
~/.gradle/gradle.properties:ossrhUsername=your-sonatype-username ossrhPassword=your-sonatype-password signingKeyId=your-gpg-key-id signingPassword=your-gpg-passphrase
Or use the properties format supported by the
com.vanniktech.maven.publishplugin:mavenCentralUsername=your-sonatype-username mavenCentralPassword=your-sonatype-password signing.gnupg.keyName=your-gpg-key-id signing.gnupg.passphrase=your-gpg-passphrase signing.gnupg.executable=gpg
Security Note: Never commit these credentials to the repository. Use
~/.m2/settings.xmlor~/.gradle/gradle.properties(both are typically in.gitignore).
To create a release and publish to Maven Central:
# 1. Update CHANGELOG.md with release notes
# 2. Prepare release (this will update version in gradle.properties and create Git tag)
# Option 1: Specify version manually (recommended for major/minor releases)
./gradlew release -Prelease.version=1.2.0
# Option 2: Auto-increment patch version (only increments the last number, e.g., 1.0.2 -> 1.0.3)
./gradlew release -Prelease.useAutomaticVersion=true
# 3. Build and test
./gradlew clean build
# 4. Publish to Maven Central staging repository
./gradlew publish
# 5. After successful upload, go to https://s01.oss.sonatype.org/
# - Login and navigate to "Staging Repositories"
# - Find your repository (starts with iogithubngirchev)
# - Close the repository (this validates the artifacts)
# - Release the repository (this syncs to Maven Central)
# - Wait 10-30 minutes for sync to Maven Central
# 6. Push changes and tags (release plugin creates the tag automatically)
git push origin master
git push --tags
# 7. Create GitHub release with CHANGELOG notes
# Replace v1.0.2 with your actual version tag (created by release plugin)
gh release create v1.0.2 -F CHANGELOG.md
# Attach artifacts and their signatures (for OpenSSF Security Score)
gh release upload v1.0.2 build/libs/*.jar build/libs/*.jar.asc --clobberNote:
- The
releaseplugin automatically:- Updates version in
gradle.properties(removes -SNAPSHOT) - Creates a Git tag with format
v{version}(e.g.,v1.0.2) - Commits version changes
- Updates version in
- After publishing, artifacts will be available at: https://repo1.maven.org/maven2/io/github/ngirchev/fsm/
- Maven Central sync usually takes 10-30 minutes after release
Contributions are welcome! We appreciate your help in making this project better.
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature-name) - Make your changes
- Run tests (
./gradlew test) - Run code quality checks (
./gradlew detekt lintKotlinMain lintKotlinTest) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin feature/your-feature-name) - Create a Pull Request
All contributions must meet our requirements for acceptable contributions, including:
- Coding Standards: Follow Kotlin code conventions and maintain consistent style
- Testing: Ensure all tests pass and add tests for new features
- Documentation: Add KDoc comments for public APIs
- Code Quality: Write clean, readable, and maintainable code
For detailed contribution requirements, coding standards, and guidelines, please see CONTRIBUTING.md.
Found a bug or have an idea for improvement? We'd love to hear from you!
Issue Tracker URL: https://github.com/NGirchev/fsm/issues
To submit a bug report:
- Go to the GitHub Issues page
- Click "New Issue"
- Select "Bug Report" template (if available) or create a new issue
- Include the following information:
- Description: Clear description of the problem
- Steps to Reproduce: Detailed steps to reproduce the issue
- Expected Behavior: What you expected to happen
- Actual Behavior: What actually happened
- Environment: Java version, OS, library version, etc.
- Code Example: Minimal code example that demonstrates the issue (if applicable)
Language: Please submit bug reports, feature requests, and comments in English to ensure they can be understood and addressed by the global developer community.
To suggest an enhancement or new feature:
- Go to the GitHub Issues page
- Click "New Issue"
- Select "Feature Request" template (if available) or create a new issue with the
enhancementlabel - Describe the enhancement, its use case, and potential benefits
See LICENSE file for details.
- NGirchev - Creator and maintainer