Skip to main content
SaaS Development9 min read

Building a Hotel Reservation System: Architecture & Lessons

A
Axiosware
Engineering Team

Building a hotel reservation system isn't just about booking rooms—it's about orchestrating availability, payments, and guest experiences in real-time. At Axiosware, we've learned this through building platforms like Isla Hotel, which achieved a 68% reduction in front-desk calls in just 6 weeks.

Key Takeaways

  • ✓ Real-time availability requires optimistic locking and cache invalidation strategies
  • ✓ Payment webhooks must handle race conditions and idempotency
  • ✓ Multi-tenant architecture scales better than single-property designs
  • ✓ Mobile-first booking flows convert 2.3x better than desktop-only

The Core Challenge: Availability is Everything

The single hardest problem in hotel reservation systems is managing availability across multiple channels. A guest books on your website, then calls to modify, then a third-party OTA (Online Travel Agency) pushes a cancellation. Your database needs to handle all of this without double-bookings.

We solved this with a combination of database-level optimistic locking and Redis-based caching:

-- Drizzle ORM schema with optimistic locking
export const rooms = pgTable('rooms', {
  id: uuid('id').primaryKey(),
  hotelId: uuid('hotel_id').notNull(),
  roomNumber: varchar('room_number').notNull(),
  capacity: integer('capacity').notNull(),
  basePrice: decimal('base_price').notNull(),
  version: integer('version').notNull().default(0), // Optimistic lock
});

export const bookings = pgTable('bookings', {
  id: uuid('id').primaryKey(),
  roomId: uuid('room_id').notNull(),
  guestId: uuid('guest_id').notNull(),
  checkIn: timestamp('check_in').notNull(),
  checkOut: timestamp('check_out').notNull(),
  status: varchar('status').notNull(), // pending, confirmed, cancelled
  version: integer('version').notNull().default(0),
  totalPrice: decimal('total_price').notNull(),
});

The critical piece is the booking transaction that checks version numbers:

async function createBooking(roomId: string, guestId: string, checkIn: Date, checkOut: Date) {
  const bookingId = crypto.randomUUID();
  
  return db.transaction(async (tx) => {
    // Check availability with optimistic lock
    const room = await tx
      .select()
      .from(rooms)
      .where(eq(rooms.id, roomId))
      .forUpdate() // Row-level lock
      .limit(1);
    
    if (!room.length) throw new Error('Room not found');
    
    // Check for overlapping bookings
    const conflicts = await tx
      .select()
      .from(bookings)
      .where(
        and(
          eq(bookings.roomId, roomId),
          eq(bookings.status, 'confirmed'),
          or(
            and(
              gte(bookings.checkIn, checkIn),
              lte(bookings.checkIn, checkOut)
            ),
            and(
              gte(bookings.checkOut, checkIn),
              lte(bookings.checkOut, checkOut)
            ),
            and(
              lte(bookings.checkIn, checkIn),
              gte(bookings.checkOut, checkOut)
            )
          )
        )
      );
    
    if (conflicts.length > 0) {
      throw new Error('Room not available for selected dates');
    }
    
    // Create booking with version increment
    await tx.insert(bookings).values({
      id: bookingId,
      roomId,
      guestId,
      checkIn,
      checkOut,
      status: 'pending',
      version: 0,
      totalPrice: '0',
    });
    
    return bookingId;
  });
}

Payment Processing: Don't Lose Money on Failed Bookings

Stripe webhooks are where reservation systems go to die if you don't handle them correctly. A guest books a room, gets charged, but the webhook fails. Do you refund? Do you hold the booking? The answer is: idempotency keys and webhook retry logic.

Stripe Webhook Handler Pattern

Every webhook endpoint must be idempotent—processing the same event twice should have no side effects:

// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature');
  
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature!,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return new Response('Webhook signature invalid', { status: 400 });
  }
  
  // Idempotency: check if already processed
  const existing = await db.query.bookings.findFirst({
    where: eq(bookings.stripeEventId, event.id),
  });
  
  if (existing) {
    return new Response('Already processed', { status: 200 });
  }
  
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await confirmBooking(paymentIntent.metadata.bookingId);
      break;
    case 'payment_intent.payment_failed':
      await cancelPendingBooking(event.data.object.id);
      break;
  }
  
  // Mark as processed
  await db
    .update(bookings)
    .set({ stripeEventId: event.id })
    .where(eq(bookings.id, event.data.object.metadata.bookingId));
  
  return new Response('OK', { status: 200 });
}

Case Study: Isla Hotel

Case Study: Isla Hotel

Challenge: A boutique hotel chain was drowning in phone calls for basic reservations. Front-desk staff spent 40% of their time answering booking questions instead of guest service.

Solution: We built a complete hotel reservation platform with:

  • Real-time availability calendar with 500ms response times
  • Mobile-first booking flow with Apple Pay and Google Pay
  • Automated email/SMS confirmations via Resend and Twilio
  • Admin dashboard for staff to manage bookings and room status

Results: 68% reduction in front-desk calls, 4.5x increase in online bookings, and $127K additional revenue in the first quarter post-launch.

Multi-Tenant Architecture for Hotel Chains

If you're building for a single property, a simple database works. But hotel chains need multi-tenant isolation with shared infrastructure. We use a hybrid approach:

Architecture Decision: Shared Database, Row-Level Security

Each hotel gets its own schema within a shared PostgreSQL database. This gives us:

  • ✓ Isolated data per property (guest A can't see hotel B's bookings)
  • ✓ Shared infrastructure costs (one database cluster for all hotels)
  • ✓ Easy cross-property reporting for chain owners
  • ✓ Simplified backups and scaling

