Flows

01. Booking slots

Slot Booking & Paytm Payment LLD

A real booking-slot design: timed holds, row locks, Paytm payment attempts, webhooks, retries, refunds, and double-booking protection.

Booking slots/Payments and consistency

Entry Point

App user posts bookingSlotId, groundId, sportId, sideCount, and turfMode to start a booking checkout.

Slot Claim

GroundBooking is created as Pending/Pending with holdExpiresAt, Paytm order data, and a Redis hold mirror.

Concurrency Guard

The BookingSlot row is locked with FOR UPDATE before active claims are checked, renewed, or finalized.

Payment Truth

Paytm status API and webhooks reconcile payment before the slot becomes Booked.

Flow Canvas

Execution map

Payments and consistency
LLD

Slot Booking Execution Map

Validate request, claim a slot, create Paytm attempt, reconcile payment, and recover refunds

entry

App create booking

User asks to hold one slot.

Connected
service

Auth and mobile guard

Only verified users can book.

Connected
service

Load slot context

Load slot and venue facts.

Connected
decision

Domain guards

Reject invalid venue context.

Connected
decision

Slot regen lock check

Pause while slots rebuild.

Connected
decision

Booking window check

Reject too-near slots.

Connected
service

Resolve price snapshot

Freeze payable amount.

Connected
lock

Lock BookingSlot row

Serialize this slot claim.

Connected
state

Load claim state

Read active and retry holds.

Connected
decision

Duplicate active guard

Reject corrupt active claims.

Connected
recovery

Other user active hold

Return 409 for other user.

Connected
decision

Same-user active claim

Check same-user reuse rules.

Connected
external

Paytm reuse status check

Ask Paytm before reuse.

Connected
state

Reuse active txnToken

Return existing Paytm token.

Connected
external

Expired pending sync

Sync old pending order first.

Connected
lock

Renew claim ownership

Extend safe retry hold.

Connected
state

Create fresh DB hold

Create Pending booking claim.

Connected
state

Mirror Redis hold

Mirror hold with TTL.

Connected
external

Initiate Paytm order

Create Paytm order token.

Connected
state

Persist latest and history

Save attempt history.

Connected
recovery

Init failure cleanup

Release failed init safely.

Connected
state

Checkout response

Return Paytm checkout data.

Connected
external

Verify, webhook, or manual sync

Verify by app, webhook, or ops.

Connected
decision

Normalize provider status

Map Paytm result state.

Connected
lock

Validate live claim

Recheck owner under lock.

Connected
state

Mark payment Paid

Store captured payment data.

Connected
state

Book slot conditionally

Book only if still Available.

Connected
state

Finalize booking

Confirm and release hold.

Connected
recovery

Lost-claim refund state

Money came after claim loss.

Connected
recovery

Provider failure

Cancel failed payment.

Connected
state

Non-final provider status

Keep unknown status pending.

Connected
external

Refund initiate or retry

Start or retry refund.

Connected
external

Refund status/webhook

Reconcile refund result.

Connected
33 Nodes
36 Connections

Static map based on the slot-booking and payment modules

LLD Note

Problem

The user is not buying a generic product. The user is trying to own one exact time window on one exact ground. Two users can see the same Available slot, two payment sessions can be opened, and Paytm can report the result later through either client verification or webhook.

The design therefore separates a temporary claim from a confirmed booking. A claim blocks other users for a short payment window, but the slot is not marked Booked until payment success is reconciled and the claim is still valid.

LLD Note

Core Domain Model

BookingSlot is the slot source of truth. It belongs to one ground, has startAt/endAt, and has status Available, Booked, or Blocked. The unique pair groundId plus startAt prevents duplicate generated slots for the same ground time.

GroundBooking is the payment-aware claim record. It stores userId, groundDetailsId, bookingSlotId, sportId, totalPrice, paymentStatus, bookingStatus, Paytm order fields, attempt number, and holdExpiresAt.

  • BookingSlot answers: can this time window be finally occupied?
  • GroundBooking answers: who is trying to pay for it, with which provider order, and in what state?
  • GroundBookingPaymentAttempt keeps each Paytm attempt auditable instead of overwriting history.

LLD Note

Create Booking Flow

