v1.9.0 - Durable Saga Execution (E001) Complete

State Management for
Mission-Critical Kotlin

Zero-dependency state machines, saga orchestration, and immutable containers built for financial-grade reliability and clean architecture.

0
Dependencies
581
Test Cases
<1ms
Latency
80%+
Coverage

Built for Production

Every feature designed for zero-error-tolerance environments with comprehensive testing and thread safety.

🎯

Type-Safe State Machines

Declarative DSL with sealed classes for compile-time state transition verification. Guards, side effects, and lifecycle hooks built-in.

🔄

Saga Orchestration

Distributed transactions with automatic LIFO compensation. Smart compensation sees state from later steps for intelligent rollback.

🔒

Immutable by Design

Thread-safe containers with atomic operations. State corruption is impossible by construction.

Coroutine Native

Fully suspend-aware API for non-blocking operations. Perfect integration with Kotlin's structured concurrency.

🧪

Validation Pipeline

Custom validators enforce business rules before state transitions. Failed validations are caught, not exceptions.

🔍

Full Observability

Observer pattern, saga monitors, and lifecycle events integrate with any logging or telemetry system.

📓

Durable Saga Execution

Pluggable journal SPI, crash-resume, effect-key idempotency, and an opt-in SHA-256 hash chain for tamper-evident audit. (v1.7+)

Beautiful, Readable Code

A conversational DSL that reads like documentation

Example.kt
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}")
}

Two Modules, One Mission

Pick what you need. Both integrate seamlessly.

Quick Installation

Available on Maven Central. Add to your project in seconds.

build.gradle.kts
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>

Ready to Build Reliable Systems?

Join developers building mission-critical applications with KState

Read the Docs Star on GitHub