# What is left in the system if you die halfway

> A saga engine built around one question. Steps as interceptors, compensation in reverse, and an outbox that makes "the work happened but the notification never went out" structurally impossible.

Page: https://kotlin.website/blog/what-is-left-if-you-die-halfway · Published: 2026-04-12

An operation that spans several services is not one database write. Reserve capacity, claim
a quota, apply the change, hand it to a downstream system, notify. Any step can refuse, and
by then some of the earlier ones are already irreversible.

`try/catch` does not help here. What needs undoing is not a transaction but *actions that
already happened*, in reverse order, and only the ones that really did.

[petich](https://github.com/youndie/petich) is built around that one question: what is left
in the system if you die halfway.

## One interceptor, one step forward and one step back

A step declares both directions:

```text
execute()    — do the thing
compensate() — undo the thing you did
```

Steps run in phases — `ENRICHMENT → VALIDATION → AUTHORIZATION → EXECUTION →
POST_PROCESSING` — ordered by priority within a phase. A failure at step N calls
`compensate()` on N−1 … 1, in reverse.

Putting both halves in one object is the point. When undo lives somewhere else, it drifts
from do, and it drifts silently: nobody runs the compensation path on the happy day.

## Waiting for a human without holding anything

Some sagas stop and wait for a person to confirm. The naive implementation holds a thread,
or a database connection, or both, for however long that takes.

petich suspends: the saga is a value, it goes to storage, and it resumes on a later HTTP
request. Nothing is held in the meantime.

Which immediately raises the real question — what about the one nobody comes back to? A
suspended saga has already claimed resources. Left alone it holds them forever. So the wait
has a deadline, and a background sweeper rolls back what expired.

That pair matters more than the suspend itself. Suspension without a deadline is a resource
leak with better manners.

## The outbox is the interesting part

The classic distributed failure is not a crash. It is this: the work committed, and the
event announcing it did not. Downstream never finds out, and no error was raised anywhere.

With an outbox-aware repository — `petich-postgres` is one — the *intent to emit* is
written in the same transaction as the state change. Either both land or neither does.
There is no window.

A repository without that support still works. The engine falls back to a plain update and
drops the events, which is a deliberate choice: degrade visibly to "no events" rather than
pretend to a guarantee the storage cannot make.

Races are handled by optimistic locking on a version, plus a per-saga mutex inside the
process.

## Three modules that do not depend on the core

| module | what for |
|---|---|
| `petich-core` | the engine: sagas, interceptors, phases, compensation, suspend/resume, TTL |
| `petich-ktor` | REST endpoints for creating and resuming a saga |
| `petich-postgres` | storage on Exposed — outbox, idempotency, scheduler |
| `petich-outbox-core` | at-least-once delivery with backoff and dead lettering |
| `petich-idempotency` | protection against a key reused with a *different* request |
| `petich-scheduler` | a saga on a schedule, with no HTTP initiator |

The last three know nothing about sagas. `petich-outbox-core` knows only about a row —
id, type, payload, deliver at least once. `petich-scheduler` knows "it is time" and "here
is the payload". `petich-idempotency` knows "this key already arrived with a different
fingerprint".

That independence is not tidiness. It is the condition under which they stay usable on
their own instead of turning into part of somebody's feature.

`petich-postgres` also ships no driver and no connection pool. It takes an Exposed
`Database` and does not care what is underneath it — choosing a driver is the
application's decision, not a library's.

---

Source: [github.com/youndie/petich](https://github.com/youndie/petich)