The create flow first rejects impossible requests: unverified mobile, slot not found, slot/ground mismatch, unsupported sport, inactive or maintenance ground, inactive or maintenance venue, tournament-only venue, past or too-near slots, and already Booked or Blocked slots.

Pricing is resolved before the hold is created. That matters because a same-user active hold can only be reused when sport, pricing rule, side count, turf mode, and base amount still match.

  • Minimum lead time is payment token expiry plus the last allowed booking buffer.
  • Fresh checkout creates a Pending/Pending GroundBooking and a Paytm order.
  • Same-user active checkout returns the existing txnToken when it is still safe.
  • Expired or failed same-user attempts create a new Paytm order under the same booking id.

LLD Note

Hold And Lock Strategy

The critical section is the BookingSlot row. The code runs SELECT FOR UPDATE with a lock timeout, then checks active claims for the slot inside the transaction. That makes two concurrent create requests serialize instead of both creating valid holds.

The database hold is the durable rule. Redis is a mirror with the same expiry so other reads can cheaply understand that a slot is temporarily held. Releasing the Redis hold uses a compare-and-delete script so one booking cannot delete another booking's hold.

  • Active hold payment statuses are Pending and Failed while holdExpiresAt is still in the future.
  • More than one active claim is treated as a data integrity emergency and rejected.
  • Lock contention returns 409 instead of letting the request wait too long.

LLD Note

Paytm Reconciliation Flow

Paytm initiation returns a txnToken and retry hints. The booking stores the latest Paytm order fields, while GroundBookingPaymentAttempt stores the attempt snapshot and provider payload.

Payment success is never trusted blindly. Client verify and Paytm payment webhook both enter the same shape of reconciliation: find the booking by order id, lock the slot, validate that the current booking owns the order and current attempt, confirm hold expiry has not passed, and then update the slot from Available to Booked.

  • Success plus valid claim becomes Paid/Booked and BookingSlot Booked.
  • Provider failure becomes Failed/Cancelled.
  • Unknown or non-final provider status remains Pending and tells the user to verify later.
  • Success after the claim was lost becomes RefundPending/Cancelled instead of double booking.

LLD Note

Refund And Recovery

Refund is the escape hatch for real-world payment races. If Paytm captures money after the hold expired, after a newer attempt superseded the order, or after the slot is no longer Available, the booking moves to a cancelled refund state instead of stealing the slot.

Refund status can be driven by explicit refund status checks or Paytm refund webhooks. The system records Paytm refund reference, refund id, provider status, message, result code, and the latest provider payload for audit and support.

A normal refund for an already Booked slot is stricter: the booking must be Paid/Booked, the slot must not have started, Paytm transaction id must be known or resolvable, and the slot is blocked after refund initiation so the cancelled time is not resold accidentally.

Failure Modes

Edge cases handled

Mobile is not verified

Trigger

Authenticated user starts checkout before mobile verification.

System response

The create endpoint rejects the request with 403 before loading pricing or touching any slot claim.

Slot does not belong to the requested ground

Trigger

bookingSlotId is valid, but groundId in the request points to a different ground.

System response

The request is rejected with 400 so users cannot pay against a mismatched ground/slot pair.

Ground does not support requested sport

Trigger

sportId is not the primary sport and is not present in active ground sport configs.

System response

The request is rejected before pricing so the booking cannot store an invalid sport context.

Venue or ground is not bookable

Trigger

Ground is inactive, ground is under maintenance, venue is inactive, venue is under maintenance, or venue is tournament-only.

System response

Checkout stops with a 400 class error and no GroundBooking hold is created.

Slots are being regenerated

Trigger

Redis key lock:ground:slotregen:<groundId> exists while checkout starts.

System response

The system returns 409 because slot rows may be changing; if Redis check itself fails, the error is logged and checkout continues.

Slot starts too soon

Trigger

Slot is past, ongoing, within the 10 minute final booking buffer, or less than token expiry plus buffer away.

System response

The request is rejected because the user may not have enough time to complete Paytm payment safely.

Slot already final or blocked

Trigger

BookingSlot.status is Booked or Blocked before claim creation.

System response

The system returns 409 and never creates a Pending booking for that slot.

