Index

Data That Must Survive

A Transaction Defines What Must Succeed Together

A booking flow reserved funds, completed the remote booking, and captured the funds. Only then did synchronous response processing fail.

The remote booking existed. The funds were captured. The caller received a failure.

The capture was individually valid, and its own database transaction committed cleanly. It was still too early. We had drawn the success boundary around “remote booking returned” while the operation promised something larger: a complete booking result which the caller could use.

The correction moved capture behind response construction. Failures before that boundary left the reservation pending so cleanup could release it. The database transaction did not become larger. The definition of success became more precise.

Atomicity needs a business sentence

“Put it in a transaction” is incomplete advice. A transaction can make any convenient block atomic, including the wrong block.

Start with the durable statement which must never become half true:

a pending funds reservation consists of
    one reservation
    one pending financial transaction
    its pending transaction lines

Those records live in one database. If one is missing, the reservation cannot be priced, balanced, captured, released, or audited reliably. Creating them in one local transaction follows the business state transition.

This is different from wrapping an entire controller because it performs several queries. Controllers often include validation, remote calls, response formatting, events, email, and serialization. Their source-code boundary says little about which durable facts belong together.

A useful transaction sentence names:

  • the state before the operation;
  • every local fact created or changed;
  • the state after commit; and
  • what observers are allowed to see after rollback.

For the reservation, rollback should reveal none of the reservation, transaction, or lines. Commit should reveal all of them in their pending states. That is a testable unit.

Remote effects do not join a local rollback

The booking flow crossed at least two durable authorities:

billing database:
    reserve -> pending
    capture -> captured
    release -> released

remote booking system:
    request -> booking may exist

application database:
    assemble local result
    persist documents and identifiers

No ordinary database transaction spans those authorities. Holding a local transaction open while making the remote request does not change that fact. If the remote call succeeds and the local commit fails, the remote booking stays successful. If the local transaction holds locks during the call, it also turns remote latency into lock duration.

The correct mental model is not one giant rollback. It is a sequence of named, durable states with deliberate compensation:

reserve funds
    -> reversible pending state

attempt remote booking
    -> may create an external effect

finish the promised local result
    -> documents, identifiers, response shape

capture funds
    -> success is now consumable

on earlier failure
    -> release pending funds
    -> reconcile any external effect separately

Compensation is not rollback. A release is a new fact, performed later, which can fail independently. It needs an idempotency rule, retry policy, terminal state, and evidence of completion.

A reservation buys time without lying

Charging only after every downstream action completes can permit concurrent requests to spend the same available balance. Charging immediately avoids that race but makes later failures look successful financially.

A reservation separates availability from finality:

available balance decreases while pending
ledger remains explicitly pending
capture turns the hold into a completed charge
release returns the held amount
expiry closes abandoned holds

The retained implementation creates the reservation, a pending transaction, and its pending lines under an account lock. It also checks an idempotency key inside that transaction. That gives competing requests one serialized decision about available funds without pretending that booking has succeeded.

The pending state is not a vague “in progress” flag. It grants a specific option: capture after success or release after failure. If a pending record cannot be discovered, expired, or reconciled, it merely postpones ambiguity.

This is why the transaction owns more than one row. The reservation is the workflow handle; the transaction and lines are the financial evidence. They must appear together even though later capture belongs to another transition.

Success ends where the caller’s promise is usable

The original capture point followed the remote booking call. That looked reasonable because the expensive external operation had succeeded.

More synchronous work still had to happen:

  • dispatch local lifecycle behavior;
  • load generated identifiers;
  • produce documents;
  • build one of the supported response forms; and
  • return a coherent result.

A failure in that interval left a captured reservation associated with an operation the caller could not consume. Retained history says the resulting financial records required manual exclusion. It does not retain a count, monetary total, or customer-impact measurement.

Moving capture later aligned it with the observable promise:

PointRemote bookingResult usableFunds state
after reservationnonopending
after remote successyesnot yetpending
after response constructionyesyescaptured
failure before resultmaybenorelease pending

“Response construction” is specific to this flow, not a universal transaction boundary. Another operation might define success as durable event publication, document storage, or acceptance into an owned queue. The method is to name the promise and find the last fallible step required to make it true.

Capturing later creates a different failure window

Deferring capture improves agreement between the result and the charge. It does not produce perfection.

Capture itself is remote and fallible. The application may have a usable booking result ready while the capture request times out. The remote side may have accepted the capture even though the caller did not receive its response. A release attempted after a failed capture can also time out.

The retained flow handles a capture exception by attempting release, retains the pending reservation when release cannot be confirmed, and tries cleanup again during finalization. This is bounded evidence for one implementation; it does not prove that every unknown remote outcome is automatically safe.

Each transition needs its own identity and state interpretation:

reserve with operation key:
    same key returns the same open reservation

capture with reservation identity:
    repeated request must not charge twice

release with reservation identity:
    repeated request must not return funds twice

timeout:
    outcome unknown until queried or reconciled

An exception is not evidence that the remote transition failed. Where the remote API supports lookup, reconciliation should inspect the authoritative state before choosing capture, release, or manual review.

Keep transactions short, but not artificially small

Short transactions reduce lock duration and contention. That is a good constraint, not the definition of correct scope.

Splitting reservation creation into three commits would be short:

commit reservation
commit pending transaction
commit transaction lines

It would also expose states which the business cannot use. A worker could observe the reservation before its accounting evidence exists. A crash could make the incomplete state permanent.

The better split follows authorities and recoverable transitions:

local transaction:
    lock account
    check available funds and idempotency
    create reservation, transaction, and lines
    commit pending state

outside transaction:
    call remote booking system
    build the complete local result

remote transition:
    capture or release the reservation

Do not perform network calls while holding the account lock merely to make the code look atomic. Commit the reversible local state, release the lock, and coordinate later effects through the reservation identity.

If the local transaction becomes slow because it calculates or loads unrelated data, prepare that data before acquiring the lock. Revalidate only the facts which can change under concurrency after the lock is held.

The fixture refuses false rollback

The DATA-02 fixture executes a small state model with three authorities. It verifies that reservation, transaction, and lines commit together; a local failure leaves none of them. It then compares early and late capture when response construction fails.

Under early capture, the fixture ends with a remote booking, captured funds, and no usable result. Under late capture, the same failure leaves funds pending for release. A successful path captures only after the result is complete. A failed release remains pending rather than being reported as released.

The model does not run a database, make network requests, prove isolation behavior, or reproduce the retained system. Its transition names and values are synthetic. Retained tests establish the private behavior; the fixture makes the reasoning inspectable.

One transaction cannot own a distributed promise

There are alternatives. A transactional outbox can commit a local state change and an owed message together, then let a worker perform the remote transition. A process manager can persist each step and compensation. A provider which offers an atomic authorize-and-finalize operation may remove one application state. A distributed transaction may be available inside a tightly controlled infrastructure boundary.

Each option changes latency, availability, operational ownership, and what the caller is promised. None makes an external effect disappear when a later local step fails.

Reverse the design if capture must happen before result construction for legal, inventory, or provider reasons. Then the API cannot honestly promise all-or- nothing booking. It needs a durable accepted or processing response, a status resource, and reconciliation for the interval after capture.

A transaction is correct when its commit turns one complete business sentence from false to true. Across authorities, use states and compensation to connect several such sentences. The code block is incidental; the durable promise is the boundary.