Skip to content

Transactions

The katalyst-transactions module provides transactional execution for services, with configurable timeouts, retry, and isolation. It also coordinates with the event bus so events published inside a transaction are delivered only after commit. The transaction manager is injected into every Service.

For usage in context, see Define tables and repositories and Publish and handle events.

Service transaction helpers

Service exposes the transaction manager and three suspend helpers. All run their block in a managed transaction; an exception rolls everything back.

// The injected manager
val transactionManager: DatabaseTransactionManager

suspend fun <T> transaction(
    workflowId: String? = null,
    config: TransactionConfig? = null,
    block: suspend Transaction.() -> T
): T

suspend fun <T> workflowTransaction(
    workflowId: String? = null,
    config: TransactionConfig? = null,
    block: suspend Transaction.() -> T
): T

suspend fun <T> currentTransaction(block: suspend Transaction.() -> T): T
class AuthService(private val repository: AuthAccountRepository) : Service {
    suspend fun activate(id: Long) = transaction {
        val account = repository.findById(id)
        repository.save(account.copy(status = "active"))
    }
}
  • transaction — the standard entry point. Begins (or joins) a transaction, commits on success, rolls back on failure.
  • workflowTransaction — a transaction tied to a workflow id, for multi-step operations tracked across phases.
  • currentTransaction — runs the block within the transaction already in progress on the current context.

You can also call transactionManager.transaction { … } directly; the helpers are conveniences.

TransactionConfig

Passed to any helper to tune a single transaction.

Field Type Default Meaning
timeout Duration 30 s Maximum execution time; exceeding it cancels with a timeout.
retryPolicy RetryPolicy RetryPolicy() How failed transactions retry.
isolationLevel TransactionIsolationLevel READ_COMMITTED Concurrency isolation. Applied to the JDBC connection for the transaction. See TransactionIsolationLevel for how the default interacts with the pool-wide setting.
expectedBusinessExceptions Set<KClass<out Exception>> empty Treated as expected failures for log-severity purposes only; does not change retry behavior.
phaseLoggingEnabled Boolean true Verbose phase/adapter logging for this transaction.
import kotlin.time.Duration.Companion.seconds

suspend fun chargeOnce() = transaction(
    config = TransactionConfig(
        timeout = 10.seconds,
        isolationLevel = TransactionIsolationLevel.SERIALIZABLE
    )
) { /* … */ }

RetryPolicy

Field Type Meaning
maxRetries Int Maximum retry attempts.
backoffStrategy BackoffStrategy IMMEDIATE, LINEAR, or EXPONENTIAL.
initialDelayMs Long First backoff delay.
maxDelayMs Long Upper bound on backoff delay.
jitterFactor Double Randomization applied to backoff (0.0–1.0).
retryableExceptions Set<KClass<…>> Exception types that trigger a retry.
nonRetryableExceptions Set<KClass<…>> Exception types that never retry.

The default policy retries with exponential backoff up to 3 retries.

TransactionIsolationLevel

READ_UNCOMMITTED, READ_COMMITTED (default), REPEATABLE_READ, SERIALIZABLE.

A level other than the READ_COMMITTED default is applied to the JDBC connection that runs the transaction, overriding the pool-wide database.transactionIsolation from application.yaml (HikariCP default: TRANSACTION_REPEATABLE_READ).

READ_COMMITTED is the default value of the field and there is no separate "unspecified" constant, so it means inherit: the connection keeps the pool-wide level. Configure database.transactionIsolation when you want TRANSACTION_READ_COMMITTED everywhere.

Isolation is a property of the connection, so it can only be set when a transaction opens. A transaction { … } nested inside an already-open transaction joins it and keeps that transaction's level; if the nested call asked for a different level, the manager logs a warning instead of ignoring the request silently.

DatabaseTransactionManager

The implementation of TransactionManager, injected into services.

Member Purpose
transaction(workflowId, config, block) Run a block transactionally.
addAdapter(adapter) / removeAdapter(adapter) Register/unregister a TransactionAdapter.
getAdapterCount() Number of registered adapters.

Transaction adapters

A TransactionAdapter participates in transaction phases — for example, the event bus registers one so publishing defers to commit. Phases are modeled by TransactionPhase, and TransactionPhaseTracer / TransactionPhaseEvent expose phase tracing. The persistence and events modules register their adapters automatically; you rarely implement one directly.

Exceptions

Exception Meaning
TransactionFailedException The transaction failed and will not be retried.
TransactionTimeoutException The transaction exceeded its timeout.
DeadlockException A database deadlock was detected.
TransientException A retryable, transient failure.

Log severity for non-retryable failures is decided by a TransactionExceptionSeverityClassifier (INFO, WARN, ERROR); the default classifier logs expected business failures at WARN and unexpected ones at ERROR.

See also