Pricing cannot be resolved

Trigger

Dynamic/static pricing rules cannot produce a valid payable amount for the sport, side count, turf mode, and time band.

System response

The endpoint returns 422 for pricing resolution errors before any hold is created.

Two users tap the same slot

Trigger

Both hit create booking while BookingSlot is Available.

System response

First request locks the slot row and creates the active hold. The second request sees the active hold or lock contention and receives 409.

Database row lock times out

Trigger

Another transaction is holding the BookingSlot row longer than the 3000ms lock timeout or a deadlock is detected.

System response

The error is normalized to 409: this slot is already being booked by another player.

Duplicate active holds already exist

Trigger

The locked slot query finds more than one Pending/Failed hold with holdExpiresAt in the future.

System response

The system logs a data-integrity event and rejects the slot as temporarily unavailable.

Other user active hold

Trigger

The only active hold belongs to another user.

System response

The current user gets 409 while the existing hold remains untouched until payment, failure, or expiry.

Same user taps pay again

Trigger

A Pending booking exists with active Paytm txnToken.

System response

If pricing context matches and provider status is not successful, the existing session is reused instead of creating duplicate orders.

Same user changes pricing criteria

Trigger

User retries the same slot with different sport, pricing rule, side count, turf mode, or base amount while the old hold is active.

System response

The system returns 409 and asks the user to wait for hold expiry instead of silently changing a live payment session.

Paytm says active order already succeeded

Trigger

The reuse check for an active pending order gets a provider success status.

System response

Create booking returns 409 telling the user to verify payment, preventing a duplicate retry order.

Active pending provider status fetch fails

Trigger

Paytm status lookup fails before deciding whether an expired active txnToken can be retried.

System response

If the token is expired, retry is blocked with 409 because the system cannot prove whether money was captured.

Payment token expired

Trigger

Same user returns after txnToken expiry.

System response

The system renews hold ownership under the row lock and creates the next Paytm attempt with a new order id.

Expired pending order may have succeeded

Trigger

Hold expired but Paytm order is still Pending locally.

System response

Before retry, the system fetches Paytm status. If success, it reconciles; if failure, it creates a retry; if unknown, it blocks retry.

Expired failed order retry

Trigger

Same user has no active hold, but has an expired Failed booking for the same slot and sport.

System response

The system renews the booking hold, mirrors Redis again, and creates the next Paytm attempt under the same booking id.

Active failed order retry

Trigger

The user's active hold is Failed, still within holdExpiresAt, and has Paytm order/token data.

System response

Provider status is checked; if it did not succeed, the hold is renewed and a new Paytm attempt is created.

Paytm initiation fails on fresh hold

Trigger

The DB hold and Redis mirror exist, but createPaytmTransaction throws before an attempt is persisted.

System response

The uninitialized GroundBooking is deleted and Redis hold is released only if the payload still belongs to that booking/user.

Paytm initiation fails on retry

Trigger

A retry hold was renewed, but the provider order creation fails.

System response

The system releases only the owned Redis mirror and keeps the historical booking record for audit.

Redis hold mirror fails

Trigger

redis.set for hold:slot:<slotId> fails after the durable DB hold was created or renewed.

System response

The failure is logged, but the database hold remains the source of truth for blocking checkout.

Verify order belongs to another user

Trigger

Client verify is called with an orderId linked to a different user.

System response

The endpoint returns 403 and does not expose booking or payment state.

Verify order id does not match booking id

Trigger

Manual/status endpoints receive a bookingId and orderId pair that do not match the booking record.

System response

The request is rejected with 400 so ops tools cannot mutate the wrong payment attempt.

Paytm returns non-final status

Trigger

Provider status is neither in success statuses nor failure statuses.

System response

Payment remains Pending; verify returns pending and create retry paths block until a final status is known.

Payment success after lost claim

Trigger

Paytm reports success but the order is superseded, hold expired, or slot is unavailable.

System response

Booking is cancelled with RefundPending so the slot is not double booked.

Old Paytm attempt succeeds after retry

Trigger

Webhook or verify succeeds for an order that is no longer the booking's current paytmOrderId or attempt number.

System response

The old attempt is marked RefundPending/Cancelled, but the latest booking claim is not overwritten.

