Topmate HLD: Booking Paid 1:1 Sessions Without Double-Booking
How a creator-session platform works inside: turning availability windows into bookable slots, the interval-overlap test that prevents double-booking, and the hold-pay-confirm flow.
A creator on Topmate (or a mentor on Preplaced, ADPList, Calendly) says "I'm free Tuesday mornings," and strangers book paid 1:1 slots without anyone ever ending up double-booked. The product looks like a calendar, but the engineering question is sharp and small: given a mentor's availability windows and the slots already taken, what can still be booked — and how do you make sure two people racing for the same 10 a.m. can't both win? It all reduces to interval arithmetic and one carefully-placed lock.
This is the inside of Topmate / Calendly / any session-booking tool. The signature problem is conflict-free booking: offer open slots, and never sell the same one twice.
Let's start nowhere near a computer
Think of a barber with a sign-up sheet on the wall. The sheet shows the hours they're open — say 9 to 12 — ruled into one-hour lines. A walk-in scans for an empty line and writes their name. The barber's only real rule: two names can't share a line. If someone tries to squeeze "9:30" between the 9:00 and 10:00 slots, it clashes with the 9:00 appointment that already runs to 10:00, so it's refused. And critically — only one pen, so two customers can't write on the same line at the same instant.
The open hours are availability windows, each ruled line is a slot, "two names can't share a line" is overlap detection, and "only one pen" is the lock during checkout. That's the whole system.
Where this exact shape shows up
- Topmate, Calendly, Cal.com, ADPList, Preplaced — all are availability-minus-bookings with conflict checks.
- The same interval-overlap logic powers hotel/room booking and calendar free-busy.
- "Hold the resource during checkout" is the identical instinct to movie-seat locking.
Step 1 — Functional requirements (sentences first)
- A creator publishes availability (recurring or one-off windows).
- The platform shows open slots to a visitor.
- A visitor books a slot, pays, and both parties get a confirmation.
- No double-booking — a slot can be sold at most once.
- Handle timezones so 10 a.m. for the creator is shown correctly to a visitor abroad.
The load-bearing requirement is no-double-booking. It's what forces an overlap check plus a lock, not a naive "mark it taken."
Step 2 — Non-functional requirements
- Correctness (no double-booking). Even under a race for the same slot, at most one booking wins. Non-negotiable.
- Consistency of money and slot. A slot is confirmed only if payment succeeds; a failed payment frees it.
- Low latency. Browsing availability and booking feel instant.
- Timezone correctness. Times are stored absolutely and rendered per viewer.
- Scale. Many creators, each with modest traffic — huge-N creators, small-N per creator.
Listing them is the easy half; the design only earns them if it fulfills them:
| Requirement | How this design fulfills it |
|---|---|
| No double-booking | an overlap test rejects clashes, and a slot lock serializes racers — Steps 3, 4, 6 |
| Money/slot consistency | hold → pay → confirm: the slot is held during checkout, confirmed only on payment — Step 6 |
| Low latency | open slots are a cheap interval computation per creator — Steps 3, 4 |
| Timezone correctness | store times as absolute instants (UTC); convert at the edge — Steps 3, 8 |
| Scale | creators are independent; shard by creator — Step 7 |
Every trade-off below is chosen to keep one of these.
Step 3 — Slots = availability minus bookings
The model is pure interval arithmetic. A creator's availability is a set of windows; bookable slots are fixed-length intervals carved out of those windows, minus anything already booked. Two half-open intervals [a, b) and [c, d) overlap exactly when a < d and c < b — that single comparison is the heart of the whole system.
Getting the half-open convention right matters: a slot ending at 10:00 and one starting at 10:00 touch but don't overlap, so back-to-back bookings are allowed while genuine clashes are caught.
Step 4 — The booking engine
Here's the core. It generates open slots from windows and accepts a booking only if it fits a window and overlaps no existing booking:
package dev.fiveyear.slots;
import java.util.ArrayList;
import java.util.List;
/**
* The booking core behind a creator-session platform like Topmate: a mentor publishes
* availability windows, the platform offers bookable slots inside them, and a booking
* must (1) fit entirely within an availability window and (2) not overlap any existing
* booking. The whole thing reduces to interval arithmetic: two half-open intervals
* [a,b) and [c,d) overlap iff a < d AND c < b. Getting that overlap test right is what
* prevents double-booking — the cardinal sin of any scheduling system. Times here are
* absolute minutes; timezone conversion happens at the edge, before this layer.
*/
public class SlotBook {
/** A half-open time interval [start, end) in absolute minutes. */
public static final class Interval {
public final int start, end;
public Interval(int start, int end) { this.start = start; this.end = end; }
boolean overlaps(Interval o) { return start < o.end && o.start < end; }
boolean contains(Interval o) { return start <= o.start && o.end <= end; }
}
private final List<Interval> availability = new ArrayList<>();
private final List<Interval> bookings = new ArrayList<>();
/** Publish a window the mentor is free, e.g. 9:00–12:00 -> [540, 720). */
public void addAvailability(int start, int end) {
if (end <= start) throw new IllegalArgumentException("empty window");
availability.add(new Interval(start, end));
}
/** Bookable slots of the given length: every aligned slot inside a window that isn't already taken. */
public List<Interval> openSlots(int duration) {
List<Interval> out = new ArrayList<>();
for (Interval w : availability) {
for (int s = w.start; s + duration <= w.end; s += duration) {
Interval slot = new Interval(s, s + duration);
if (!overlapsAnyBooking(slot)) out.add(slot);
}
}
return out;
}
/** Atomically book [start, start+duration): accepted only if it fits a window and clashes with nothing. */
public boolean book(int start, int duration) {
if (duration <= 0) return false;
Interval req = new Interval(start, start + duration);
if (!fitsSomeWindow(req)) return false; // outside published availability
if (overlapsAnyBooking(req)) return false; // would double-book
bookings.add(req);
return true;
}
public int bookingCount() { return bookings.size(); }
private boolean fitsSomeWindow(Interval req) {
for (Interval w : availability) if (w.contains(req)) return true;
return false;
}
private boolean overlapsAnyBooking(Interval req) {
for (Interval b : bookings) if (b.overlaps(req)) return true;
return false;
}
}book enforces both rules in two lines: the request must fit inside a published window (you can't book when the creator isn't free), and it must clash with nothing (no double-booking). Everything else — recurring availability, buffers between sessions — is sugar layered on this interval core.
Step 5 — The race, and why code alone isn't enough
The book method above is correct in a single thread, but a real platform has two visitors hitting the same 10 a.m. slot simultaneously across different servers. Both could read "no overlap" before either writes. The interval test prevents logical clashes; preventing the race needs a serialization point — a lock or a unique constraint at the data layer (Step 6).
Step 6 — The architecture: hold → pay → confirm
Booking is three phases. Hold: the booking service places a short-lived lock on the slot (a row with a unique constraint on creator + slot, or a Redis lock) — the first writer wins, the second is rejected instantly, killing the race. Pay: the held slot goes to checkout. Confirm: only after payment clears is the booking made permanent, both calendars updated, and notifications sent. If payment fails or the hold expires, the slot frees and reappears as open. This hold-pay-confirm is the same pattern as movie-seat booking — never confirm a paid resource before the money lands, never let an abandoned checkout block a slot forever.
Step 7 — Trade-offs (each one keeping an NFR)
| Decision | The tempting alternative | Why ours wins | Keeps |
|---|---|---|---|
| availability − bookings on the fly | precompute a fixed slot table | windows + buffers change freely; slots are a cheap computation | latency, flexibility |
half-open intervals [a,b) | inclusive ranges | adjacency is unambiguous; back-to-back works, clashes still caught | correctness |
| unique constraint / lock on the slot | trust the app-level overlap check | serializes concurrent racers at the data layer — the true guard | no double-booking |
| hold → pay → confirm | book first, charge later | a slot is never confirmed without payment; abandoned holds expire | money/slot consistency |
| store absolute UTC instants | store wall-clock + a tz string | unambiguous math; render per viewer at the edge | timezone correctness |
The complete implementation
The slot engine is the core. Here's the driver that proves it — slot generation, exact and partial double-book rejection, out-of-window rejection, adjacency, and a second window:
package dev.fiveyear.slots;
import java.util.List;
import dev.fiveyear.slots.SlotBook.Interval;
public class Main {
public static void main(String[] args) {
SlotBook book = new SlotBook();
book.addAvailability(540, 720); // 9:00–12:00
// three 60-minute slots fit the 3-hour window
List<Interval> slots = book.openSlots(60);
assertTrue(slots.size() == 3, "three 60-min slots in a 3-hour window");
assertTrue(slots.get(0).start == 540 && slots.get(2).start == 660, "slots are aligned by duration");
// book the 9:00 slot
assertTrue(book.book(540, 60), "first slot books");
assertTrue(book.bookingCount() == 1, "one booking recorded");
// that slot disappears from the open list
assertTrue(book.openSlots(60).size() == 2, "booked slot no longer offered");
// booking the same slot again is refused (double-booking)
assertTrue(!book.book(540, 60), "exact double-book refused");
// a partial overlap is also refused: 9:30–10:30 clashes with the booked 9:00–10:00
assertTrue(!book.book(570, 60), "overlapping booking refused");
// a non-overlapping slot books fine
assertTrue(book.book(660, 60), "11:00 slot books");
assertTrue(book.bookingCount() == 2, "two bookings now");
// a request that spills past the window end is refused
assertTrue(!book.book(720, 60), "slot outside availability refused");
// ...and one starting before the window
assertTrue(!book.book(480, 60), "slot before the window refused");
// adjacency is allowed: [600,660) touches [540,600) and [660,720) but does not overlap
assertTrue(book.book(600, 60), "adjacent slot (10:00–11:00) books — touching is not overlapping");
assertTrue(book.bookingCount() == 3, "all three hours now booked");
assertTrue(book.openSlots(60).isEmpty(), "no slots remain");
// a second window adds fresh capacity
book.addAvailability(840, 900); // 14:00–15:00
assertTrue(book.openSlots(60).size() == 1, "the new window offers one more slot");
System.out.println("ALL SLOT BOOKING ASSERTIONS PASSED");
}
static void assertTrue(boolean cond, String msg) { if (!cond) throw new AssertionError(msg); }
}Step 8 — Scaling and timezones
- Per-creator independence → each creator's availability and bookings are isolated, so shard by creator id; a creator's traffic never touches another's data.
- Read-heavy browsing → open-slot lists are cacheable per creator and invalidated on a new booking.
- The race → keep the lock narrow: a unique constraint on
(creator_id, slot_start)makes the database itself reject the second writer — no distributed lock needed for single-creator slots. - Timezones → store every instant in UTC; convert to the viewer's zone only at render time, so a creator in IST and a visitor in PST always agree on the same moment.
The headline: shard by creator, cache the slot lists, and let a unique constraint be the single source of truth for "who got the slot."
Step 9 — When a piece fails: designing for failure
- Two visitors race for one slot → the unique constraint / lock lets exactly one
INSERTwin; the other gets a clean "just taken, pick another" rather than a double-booking. - Payment fails or the user abandons checkout → the hold has a TTL; it expires and the slot reappears as open. No slot is ever stuck "held forever."
- The notification service is down → the booking is already committed (source of truth); reminders are queued and retried, so a missed email never means a lost booking.
- A creator deletes availability with a booking inside → existing confirmed bookings are immutable; removing a window only stops new bookings, it never silently cancels a paid session.
The interview corner
- "How do you prevent double-booking?" Two layers: an interval-overlap test (
a < d && c < b) for logical clashes, and a unique constraint / lock on the slot to serialize concurrent racers — the data layer is the real guard. - "Why half-open intervals?" So a slot ending at 10:00 and one starting at 10:00 are adjacent, not overlapping — back-to-back bookings work, real clashes are still caught.
- "How do you keep the slot and the payment consistent?" Hold → pay → confirm: lock the slot during checkout, confirm only after payment, and expire abandoned holds.
- "How do you handle timezones?" Store absolute UTC instants; convert to each viewer's local zone only at render. Never store wall-clock alone.
- "How does it scale?" Creators are independent, so shard by creator id; a unique constraint per
(creator, slot)handles the only real contention.
Where to go from here
- The hold-pay-confirm and locking are the same as movie-seat booking; the interval and free-busy logic connect to calendar modelling and hotel booking.
- New to system design? The rookie's guide to HLD walks the method this article follows.