You pick a seat, enter your credit card details, and for those 10 minutes the seat is yours. Nobody else can take it. If you don’t complete payment in time, the seat releases back to inventory. This is the hold-and-confirm pattern: temporarily reserve a resource, give the user time to complete a multi-step transaction, then either confirm the reservation or release it.

Why You Need It#

Without holds, the user experience is broken. A buyer selects seats, spends 5 minutes entering payment details, submits, and gets “sorry, those seats are no longer available.” They’ve wasted time on a transaction that was never going to succeed. Holds give users a realistic window to complete a purchase.

The hold creates a three-state resource lifecycle: available → held → sold (or back to available on expiry). The critical properties: holds must expire automatically, confirmation must be atomic with payment, and hold expiry must release inventory immediately.

TTL and Expiry#

Holds are stored with a TTL. In Redis: SET seat:A12:hold buyer_id EX 600 (10 minutes). In a relational database: a held_until timestamp column. A background job or database event fires when held_until < NOW() and flips the seat back to available.

The TTL must be enforced at the inventory layer, not just the UI. A user who closes their browser still has a hold. The backend must release it after expiry regardless of what the frontend does. Distributed locks with TTL handle this naturally: the lock expires even if the holder crashes.

graph TD A[User selects seat A12] --> B[Create hold: status=held, held_until=now+10min] B --> C[User enters payment details] C --> D{Payment submitted before expiry?} D --> |Yes| E[Charge card] E --> F{Payment successful?} F --> |Yes| G[Confirm: status=sold, clear held_until] F --> |No| H[Release: status=available, clear hold] D --> |No, TTL expired| I[Auto-release: status=available] I --> J[Seat returns to inventory pool] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style H fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style I fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style J fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Confirm Atomicity#

The confirm step — charging the card and marking the seat as sold — must be atomic. If the payment succeeds but the seat status update fails, you’ve charged the user but not given them the seat. If the seat is marked sold before the payment is confirmed, you may give away inventory you won’t be paid for.

Use the transactional outbox pattern: write the seat status change and the payment confirmation event in the same database transaction. A downstream service processes the payment event. If payment fails, roll back the status change. If the service crashes mid-transaction, the WAL ensures no partial state survives.

Hold Abuse#

Holds are abusable. A bot can hold all seats in a venue, preventing legitimate buyers from purchasing, then release them or sell them on secondary markets. Countermeasures: limit holds per user session, limit holds per IP, require a logged-in account to hold, and use shorter TTLs for high-demand events (5 minutes instead of 10).

At Oracle#

We used this pattern for exclusive configuration locks: a user editing a configuration held a lock for 30 minutes. If they didn’t save, the lock expired. If another user tried to edit, they saw “locked by [user] until [time].” We stored locks in Redis with TTL. The tricky part: on save, we needed to confirm atomically — update the config and release the lock in one operation. We used a Lua script in Redis for the atomic check-and-release, combined with a MySQL transaction for the config write.

What I’m Learning#

The hold-and-confirm pattern is the right abstraction whenever a resource needs to be reserved for a multi-step transaction. The implementation complexity is mostly in expiry: getting TTL enforcement right, handling the confirm-payment race, and defending against hold abuse. Get those three right and the pattern is robust.

Have you used hold-and-confirm in production, and what edge cases caused the most trouble?