Slot update loses the race

Trigger

Claim validation passed, but BookingSlot update where status Available affects zero rows.

System response

The booking is moved from Paid candidate state to RefundPending/Cancelled and the slot is not stolen.

Payment failure webhook

Trigger

Paytm webhook returns TXN_FAILURE, FAILURE, F, or NO_RECORD_FOUND for a Pending booking.

System response

The latest booking and payment attempt are updated to Failed/Cancelled with provider transaction metadata.

Webhook repeats or arrives after verify

Trigger

Paytm sends duplicate or late payment webhook.

System response

Idempotent update conditions and attempt history prevent a resolved booking from being incorrectly rewritten.

Payment webhook missing order id

Trigger

Paytm payment webhook does not include ORDERID.

System response

The webhook is logged and rejected with 400, because no booking can be safely identified.

Payment webhook checksum fails

Trigger

Key-value Paytm webhook signature verification fails.

System response

The webhook is rejected with 400 and no booking/payment state is changed.

Payment webhook has non-final status

Trigger

Webhook status is unknown or pending.

System response

The event is logged as ignored and the booking remains in its current state.

Refund requested after slot start

Trigger

Ops/user tries to refund a paid booking after the slot start time.

System response

Refund is rejected because only pre-start active bookings can be refunded through the normal paid-booking refund path.

Refund requested for non-paid booking

Trigger

Refund endpoint is called for a booking that is not Paid/Booked.

System response

Normal refund is rejected; only RefundInitiated, RefundPending, or RefundFailed cancelled bookings go through refund-pending processing.

Refund transaction id missing

Trigger

Booking has no paytmTxnId and Paytm status lookup cannot resolve one.

System response

Refund initiation is marked RefundFailed or rejected because Paytm requires a transaction id.

Refund provider returns failed status

Trigger

Paytm refund initiate/status returns a known failed status.

System response

The booking remains Cancelled and paymentStatus moves to RefundFailed for manual retry/support.

Refund webhook lacks order id

Trigger

Paytm refund webhook contains only refund reference.

System response

The system tries to recover the order from paytmRefundRefId or paytmRefundId before applying refund status.

Refund webhook checksum fails

Trigger

JSON or key-value refund webhook signature verification fails.

System response

The webhook is rejected with 400 and no refund state is changed.

Refund webhook only says accepted

Trigger

Webhook status maps to accepted/pending but not completed.

System response

The booking stays Cancelled and paymentStatus becomes RefundPending until status sync or a later webhook resolves it.

Refund completed later

Trigger

Refund status sync or webhook returns TXN_SUCCESS/SUCCESS/S.

System response

The booking remains Cancelled and paymentStatus becomes Refunded with refund id, provider status, and payload history stored.

State Machine

Payment, booking, and slot states

EventPaymentBookingSlotNote
Fresh hold createdPendingPendingAvailableSlot stays Available but active hold blocks other checkout attempts.
Paytm order initiatedPendingPendingAvailablepaytmOrderId, txnToken, token expiry, and attempt history are stored.
Active same-user session reusedPendingPendingAvailableExisting order and txnToken are returned only when pricing context still matches and provider did not already succeed.
Retry attempt createdPendingPendingAvailableholdExpiresAt is renewed, Redis is mirrored again, paymentAttempt increments, and a new Paytm order is inserted into history.
Provider status unknownPendingPendingAvailablePayment is not failed or retried until Paytm returns a final success/failure status.
Paytm success and valid claimPaidBookedBookedSlot update uses Available condition before final booking status is written.
Paytm failureFailedCancelledAvailableUser can retry when the system can safely renew or recreate the attempt.
Success but claim lostRefundPendingCancelledAvailable or BookedMoney is handled by refund path; slot ownership is not changed for that order.
Normal booking refund initiatedRefundInitiatedCancelledBlockedA previously Paid/Booked slot is blocked after accepted refund initiation so the cancelled time is not immediately resold.
Refund provider failedRefundFailedCancelledAvailable, Booked, or BlockedRefund can be retried or refreshed through the pending-refund flow using the stored refund reference.
Refund completedRefundedCancelledAvailable or BookedRefund webhook or status sync closes the financial state.