Skip to content

Case study

Hardening a Self-Service Booking Engine

Concurrency guarantees went from zero executed lock tests to a Postgres-backed suite that proves the locks work, including cross-plan races.

Project
Online tutoring platform
Industry
EdTech / SaaS
Role
Audit, backend rules, concurrency tests, booking UI
  • Python
  • FastAPI
  • SQLModel / SQLAlchemy
  • Alembic
  • PostgreSQL
  • Next.js
  • TypeScript
  • SWR
  • dayjs
  • Vitest

The context

A self-service booking engine that lets parents and tutors book, move, and undo lessons against an hours balance without admin involvement, on a multi-country online tutoring platform.

The problem

Booking looked finished: it had 36 passing tests. An audit showed otherwise.

  • The core guarantee, "an instructor can't be double-booked", was never executed by any test. The suite ran on in-memory SQLite, which silently ignores SELECT … FOR UPDATE.
  • A student could be booked with two different tutors at the same time.
  • Failures reported only the first bad slot, so a parent retried slot by slot.
  • Undoing a booking sent one "cancelled" email per lesson and counted as a cancellation.
  • A repeat-weekly series drifted an hour across a daylight-saving boundary.

My responsibility

  • The audit
  • Backend booking rules
  • The concurrency test harness
  • The parent booking UI

Architecture

  1. Parent / tutor UI → POST /self-scheduleslots[], atomic, capped at 20
  2. Lock instructor rowFOR UPDATE, shared with the confirm and reschedule paths
  3. Lock plan rowFOR UPDATE, for stale-plan revalidation
  4. Check conflictsThe instructor's calendar and the student's own, across any tutor
  5. Check availability, notice, bufferAll failures returned together as failed_slots[]
  6. Insert sessions and debit hoursOne transaction
  7. Undo within 10 minutesAtomic, one "withdrawn" digest, not a cancellation
  8. Late cancel → admin review queueWaive releases the hour, no-show charges it
The booking path. Every step from the locks to the debit runs in one transaction.

Key decisions

  • Two locks on purpose

    The instructor lock serialises against the admin confirm and reschedule paths. The plan lock exists for stale-plan revalidation and also serialises same-plan races. They were added for different defects, and the redundancy is defense in depth.

  • The student's own calendar joins the conflict check

    It returns a distinct student_already_booked reason.

  • Undo is not a cancellation

    It has its own endpoint, a time-bounded grace window, and a single digest email.

  • Late cancels never block

    They open a review case for an admin to rule on, because the business rule was "cancel any time, adjudicate afterwards".

  • Series dates rebuilt from local date strings

    dayjs.add(7, 'day') is instant-based and drifts across DST, so each week is rebuilt from its own local date and time.

Technical challenges

  1. 1.Proving a lock works

    A Postgres-backed suite with two real connections and transactions. Same-plan races pass even with the instructor lock removed, because the plan lock catches them; only cross-plan, same-instructor tests exercise it. A meta-test neutralises the lock and asserts the race appears.

  2. 2.Reporting every bad slot at once

    Done backwards-compatibly: the first failure stays in the old top-level fields and the full list goes in failed_slots[].

  3. 3.A latent shadowing bug

    A timezone: str parameter shadowed the datetime module and crashed when re-posting an existing exception date.

  4. 4.Deploy ordering

    The frontend's new calls 404 on an old API, so the backend ships first.

Results

  • Concurrency guarantees went from 0 executed lock tests to [X] Postgres-backed ones, including cross-plan cases
  • Double-bookings in production: [X → 0]
  • Failed-booking retries per parent session: [X → Y]
  • Support tickets about vanished bookings or duplicate cancellation emails: [X% drop]
  • Parent time-to-book: [X taps / seconds]

What I learned

  • "Tests pass" says nothing about a lock. Check that the test engine can express the behaviour before trusting the test.
  • Removing one of two redundant locks looks safe until a cross-plan test says otherwise. Mutation-test your safety claims.
  • Model undo as its own domain operation, not as a cancellation with an exception.

Need a backend engineer who owns it end to end?

From architecture to production support, I take features from requirements to running software.