**00:00:00 — Alex:** Hey, welcome to Development Radio. I'm Alex, and today's story is one concert seat that sold twice. With a lock on it.

**00:00:08 — Maya:** I'm Maya. The seat is 14C. Two people clicked Buy. Two people got a confirmation email. Same concert, same seat. Two emails, one seat. And both of our servers say they were holding the lock when they booked it.

**00:00:23 — Alex:** Which is the one thing that lock was there to prevent. So that's our question today. If the lock didn't stop the second booking, what should have?

**00:00:31 — Maya:** Let's find out before the doors open. We'll build the smallest version. A ticketing app with two API servers, A and B. Bookings live in Postgres. Before a server books a seat, it takes a lock for that seat in Redis: set the key only if nobody else has it, with a ten-second expiry. Then it checks the seat is free, writes the booking, sends the email, and releases the lock.

**00:00:55 — Alex:** The expiry is there so a crashed server can't hold 14C forever. That's a normal lock. So what did server A do with its ten seconds?

**00:01:04 — Maya:** Lena clicks Buy, and her request lands on A. A takes the lock and checks Postgres: 14C is free. Then A stops. Say a long garbage-collection pause, twelve seconds. At ten seconds, Redis expires the lock. Omar clicks Buy on server B. B takes the lock, sees 14C free, books it for Omar, sends his email and releases.

**00:01:28 — Alex:** And then A wakes up in the middle of its own sentence. It already checked the seat. From where A stands, no time has passed.

**00:01:35 — Maya:** So it writes Lena's booking over Omar's and sends her email. Postgres says Lena. Omar's inbox says Omar. On concert night, they both walk up to 14C. Every step was correct when it ran.

**00:01:48 — Alex:** That's the part that bothers me. Nothing failed loudly. The lock just ended during A's work, and nobody told A. Could A have noticed?

**00:01:57 — Maya:** My first instinct is to have A check, right before writing, that its value is still on the key in Redis. But the pause can land between that check and the write. Same problem, smaller window.

**00:02:09 — Alex:** Any check A makes is a fact about the past by the time its write lands. What about the expiry itself? Thirty seconds instead of ten?

**00:02:17 — Maya:** That moves the line. A thirty-one-second pause does the same thing. And if A crashes for real, 14C stays locked for up to thirty seconds while buyers wait. I don't love paying that for a guarantee we still don't get.

**00:02:31 — Alex:** And the part we were proud of doesn't save us either. We only delete the key if it still holds our value.

**00:02:37 — Maya:** That protects the lock. When A wakes up, it can't delete somebody else's lock by mistake. It does nothing for the seat. A's write to Postgres never asks Redis anything.

**00:02:48 — Alex:** Then let's ask who sees both bookings arrive. Not Redis. Not A.

**00:02:53 — Maya:** Postgres. So Postgres is the one that has to say no. Update seat 14C, set the buyer to Lena, only where the buyer is still empty. Omar's booking committed first, so A's update matches zero rows.

**00:03:06 — Alex:** So A gets an answer it didn't expect: zero rows. What does Lena see?

**00:03:11 — Maya:** Not a confirmation. Zero rows means the seat wasn't empty anymore. Someone else got 14C, so A skips the email and tells her the seat was just taken. She picks another seat. Disappointing, but true. Two confirmations for one seat is worse, because one of them is false.

**00:03:29 — Alex:** Redis's own lock documentation lands in the same place. If you care about correctness, use fencing tokens, and don't assume you still hold a lock just because your process is alive. Martin Kleppmann describes the token: each new lock comes with a bigger number, and storage rejects a write that carries an older one.

**00:03:48 — Maya:** Our empty-buyer condition does that job here, because the seat itself says whether it's taken. If the write had no rule like that, say replacing a whole document, that's where the token earns its keep.

**00:04:01 — Alex:** Here's my uncomfortable question, then. If Postgres is the referee now, what is the Redis lock still doing?

**00:04:08 — Maya:** For this booking? Not much. The conditional write is correct without it. Honestly, I'd be tempted to delete it.

**00:04:15 — Alex:** I'd keep it. Picture the on-sale. Half the room clicks 14C in the same second. With the lock, one request goes on to Postgres, and everyone else hears 'someone's booking that seat' straight from Redis. Without it, they all line up on that one row just to hear no.

**00:04:32 — Maya:** Fine. A bouncer, not a referee. It keeps the crowd off the row. It doesn't decide who gets the seat.

**00:04:39 — Alex:** That's the split I'd bring to a design review. Ask what breaks if two holders overlap. A little wasted work? A lock with an expiry is fine on its own. Two people holding seat 14C? The final check lives where the seat lives.

**00:04:54 — Maya:** How would we test that, though? A twelve-second pause won't show up on demand.

**00:04:58 — Alex:** My first booking page double-booked a neighborhood workshop. The organizer spent a Saturday apologizing. I had never once tried two bookings at the same moment.

**00:05:08 — Maya:** So this time we try it on purpose.

**00:05:10 — Alex:** On purpose. Hold server A right before its write, with a breakpoint or a test hook, until the lock expires. Let B book Omar. Release A. You want zero rows, no email for Lena, and Omar still in 14C.

**00:05:26 — Maya:** Then here's this week's move. Find one lock in your system and ask what breaks if two holders overlap. If it's money or a promise, make the write refuse the second one.

**00:05:37 — Alex:** Let the lock work the door. Let the write decide who gets seat 14C.
