Failed transaction: releasing the hold
Every reservation needs a release path, or pending lives forever.
By Solomon Ajayi · Free to read, no signup
A user initiated a ₦5,000 withdrawal yesterday and your code dutifully held the funds (Lesson 6 pattern). Today the provider rejected it, closed bank account, wrong details, whatever. The hold has to be RELEASED. Most fintech engineers build the settle path and forget the release path; the result is funds locked in 'pending' forever until a customer support ticket arrives.
When a user starts a withdrawal, you do not move cash yet; you move the user's balance from Available into Pending so they cannot spend it twice while it is in flight. That hold is a promise with two possible endings: the withdrawal settles and real cash leaves, or it fails and the held funds go back. A reservation that can be created must also be undoable, or it is a one-way door.
When the provider rejects the withdrawal, no cash ever left your bank, so the release does not touch the Bank Account at all. It simply reverses the hold: User Pending Withdrawal goes down, User Wallet (Available) goes back up, and the user's spendable balance is whole again. The release entry is the exact mirror image of the hold, which is what makes the round trip provably clean.
The trap is that the happy path is the one everyone builds. Settle is obvious because it is what success looks like, so the release path quietly never gets written. Then the first rejected withdrawal has nowhere to go, the funds sit in Pending indefinitely, and the user just sees money that vanished from their available balance with no explanation.
Worked example, step by step
Set the stage: seed wallet with ₦20,000
User had ₦20,000 of available balance before any of this started.
| Account | Debit | Credit |
|---|---|---|
| Bank Account (1200) | ₦20,000.00 | |
| User Wallet (Available) (2000) | ₦20,000.00 |
Just establishing initial state. Bank UP ₦20,000, User Wallet (Available) UP ₦20,000. Standard funding entry.
User initiates ₦5,000 withdrawal (the hold)
User clicks withdraw. Funds move from Available to Pending. We're committed to either settling or releasing.
| Account | Debit | Credit |
|---|---|---|
| User Wallet (Available) (2000) | ₦5,000.00 | |
| User Pending Withdrawal (2100) | ₦5,000.00 |
Same as Lesson 6 step 2. User Wallet (Available) DOWN ₦5,000, User Pending Withdrawal UP ₦5,000. Bank is untouched, money has not actually left.
Provider rejected: release the hold
Provider says 'no, that bank account is closed.' The withdrawal will not happen. The held funds must go back to the user's available balance immediately.
| Account | Debit | Credit |
|---|---|---|
| User Pending Withdrawal (2100) | ₦5,000.00 | |
| User Wallet (Available) (2000) | ₦5,000.00 |
EXACT inverse of the hold entry. User Pending Withdrawal DOWN ₦5,000 (debit clears it). User Wallet (Available) UP ₦5,000 (credit restores it). Bank is untouched, because the cash never left in the first place. After this entry: Available = ₦20,000, Pending = ₦0. We're back to the starting state.
Takeaway
Every hold needs a release path. The pattern is symmetrical: the release entry is the exact inverse of the hold. Engineers who only build the settle path leave failed withdrawals stuck in 'pending' forever, the user sees a missing balance, and support tickets pile up. Build both paths from day one.
Practice this on a real ledger
Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.