sports

Playlyne

Sports Venue Booking Platform

ReactTypeScriptReduxNode.jsSequelizeFirebaseGoogle Maps
4
Portals Built
Real-time
Booking Flow
Multi-region
Timezone Handling

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:

  1. Customer App - Browse venues, book slots, pay, review
  2. Vendor Hub - Manage courts, pricing, availability, bookings, payouts
  3. Superadmin Portal - Approve vendors, manage payouts, platform analytics
  4. 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 pending bookings 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 Date API terrible for timezones
  • Moment-timezone has IANA timezone database
  • Consistent date arithmetic across browsers
  • (Note: Would migrate to date-fns-tz today 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

  1. Optimistic locking beats pessimistic for booking systems - race conditions are rare, pessimistic locks hurt UX
  2. Timezones are hard - always store UTC, convert for display, test DST boundaries
  3. State machines prevent bugs - explicit transitions catch invalid states early
  4. Database constraints are your friend - they're the final arbiter of truth
  5. Feature flags enable fearless deployment - toggle problematic features without redeployment