Docs / Booking engine▾

Core flows

Booking engine

The heart of Parkezy: turning a driver's request into a guaranteed slot. Three ideas make it safe: a strict state machine, a database constraint that makes double booking impossible, and idempotent requests that make retries harmless.

The booking lifecycle#

A booking can only move along the arrows below. The rules live in one place (domain/booking_state.py), so the API, the background jobs and the admin tools can never disagree about what is allowed.

Booking state machinerequestauto-acceptpending_approvalpending_paymentconfirmedchecked_incompletedapprovepaidcheck-incheck-outcancel · expire (any time before check-in)rejectrejectedcancelledexpiredpay-at-venue bookings skippending_payment entirely

Swipe sideways to see the whole figure.

Hosts either auto-accept or approve each request. Unpaid holds expire after 10 minutes, unanswered requests after 30, and confirmed bookings nobody checks in to are marked no-show by the maintenance job.
app/domain/booking_state.py (simplified)python
TRANSITIONS: dict[S, frozenset[S]] = {
S.PENDING_APPROVAL: frozenset({S.PENDING_PAYMENT, S.CONFIRMED, S.REJECTED, S.CANCELLED, S.EXPIRED}),
S.PENDING_PAYMENT: frozenset({S.CONFIRMED, S.CANCELLED, S.EXPIRED}),
S.CONFIRMED: frozenset({S.CHECKED_IN, S.CANCELLED, S.EXPIRED}),
S.CHECKED_IN: frozenset({S.COMPLETED}),
S.COMPLETED: frozenset(), # terminal states accept nothing
...
}
def ensure_transition(current, target) -> None:
if not can_transition(current, target):
raise Conflict(f"Booking cannot move from {current} to {target}", code="invalid_booking_transition")

From tap to confirmed#

Booking sequenceDriver appAPIPostgresRazorpayPOST /v1/bookings/quotesubtotal + 18% GSTPOST /v1/bookings + Idempotency-KeyINSERT booking (pending_payment)exclusion constraint checks overlap atomicallyok, or overlap violation201 booking (slot held 10 min)POST /bookings/{id}/paymentcreate ordercheckout (card / UPI)payment/confirm (signed result)confirmed + outbox events

Swipe sideways to see the whole figure.

Happy path for an auto-accepted listing with online payment. The price is always computed by the server; the app only displays it.

Zero double booking, enforced by Postgres#

Checking availability in application code and then inserting is a race: two requests can both see the slot as free. Instead, the database refuses the second row. Each booking stores its time window as a range, and an exclusion constraint forbids two live bookings on the same slot whose ranges overlap.

migrations/versions/0001_initial.pysql
-- The core integrity guarantee: one slot can never hold two overlapping live bookings.
CONSTRAINT ex_bookings_no_overlap EXCLUDE USING gist (slot_id WITH =, period WITH &&)
WHERE (status IN ('pending_approval', 'pending_payment', 'confirmed', 'checked_in'))

Cancelled, rejected and expired bookings fall outside the WHERE clause, so their time frees up immediately. The service turns the violation into a clean 409; when the driver did not choose a specific slot, it quietly retries with the next free one.

Measured: 100 drivers, one slot, the same instant

k6 race test: 100 virtual drivers booked the same single slot for the same window at once. The constraint let exactly one through.

Safe retries with Idempotency-Key#

Phones lose connections mid-request. If the app retries a booking after a timeout, the driver must not end up with two bookings. Every POST /v1/bookings carries an Idempotency-Key header, and a unique constraint on (driver_id, idempotency_key) means a repeat returns the original booking.

ScenarioResult
20 parallel retries with one Idempotency-Key1 booking, every response points at it
20 parallel sign-ups with one email1 account, 19 × 409
Same key reused for a different booking409 idempotency_mismatch: a key can never be recycled by accident

Pricing, server-side#

  • Times align to 15-minute steps; bookings run from 30 minutes to 7 days, up to 30 days ahead.
  • Price is pro-rata by the minute, with an optional daily cap per 24-hour block. All money is Decimal, rounded half-up to the paisa.
  • GST (18%) is added on the subtotal; the platform commission (15% private, 12% commercial) comes out of the host's share, not the driver's price.
  • Free cancellation until 60 minutes before the start; after that 25% of the subtotal is kept.
  • Overstay: 5 minutes of grace, then ₹20 (plus GST) for every started 15-minute block.