The Problem
Playlyne needed a sports venue booking platform (think Booking.com for sports) where customers could discover courts, book time slots, and pay online, while venue owners managed availability, pricing, and financials. The core technical challenge: preventing time slot overlaps across concurrent bookings while handling multi-timezone complexity for international venues and customers.
What Made It Hard
Time Slot Overlap Prevention
Sports venues operate on strict time slots (e.g., 6 PM - 7 PM):
- Slot already booked → must be unavailable instantly
- Partial overlaps rejected (can't book 6:30-7:30 if 6-7 is taken)
- Concurrent booking attempts (two users clicking "Book" simultaneously)
- Pending bookings hold slots temporarily (5 minutes to complete payment)
Challenge: Prevent race conditions without pessimistic database locks (bad UX).
Multi-Timezone Complexity
Venues in different timezones, customers traveling:
- Venue in Dubai (UTC+4), customer in NYC (UTC-5)
- Display slots in customer's local time but store in venue's timezone
- Booking confirmations must show both timezones
- Email notifications must respect recipient timezone
Example Edge Case:
- Customer books "6 PM NYC" for Dubai venue
- Converts to 3 AM next day Dubai time
- Confirmation email to customer shows 6 PM NYC
- Confirmation email to vendor shows 3 AM Dubai time
Payment Workflow State Machine
Bookings transition through complex states:
- Pending → Slot held, payment not started
- Payment Processing → Slot locked, awaiting gateway response
- Payment Failed → Slot released, user can retry
- Confirmed → Slot booked, vendor notified
- Cancelled → Refund initiated, slot released
Each portal needs real-time visibility into booking status.
My Solution
4-Portal Architecture
Built complete ecosystem with role-based separation:
- Customer App - Browse venues, book slots, pay, review
- Vendor Hub - Manage courts, pricing, availability, bookings, payouts
- Superadmin Portal - Approve vendors, manage payouts, platform analytics
- Backend API - Business logic, payment orchestration, email notifications
Time Slot Validation with Optimistic Locking
Implemented database-level uniqueness constraint + optimistic UI:
// Database schema
CREATE UNIQUE INDEX unique_slot
ON bookings(court_id, booking_date, start_time, end_time)
WHERE status IN ('confirmed', 'pending', 'processing');
// Application logic
async function createBooking(courtId, date, startTime, endTime) {
// Step 1: Check availability (fast, non-blocking)
const overlaps = await checkOverlaps(courtId, date, startTime, endTime);
if (overlaps) throw new Error('Slot unavailable');
// Step 2: Create pending booking (DB enforces uniqueness)
try {
const booking = await Booking.create({
courtId, date, startTime, endTime,
status: 'pending',
expiresAt: Date.now() + 5 * 60 * 1000 // 5 min hold
});
return booking;
} catch (error) {
if (error.code === '23505') { // Unique constraint violation
throw new Error('Slot just booked by another user');
}
throw error;
}
}
Why This Works:
- Database guarantees no overlaps (atomic operation)
- Optimistic check gives fast feedback
- Race condition caught by unique constraint
- Failed bookings show clear error message
Multi-Timezone Handling
Built timezone-aware date utilities:
interface BookingTimezones {
venueTimezone: string; // 'Asia/Dubai'
customerTimezone: string; // 'America/New_York'
displayToCustomer: string; // '6:00 PM EST'
displayToVendor: string; // '3:00 AM GST'
storedInDB: Date; // UTC
}
function createTimezoneAwareBooking(
venueTimezone: string,
customerTimezone: string,
customerLocalTime: string
) {
// Convert customer's local time to venue timezone
const venueTime = moment.tz(customerLocalTime, customerTimezone)
.tz(venueTimezone);
// Store as UTC in database
const utcTime = venueTime.utc();
return {
storedTime: utcTime,
displayToCustomer: moment.tz(utcTime, customerTimezone).format('h:mm A z'),
displayToVendor: moment.tz(utcTime, venueTimezone).format('h:mm A z'),
};
}
Email Notifications:
- Customer receives: "Your booking at 6:00 PM EST"
- Vendor receives: "New booking at 3:00 AM GST"
- Backend stores: UTC timestamp for consistency
Payment State Machine
Implemented explicit state transitions with status tracking:
enum BookingStatus {
PENDING = 'pending', // Slot held, no payment
PROCESSING = 'processing', // Payment in progress
CONFIRMED = 'confirmed', // Payment successful
FAILED = 'failed', // Payment failed
CANCELLED = 'cancelled', // User cancelled
EXPIRED = 'expired', // Pending timeout
}
const transitions = {
pending: ['processing', 'expired', 'cancelled'],
processing: ['confirmed', 'failed'],
failed: ['pending', 'cancelled'], // Can retry
confirmed: ['cancelled'], // Can request refund
// ... validation logic
};
Background Job:
- Cron runs every minute
- Releases slots from
pendingbookings older than 5 minutes - Updates status to
expired - Notifies customer "Booking expired, please try again"
Firebase Remote Config Integration
Used Firebase for feature flags and dynamic config:
- Payment gateway toggle (Stripe vs local provider)
- Booking hold timeout (5 min vs 10 min based on A/B test)
- Currency preferences per country
- Commission rates per venue tier
Why Firebase?
- No redeployment for config changes
- A/B testing built-in
- Real-time updates across all clients
Technical Decisions
Why Optimistic Locking over Pessimistic?
- Pessimistic locks hurt concurrent bookings (long wait times)
- Optimistic approach: fast for most users, only conflicts fail
- Database constraint is final source of truth
- Better UX (instant feedback, rare conflicts)
Why Moment.js for Timezones?
- Native
DateAPI terrible for timezones - Moment-timezone has IANA timezone database
- Consistent date arithmetic across browsers
- (Note: Would migrate to
date-fns-tztoday for smaller bundle)
Why Separate Vendor and Superadmin Portals?
- Different permissions and workflows
- Vendors can't see platform-wide data
- Superadmin manages payouts, disputes, fraud detection
- Clear separation of concerns
Why Redux for Booking State?
- Booking flow spans multiple components (court selection → date picker → payment → confirmation)
- Need to maintain state across page transitions
- Debugging payment issues easier with Redux DevTools
- Time-travel debugging for state machine bugs
Impact
Business Metrics
- 4 portals delivered across customer, vendor, superadmin, backend
- Zero double-booking incidents since launch
- Multi-timezone support for 10+ countries
- Payment success rate: 94% (industry average ~90%)
Technical Achievements
- Optimistic locking prevents overlaps without pessimistic locks
- Timezone-aware bookings work across 15+ timezones
- Payment state machine handles edge cases (retry, refund, expiry)
- Firebase remote config enables instant feature toggles
Developer Experience
- Reusable timezone utilities across all portals
- State machine pattern prevents invalid booking states
- Comprehensive E2E tests for booking critical path
- Redux DevTools for debugging customer support issues
What I'd Do Differently
Replace Moment.js
Would migrate to date-fns-tz:
- Moment.js is 67KB (huge bundle)
- date-fns is tree-shakeable, modular
- Faster performance, better TypeScript support
- Current approach works but bloats bundle
WebSocket for Real-Time Slots
Would implement WebSocket updates for availability:
- Current polling (every 10s) has lag
- WebSocket pushes instant updates when slots booked
- Better UX (no stale availability)
- Reduced server load
Idempotent Payment Handling
Would enforce idempotency keys on payment requests:
- Prevents duplicate charges on retry
- Current approach has race condition risk
- Stripe supports idempotency natively
- Should have implemented from day one
Automated Timezone Testing
Would add timezone simulation tests:
- Test booking flow in every supported timezone
- Automated checks for DST edge cases
- Current manual testing is error-prone
- Hard-to-catch bugs in production
Key Takeaways
- Optimistic locking beats pessimistic for booking systems - race conditions are rare, pessimistic locks hurt UX
- Timezones are hard - always store UTC, convert for display, test DST boundaries
- State machines prevent bugs - explicit transitions catch invalid states early
- Database constraints are your friend - they're the final arbiter of truth
- Feature flags enable fearless deployment - toggle problematic features without redeployment