Why it exists
An operation spanning several services is not one database write. Reserve capacity, claim a quota, apply the change, hand it downstream, notify — any step can refuse, and by then some of the earlier ones are already irreversible. try/catch does not help: what needs undoing is not a transaction but actions that already happened, in reverse, and only the ones that really did.
What you get
- One interceptor, both directions. A step declares
execute()andcompensate()in the same object, so undo cannot quietly drift from do. - Phases —
ENRICHMENT → VALIDATION → AUTHORIZATION → EXECUTION → POST_PROCESSING— with priority inside each. - Waiting for a human without holding a thread or a connection: the saga is a value, it goes to storage and resumes on a later request. With a deadline, because a suspended saga nobody returns to is a resource leak with better manners.
- An outbox in the same transaction as the state change, which makes "the work happened but the notification never went out" structurally impossible. A repository without outbox support still works — the engine degrades visibly to no events rather than pretending.
- Optimistic locking by version plus a per-saga mutex.
Install
repositories { maven("https://reposilite.kotlin.website/snapshots") }
dependencies {
implementation("io.github.youndie:petich-core:0.1.0.2")
implementation("io.github.youndie:petich-ktor:0.1.0.2")
implementation("io.github.youndie:petich-postgres:0.1.0.2")
}
petich-outbox-core, petich-idempotency and petich-scheduler deliberately do not depend on the core, and are usable on their own.
petich-postgres ships no driver and no connection pool: it takes an Exposed Database and does not care what is underneath. Choosing a driver is the application's decision.