Zero-dependency state machines, saga orchestration, and immutable containers built for financial-grade reliability and clean architecture.
Every feature designed for zero-error-tolerance environments with comprehensive testing and thread safety.
Declarative DSL with sealed classes for compile-time state transition verification. Guards, side effects, and lifecycle hooks built-in.
Distributed transactions with automatic LIFO compensation. Smart compensation sees state from later steps for intelligent rollback.
Thread-safe containers with atomic operations. State corruption is impossible by construction.
Fully suspend-aware API for non-blocking operations. Perfect integration with Kotlin's structured concurrency.
Custom validators enforce business rules before state transitions. Failed validations are caught, not exceptions.
Observer pattern, saga monitors, and lifecycle events integrate with any logging or telemetry system.
Pluggable journal SPI, crash-resume, effect-key idempotency, and an opt-in SHA-256 hash chain for tamper-evident audit. (v1.7+)
A conversational DSL that reads like documentation
val paymentMachine = stateMachine<PaymentState, PaymentEvent> { initially at Pending during(Pending) { on<Authorize> first do { auditLog.record(it) } then go to Authorized } during(Authorized) { on<Capture> only if { amount > 0 } then go to Captured on<Void> go to Voided } watching changes { old, new -> logger.info("$old -> $new") } }
// Smart compensation: Step 1's undo sees state from Step 3! val saga = sagaExecutor<Cart, Transaction, PaymentState>(PaymentState()) { first `do` "Take Payment" with { cart -> val payment = cashManager.takePayment(cart.total) updateState { it.copy(paymentAmount = payment.amount) } createTransaction(cart, payment) } and undo { // `state.escrowCaptured` is from Step 3! when { state.escrowCaptured -> cashManager.dispenseCashRefund(state.paymentAmount) state.paymentAmount > 0 -> cashManager.returnEscrow() else -> cashManager.disableAcceptors() } } then `do` "Capture Escrow" with { cart -> cashManager.captureEscrow() updateState { it.copy(escrowCaptured = true) } result!! } }
// Saga Interceptor: pre/post-step hooks with veto power val saga = sagaExecutor<Cart, Transaction> { interceptor { phase -> when (phase) { is StepPhase.Before -> if (!authorized(phase.step)) InterceptorOutcome.Veto("not authorized") else InterceptorOutcome.Continue is StepPhase.After -> { audit(phase.step, phase.result); InterceptorOutcome.Continue } is StepPhase.Compensation -> { metrics.compensated(phase.step); InterceptorOutcome.Continue } } } first `do` "Take Payment" with { cart -> takePayment(cart) } and undo { reversePayment() } then `do` "Capture Escrow" with { captureEscrow() } }
// Saga Journal: durable, append-only event log per run val journal: SagaJournal<String> = InMemorySagaJournal() val saga = sagaExecutor<Cart, Transaction>(journal = journal) { first `do` "debit" with { debit(it.account, it.total) } then `do` "credit" with { credit(merchant, it.total) } monitor { event -> if (event is SagaEvent.JournalWriteFailure) alert("journal failed at ${event.phase} seq=${event.seq}", event.cause) } } // Pass runId at execute-time — same definition, distinct logical run saga.execute(cart, runId = RunId("order-${cart.id}")) // write-before-effect: Intent is journaled BEFORE the step body runs. // A journal write failure vetoes the step; no half-applied side effect.
// Resume & Replay: rebuild a crashed saga from the journal (v1.8 / F003) val outcome = executor.resume(runId, context) when (outcome) { is ResumeOutcome.Resumed -> process(outcome.result) is ResumeOutcome.Indeterminate -> alert(outcome.reason) is ResumeOutcome.NotFound -> startFresh() is ResumeOutcome.CorruptJournal -> quarantine(outcome.runId, outcome.reason) } // Or use the conversational form (v1.9 / F006) when (val r = saga resume "order-42" with ctx) { is Resumed -> log("recovered: ${r.result}") is Indeterminate -> reviewQueue.enqueue(r) is NotFound -> saga.execute(ctx, RunId("order-42")) is CorruptJournal -> alert("integrity at seq ${r.atSeq}") } // Re-resuming a Terminal run is idempotent — same outcome, no work re-done. // Crashed-during-compensation runs replay rollback to completion.
// Effect-Key Idempotency: short-circuit a side effect on retry/resume (v1.8 / F004) val saga = sagaExecutor<Cart, ChargeResult, ChargePayload>( journal = journal, decodeEffectPayload = { (it as ChargePayload.Result).result }, encodeEffectPayload = { ChargePayload.Result(it) }, ) { step(OrderStep.CHARGE) { ctx -> chargeService.charge(ctx.orderId, ctx.amount) } effectKey { ctx -> "charge:${ctx.orderId}:${ctx.amount}" } compensate { result -> chargeService.refund(result.txId) } } // First run charges; retry with same runId short-circuits — no second charge. saga.execute(cart, runId = RunId("order-42")) saga.execute(cart, runId = RunId("order-42")) // idempotent // Conversational form: `once per { ctx -> ... }` first call Step.CHARGE with { ctx -> chargeService.charge(ctx.orderId, ctx.amount) } once per { ctx -> "charge:${ctx.orderId}:${ctx.amount}" } otherwise { result -> chargeService.refund(result.txId) }
// Tamper-Evident Hash Chain: SHA-256 chain over the journal (v1.9 / F005) // Off by default; enable with JournalConfig(hashing = Hashing.Sha256). // P99 append overhead < 2ms (AC11 NFR). val saga = sagaExecutor<OrderCtx, OrderResult, OrderEvent>( journal = roomJournal, config = JournalConfig(hashing = Hashing.Sha256), decodeEffectPayload = OrderEventCodec::decode, ) { /* steps */ } when (val r = saga.exposedJournal.verify(RunId("order-42"))) { is VerifyResult.Valid -> log("chain ok: head=${r.headHash}") is VerifyResult.Broken -> alert("tamper at seq ${r.firstBadSeq}") is VerifyResult.Incomplete -> resumeAt(r.resumeFromSeq) } // Conversational form (v1.9 / F006): `protect with tamperEvidence` + `saga audit` val saga = sagaExecutor<OrderCtx, OrderResult, OrderState, OrderEvent>(OrderState.fresh()) { keep audit in roomJournal as OrderEvent protect with tamperEvidence before each step { phase -> if (!auth.ok(phase.step)) halt because "unauthorized" } /* ...steps with `once per { ctx -> ... }` and `otherwise { ... }` ... */ } when (val r = saga audit "order-42") { is Intact -> log("chain ok: ${r.headHash}") is Tampered -> alert("tampered at ${r.atSeq}") is Incomplete -> paginate(r.resumeFromSeq) is Unavailable -> retry("journal i/o: ${r.cause.message}") }
// Thread-safe, validated state container data class AccountState( val balance: BigDecimal, val frozen: Boolean = false ) val account = stateContainer( initialState = AccountState(balance = 1000.0.toBigDecimal()), validator = { state -> if (state.balance < BigDecimal.ZERO) ValidationResult.Invalid("Negative balance") else ValidationResult.Valid } ) // Safe atomic updates val result = account.updateState { state -> state.copy(balance = state.balance - 50.toBigDecimal()) } when (result) { is StateUpdateResult.Success -> println("New balance: ${result.newState.balance}") is StateUpdateResult.ValidationFailure -> println("Rejected: ${result.errors}") }
Pick what you need. Both integrate seamlessly.
Pure Kotlin state management with immutable containers, event-driven state machines, validation pipelines, and the observer pattern.
Coroutine-friendly saga orchestration for distributed transactions with automatic compensation and smart state-aware rollback.
Available on Maven Central. Add to your project in seconds.
dependencies { // Core state management implementation("ca.acendas:kstate:1.9.0") // Saga module (optional) implementation("ca.acendas:kstate-saga:1.9.0") }
dependencies { // Core state management implementation 'ca.acendas:kstate:1.9.0' // Saga module (optional) implementation 'ca.acendas:kstate-saga:1.9.0' }
<dependencies> <!-- Core state management --> <dependency> <groupId>ca.acendas</groupId> <artifactId>kstate</artifactId> <version>1.9.0</version> </dependency> <!-- Saga module (optional) --> <dependency> <groupId>ca.acendas</groupId> <artifactId>kstate-saga</artifactId> <version>1.9.0</version> </dependency> </dependencies>
Join developers building mission-critical applications with KState