The database schema includes tenant isolation:

export const hotels = pgTable('hotels', {
  id: uuid('id').primaryKey(),
  name: varchar('name').notNull(),
  slug: varchar('slug').notNull().unique(),
  address: jsonb('address').notNull(),
  timezone: varchar('timezone').notNull(), // For availability calculations
  commissionRate: decimal('commission_rate').notNull(), // Platform fee %
  isActive: boolean('is_active').notNull().default(true),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

export const bookings = pgTable('bookings', {
  id: uuid('id').primaryKey(),
  hotelId: uuid('hotel_id').notNull().references(() => hotels.id),
  // ... rest of booking fields
});

Mobile-First Booking Flows

Our data shows that mobile bookings convert 2.3x better than desktop. The key is reducing friction:

  1. Guest recognition: Allow guests to book with just email—no account creation required. We send a magic link for future bookings.
  2. Apple Pay/Google Pay: One-tap payment reduces checkout abandonment by 35%.
  3. Offline-first caching: Guests can browse availability even with spotty hotel Wi-Fi. The app syncs when connection is restored.
  4. Push notifications: Check-in reminders, room-ready alerts, and checkout links keep guests engaged without phone calls.

Lessons from Production

After shipping multiple hospitality platforms, here are the hard-won lessons:

1. Timezones Break Everything

Guests in New York booking a Los Angeles hotel will get confused if you show availability in their local time vs. hotel time. We store all times in UTC in the database, but display in the hotel's timezone for availability and the guest's timezone for confirmations.

2. Cancellation Policies Need Flexibility

Different properties have different policies: 24-hour, 48-hour, 7-day, non-refundable. We built a policy engine that calculates refund amounts based on cancellation timing:

function calculateRefund(booking: Booking, cancellationTime: Date): Refund {
  const policy = getPolicy(booking.hotelId);
  const timeUntilCheckIn = booking.checkIn.getTime() - cancellationTime.getTime();
  
  if (policy.type === 'non_refundable') {
    return { amount: '0', reason: 'non_refundable' };
  }
  
  if (timeUntilCheckIn < policy.gracePeriodMs) {
    return { amount: booking.totalPrice, reason: 'full_refund' };
  }
  
  if (timeUntilCheckIn < policy.fullChargeThresholdMs) {
    return { amount: '0', reason: 'one_night_charge' };
  }
  
  return { amount: '0', reason: 'no_refund' };
}

3. OTA Integrations Are a Nightmare

Booking.com, Expedia, Airbnb—they all have different APIs, different cancellation rules, different payment flows. We built a unified OTA adapter pattern that normalizes these differences:

interface OTAAdapter {
  syncAvailability(rooms: Room[]): Promise;
  syncBookings(bookings: ExternalBooking[]): Promise;
  cancelBooking(externalId: string): Promise;
}

class BookingComAdapter implements OTAAdapter {
  async syncAvailability(rooms: Room[]) {
    // Map Axiosware room IDs to Booking.com inventory IDs
    // Handle rate limits and batch updates
  }
}

class ExpediaAdapter implements OTAAdapter {
  async syncAvailability(rooms: Room[]) {
    // Different API, different payload structure
  }
}

Tech Stack That Scales

The Stack

Frontend: Next.js 14 with App Router and Server Components for fast initial loads

Mobile: React Native with Expo for iOS and Android from one codebase

Backend: Node.js API routes + Supabase for PostgreSQL

Payments: Stripe with webhooks for all payment events

Email: Resend with React Email for transactional messages

Hosting: Vercel for automatic scaling and global CDN

Analytics: PostHog for product analytics and feature flags

When NOT to Build Custom

Honesty matters: sometimes a hotel chain should use existing software instead of building custom. Here's when we recommend off-the-shelf:

Use Existing Software If:

  • ✓ You're a single-property boutique hotel with < 20 rooms
  • ✓ You don't need custom integrations with property management systems
  • ✓ Your budget is under $10K
  • ✓ You're okay with monthly SaaS fees vs. one-time build cost

For these cases, we recommend platforms like Cloudbeds or Little Hotelier. But if you need custom features, multi-property support, or want to own your data—custom builds make sense.

The Bottom Line

Building a hotel reservation system is hard because you're competing with years of optimization from established players. But the opportunity is real: hospitality software is still stuck in the past, and guests expect modern experiences.

At Axiosware, we've learned that the key differentiators are:

  • ✓ Real-time availability that actually works (no double-bookings)
  • ✓ Payment processing that doesn't lose money on edge cases
  • ✓ Mobile-first design that converts better than desktop
  • ✓ Multi-tenant architecture that scales from 1 property to 100

If you're a hotel chain or property management company looking to modernize your booking system, we can help. Our Growth Engine package includes multi-platform builds (web + iOS + Android) with all the features discussed here.

Ready to Build?

Whether you're a single-property owner or a hotel chain, we can build a reservation system that reduces front-desk calls, increases online bookings, and gives you 100% ownership of your platform.

Start a Project

Want to see more examples? Check out our case studies including Isla Hotel, Lefty's Cheesesteaks, and other successful launches.

Tags

hotel reservationbooking systemhospitality softwareSaaS architectureStripe integrationmulti-tenant

Want More Engineering Insights?

Get startup architecture patterns, AI development techniques, and product launch strategies delivered to your inbox.

Join the Axiosware Newsletter

Weekly insights for founders and technical leaders

We respect your privacy. Unsubscribe at any time.