The Timestamp Trick for an Abandoned Tab

A soft lock needs to expire even when nobody tells the backend it should, and why a lazily-checked timestamp handles that with no scheduler, no background job, and no cron.

August 30, 20264 min read10 / 44

Soft locking replaces the expensive database lock with a locked status on the seat, held until the booking finishes. Three things can end that lock: the user presses back, the payment succeeds, or the payment fails. All three are events your own code already sees, so releasing the lock in any of them is a direct API call.

There's a fourth way a lock can end, and it's the hard one.

The Real Challenge: Nobody Tells You the Timer Ran Out

A user who just closes the tab triggers none of those three events. Nothing fires. No button click, no API call, no signal of any kind reaches the backend. Something still has to eventually let that seat go, with no event to react to.

The tempting answer is a background job: a scheduler that periodically scans for locks older than fifteen minutes and releases them. That's more machinery than this problem needs.

The KISS Answer: A Timestamp, Checked Lazily

Add one more column to the seat, alongside its status: lockedAt, the time the lock was applied.

Then change how "available" gets decided. A seat counts as available if its status literally says available, or if its status says locked but lockedAt is more than fifteen minutes in the past.

Nothing ever has to actively change that status back. The next time anyone checks, the timestamp does the deciding.

The status column can still say "locked" long after the fifteen-minute window has passed. Nothing updates it. Every check just compares the timestamp at read time. ExpandThe status column can still say "locked" long after the fifteen-minute window has passed. Nothing updates it. Every check just compares the timestamp at read time.

No scheduler. No background job. No cron. The expiry is a condition evaluated the moment someone actually asks, not an event that has to be watched for. This is a general pattern worth keeping, not a BookMyShow-specific trick: anywhere something needs to "expire," check the condition lazily at read time before reaching for a background process to enforce it.

A natural worry: won't checking a timestamp on every read get slow once the seats table has millions of rows? It won't, because you're never scanning the whole table. A single booking touches at most ten seats, since selection is capped, and with an index on the seat's ID, looking up ten specific rows takes a few milliseconds regardless of how big the table is.

The frontend runs the identical check. If it gets back a seat marked locked, but the lockedAt timestamp is older than fifteen minutes, it just displays the seat as available. No special case, no separate logic, the same rule applied on both sides.

Adapting This for a Document Database

Everything above assumes a relational database, with an explicit lock you take and release by hand. A MongoDB-backed build doesn't hand you a lock like that.

The soft-locking idea carries over exactly as described: three statuses, and a lockedAt timestamp checked lazily. What changes is how the lock actually gets set.

Instead of lock, read, write, unlock, MongoDB does the check-and-update as one atomic operation: flip a seat to locked, but only if it's still available. If someone got there first, the update matches nothing and does nothing, the same rejection, with no explicit lock at all.

ℹ️ This holds even with several application servers behind a load balancer, which is the real deployment shape here. Two overlapping requests can land on two different servers at the same instant, and neither server does any locking of its own. Both just send their conditional update to the same MongoDB deployment, and MongoDB serializes the two writes at the document level.

The atomicity lives in the database, not in any one server's process. An in-memory lock fails because it only protects one process; a database-level atomic write succeeds because every process is really just asking the same database to do the work.

Redis solves a different problem: coordinating something that isn't already one atomic write to one shared datastore. Reaching for it here would mean rebuilding, in a separate service, the guarantee MongoDB already gives for free.

One real gap worth naming: findOneAndUpdate only protects one seat at a time, and a booking can span up to ten. All-or-nothing across several seats needs a MongoDB multi-document transaction, not ten separate atomic updates in a row. Still enforced by MongoDB itself, no external lock service involved, just a wider guarantee than one document's atomicity.