Free preview

Why this matters: the implementation act here is graded on whether the invariants survive contact with code — one writer for seat state, all-or-nothing holds, idempotent confirmation. The walkthrough builds the vocabulary types first, then the show's seat map (where the race is decided), then the service that faces payment.

Vocabulary types

java
enum SeatStatus { AVAILABLE, HELD, BOOKED } record SeatId(String row, int number) { } // "F", 7 class Hold { final String id; final Set<SeatId> seats; // all-or-nothing unit final String userId; final Instant expiresAt; // TTL: now + 5 minutes boolean confirmed = false; boolean isExpired(Instant now) { return !confirmed && now.isAfter(expiresAt); } } record Booking(String id, String showId, Set<SeatId> seats, String userId) { }

Narrate the Hold fields: identity, contents, owner, deadline — the four things lesson 02 said a claim needs. Note isExpired never expires a confirmed hold: confirmation freezes the claim, which is half of retry-safety already.

The show: one owner, one writer

java
class Show { final String id; final ScreenLayout layout; // immutable, shared private final Map<SeatId, SeatStatus> seats; // THIS show's truth private final Map<SeatId, String> heldBy; // seat -> holdId private final Map<String, Hold> holds = new HashMap<>(); // The race is decided here: one indivisible check-then-claim. synchronized Optional<Hold> placeHold(Set<SeatId> wanted, String userId, Instant now) { expireLapsed(now); // lazy expiry first for (SeatId s : wanted) if (seats.get(s) != SeatStatus.AVAILABLE) return Optional.empty(); // ANY taken -> claim nothing Hold hold = new Hold(newId(), wanted, userId, now.plus(TTL)); wanted.forEach(s -> { seats.put(s, SeatStatus.HELD); heldBy.put(s, hold.id); }); holds.put(hold.id, hold); return Optional.of(hold); } private void expireLapsed(Instant now) { for (Hold h : List.copyOf(holds.values())) if (h.isExpired(now)) releaseSeatsOf(h); } }

Three sentences to say while writing this. First: synchronized on the one method that mutates makes the check-then-claim indivisible — two users tapping F7 serialize here, and the second sees HELD. Second: the loop checks every wanted seat before touching any — the all-or-nothing rule as control flow, not commentary. Third: expireLapsed runs at the top of every mutating call — lazy expiry means an expired hold is indistinguishable from a released one by the time any decision is made.

Confirm and release: payment's three outcomes

java
class Show { // continued synchronized Optional<Booking> confirm(String holdId, Instant now) { Hold h = holds.get(holdId); if (h == null) return Optional.empty(); if (h.confirmed) // duplicate callback: return Optional.of(bookings.get(holdId)); // same result, no effect if (h.isExpired(now)) { releaseSeatsOf(h); return Optional.empty(); } h.confirmed = true; h.seats.forEach(s -> seats.put(s, SeatStatus.BOOKED)); Booking b = new Booking(newId(), id, h.seats, h.userId); bookings.put(holdId, b); // keyed BY HOLD -> idempotent return Optional.of(b); } synchronized void release(String holdId) { // payment failed / user cancelled Hold h = holds.get(holdId); if (h != null && !h.confirmed) releaseSeatsOf(h); } }

confirm is the method to walk through slowest, because its branch order is the requirements: duplicate success callbacks return the existing booking (idempotency, keyed by hold id); a hold that expired before confirmation fails cleanly — the honest outcome when payment succeeded too late; only a live, unconfirmed hold books seats. And the timeout case from lesson 01 appears nowhere — which is the point. Unknown-outcome payments require no code path: the caller simply never calls, and the TTL retires the hold.

The service: the flow reads like the lesson

java
class BookingService { private final Map<String, Show> shows; private final PaymentGateway payments; // external; can fail/timeout private final Clock clock; // injectable time Optional<Booking> book(String showId, Set<SeatId> wanted, String userId) { Show show = shows.get(showId); Optional<Hold> hold = show.placeHold(wanted, userId, clock.instant()); if (hold.isEmpty()) return Optional.empty(); // lost the race PaymentResult r = payments.charge(userId, price(show, wanted)); if (r.succeeded()) return show.confirm(hold.get().id, clock.instant()); show.release(hold.get().id); // known failure return Optional.empty(); // timeout? TTL handles it } }

Two injections to point at: PaymentGateway as an interface (tests script success, failure, and the duplicate callback), and Clock rather than direct time calls — because expiry is untestable without controlling the clock. A test that advances a fake clock past the TTL and watches F7 come back is the proof that holds actually die.

One honest note to volunteer: in a real service the provider's success can also arrive as an asynchronous callback, hitting confirm(holdId) directly — which is exactly why confirm is keyed by hold id and idempotent, so the synchronous path and a retried callback land on the same safe method.

Key takeaway

The code keeps the design's promises structurally: every seat mutation lives inside the show behind one lock, placeHold checks all seats before claiming any, lazy expiry at the top of each mutating call makes expired and released holds indistinguishable, and confirm's branch order — duplicate returns the same booking, expired fails cleanly, live hold books — is the requirements list executable. The strongest lines in the file are the ones for cases that need no code: timeouts handled entirely by the TTL.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue