Building search, booking, pricing, and trust systems at 7M+ listing global scale
Table of Contents
- Introduction — Airbnb at Global Scale
- Requirements Gathering
- Capacity Estimation
- Data Model
- API Design
- High-Level Architecture
- Search & Discovery
- Calendar & Availability System
- Booking Flow & State Machine
- Dynamic Pricing Engine
- Payment Processing
- Review & Rating System
- Trust & Safety
- Messaging Between Host & Guest
- Photo Management & Verification
- Map-Based Search
- Recommendation Engine
- Host Dashboard & Tools
- Cancellation & Refund Policy
- Database Sharding
- Caching Strategy
- Multi-Region Design
- Cost Estimation
- Interview Q&A (10+ Questions)
- Full C# Implementation (300+ Lines)
- Conclusion
1. Introduction — Airbnb at Global Scale
Airbnb fundamentally disrupted the hospitality industry by creating a two-sided marketplace connecting hosts who have space with guests who need a place to stay. Since its founding in 2008, the platform has grown to host over 7 million listings across 220+ countries and regions, facilitating hundreds of millions of nights booked annually. The platform processes billions of dollars in transactions each quarter and serves as the backbone for millions of hosts who depend on it for their livelihood.
Designing an accommodation platform at this scale is one of the most fascinating system design challenges because it combines elements of marketplace design, real-time inventory management, geo-spatial search, dynamic pricing, payment processing, trust and safety, and content delivery. Unlike many other marketplace platforms, accommodation booking has unique constraints: inventory is perishable (an unsold night is revenue lost forever), each listing is unique, availability changes in real-time, and pricing is highly dynamic.
In this comprehensive guide, we will walk through the complete system design of an Airbnb-like accommodation platform. We will cover everything from high-level architecture down to database sharding strategies, from the booking state machine to the dynamic pricing engine, from trust and safety mechanisms to multi-region deployment strategies. This guide is designed for senior+ engineers preparing for system design interviews or architects building similar platforms in production.
2. Requirements Gathering
Functional Requirements
| Requirement | Description | Priority |
|---|---|---|
| Property Search | Search listings by location, dates, guests, price range, and amenities with geo-filtering | P0 |
| Listing Management | Hosts can create, edit, and manage listings with photos, descriptions, pricing, and availability | P0 |
| Booking | Guests can book listings for specific date ranges with availability validation | P0 |
| Payment Processing | Secure payments with split payment between platform and host, holds, and refunds | P0 |
| Calendar & Availability | Real-time availability calendar with instant book and request-to-book options | P0 |
| Reviews & Ratings | Two-way review system after completed stays | P0 |
| Messaging | Real-time messaging between hosts and guests | P1 |
| Dynamic Pricing | Price suggestions based on demand, seasonality, events, and competition | P1 |
| Trust & Safety | Identity verification, fraud detection, and content moderation | P0 |
| Map-Based Search | Interactive map view for property discovery | P1 |
| Recommendations | Personalized property recommendations based on user behavior | P1 |
| Host Dashboard | Analytics, earnings reports, booking management for hosts | P1 |
| Cancellation Policies | Flexible, moderate, strict, and custom cancellation policies | P0 |
Non-Functional Requirements
- Availability: 99.99% uptime (less than 52 minutes downtime per year)
- Latency: Search results returned within 200ms (p99), booking confirmation within 500ms
- Consistency: Strong consistency for booking (no double-booking), eventual consistency for search results
- Throughput: Handle 500K search queries per second during peak, 10K bookings per second
- Scalability: Support 100M+ registered users, 7M+ active listings
- Durability: Zero data loss for booking and payment records
- Geo-Distribution: Low-latency access from all major markets globally
3. Capacity Estimation
| Metric | Daily | Monthly | Annual |
|---|---|---|---|
| Total Users | — | 150M MAU | 500M+ registered |
| Search Queries | 200M | 6B | 72B |
| Page Views | 500M | 15B | 180B |
| Bookings | 1.5M | 45M | 540M |
| Active Listings | — | 7M | 7M+ |
| Reviews | 500K | 15M | 180M |
| Messages Sent | 10M | 300M | 3.6B |
| Photos Uploaded | 2M | 60M | 720M |
| Revenue (GTV) | $1.5B | $45B | $540B |
Storage Estimation
Listing data: 7M listings × ~5KB metadata = 35 GB
Booking records: 540M bookings/year × ~2KB = ~1 TB/year
Reviews: 180M reviews/year × ~1KB = 180 GB/year
Photos: 720M photos/year × ~3MB avg = 2.1 PB/year (stored in S3, metadata in DB)
User profiles: 500M users × ~2KB = 1 TB
Bandwidth Estimation
Inbound: 2M photos × 3MB = ~6 TB/day = ~70 MB/s average
Outbound: 500M page views × ~500KB avg page = 250 TB/day = ~2.9 GB/s average
4. Data Model
The data model for an accommodation platform is inherently complex due to the relationships between users (both hosts and guests), listings, bookings, availability, pricing, and reviews. Below we present the core entities and their relationships.
Entity Relationship Overview
Core Table Definitions
| Table | Storage Engine | Shard Key | Index | Est. Size/Year |
|---|---|---|---|---|
| users | InnoDB (MySQL) | user_id | email, phone | 1 TB |
| listings | InnoDB + ES | listing_id | host_id, location (geo) | 50 GB |
| availability_calendar | InnoDB + Redis | listing_id | listing_id + date (unique) | 200 GB |
| bookings | InnoDB | listing_id | guest_id, check_in, status | 1 TB |
| reviews | InnoDB + ES | listing_id | booking_id (unique) | 180 GB |
| messages | Cassandra | conversation_id | sender_id, sent_at | 500 GB |
| payments | InnoDB | booking_id | payer_id, status | 500 GB |
| pricing_rules | InnoDB | listing_id | listing_id + rule_type | 10 GB |
Key Design Decisions
The availability_calendar table is one of the most critical and heavily accessed tables. For each active listing, we maintain pre-computed availability rows for the next 365 days. This means 7M listings × 365 days = 2.56 billion rows — far too large for a single database. We shard this by listing_id and heavily cache hot listings in Redis.
Bookings use listing_id as the shard key rather than guest_id because the majority of query patterns involve checking bookings for a specific listing (availability check, host dashboard). Guest-centric queries (my trips) are handled through secondary indexes or denormalized views.
Messages use Cassandra instead of MySQL because the access pattern is append-heavy, time-series oriented, and benefits from Cassandra's linear write scalability and tunable consistency.
5. API Design
All APIs follow RESTful conventions with JSON payloads. Authentication uses JWT tokens with refresh token rotation. Rate limiting is applied per-user and per-IP. All endpoints are versioned under /api/v1/.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/search | Search listings by location, dates, guests, filters | Optional |
| GET | /api/v1/listings/{id} | Get listing details, photos, calendar, reviews | Optional |
| POST | /api/v1/listings | Create a new listing | Host |
| PUT | /api/v1/listings/{id} | Update listing details | Host |
| GET | /api/v1/listings/{id}/calendar | Get availability calendar for a listing | Optional |
| PUT | /api/v1/listings/{id}/calendar | Update availability calendar | Host |
| GET | /api/v1/listings/{id}/price | Get price estimate for date range | Optional |
| POST | /api/v1/bookings | Create a new booking | Guest |
| GET | /api/v1/bookings/{id} | Get booking details | Guest/Host |
| POST | /api/v1/bookings/{id}/confirm | Host confirms a booking | Host |
| POST | /api/v1/bookings/{id}/cancel | Cancel a booking | Guest/Host |
| GET | /api/v1/users/me/trips | Get current user's bookings | Guest |
| GET | /api/v1/users/me/listings | Get host's listings | Host |
| POST | /api/v1/reviews | Submit a review for a completed booking | Guest/Host |
| GET | /api/v1/conversations | List user's conversations | Auth |
| POST | /api/v1/conversations/{id}/messages | Send a message | Auth |
| POST | /api/v1/payments | Process payment for a booking | Guest |
| POST | /api/v1/listings/{id}/photos | Upload listing photos | Host |
| GET | /api/v1/map/search | Map-bound search with clustering | Optional |
| GET | /api/v1/recommendations | Personalized listing recommendations | Optional |
Search API Request/Response Example
// Request: GET /api/v1/search?lat=40.7128&lng=-74.0060&checkin=2026-08-01&checkout=2026-08-05&guests=2&price_min=50&price_max=300&room_type=entire_home
// Response:
{
"search_id": "srch_abc123",
"results_count": 1847,
"listings": [
{
"id": "lst_xyz789",
"title": "Cozy Brooklyn Loft with Skyline View",
"lat": 40.6892,
"lng": -73.9857,
"price_per_night": 145.00,
"total_price": 623.50,
"rating": 4.87,
"review_count": 342,
"room_type": "Entire home/apt",
"thumbnail_url": "https://cdn.airbnb.com/photos/lst_xyz789_thumb.webp",
"host": {
"name": "Sarah",
"avatar": "https://cdn.airbnb.com/avatars/sarah.webp",
"is_superhost": true
},
"amenities": ["wifi", "kitchen", "washer", "air_conditioning"],
"instant_book": true,
"distance_km": 3.2
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total_pages": 93
}
}
6. High-Level Architecture
The platform uses a microservices architecture with clear domain boundaries. Each service owns its data and communicates via a combination of synchronous REST/gRPC for real-time queries and asynchronous message queues for event-driven workflows.
(React)"] MobileiOS["iOS App
(Swift)"] MobileAndroid["Android App
(Kotlin)"] end subgraph "Edge Layer" CDN["CDN
(CloudFront)"] WAF["WAF / DDoS
Protection"] APIGW["API Gateway
/ Load Balancer"] end subgraph "Service Layer" SearchSvc["Search Service"] ListingSvc["Listing Service"] BookingSvc["Booking Service"] PricingSvc["Pricing Service"] PaymentSvc["Payment Service"] UserSvc["User Service"] ReviewSvc["Review Service"] MessageSvc["Messaging Service"] TrustSvc["Trust & Safety"] NotifSvc["Notification Service"] RecSvc["Recommendation
Service"] CalendarSvc["Calendar Service"] end subgraph "Data Layer" ES["Elasticsearch
Cluster"] MySQL["MySQL
(Sharded)"] Redis["Redis
Cluster"] Cassandra["Cassandra
Cluster"] S3["S3 / Object
Storage"] GraphDB["Neo4j
(Graph)"] end subgraph "Infrastructure" Kafka["Apache Kafka"] SQS["SQS / RabbitMQ"] Consul["Service
Discovery"] Prometheus["Prometheus
& Grafana"] end WebApp --> CDN MobileiOS --> CDN MobileAndroid --> CDN CDN --> WAF --> APIGW APIGW --> SearchSvc APIGW --> ListingSvc APIGW --> BookingSvc APIGW --> PaymentSvc APIGW --> UserSvc APIGW --> ReviewSvc APIGW --> MessageSvc SearchSvc --> ES SearchSvc --> Redis ListingSvc --> MySQL ListingSvc --> S3 BookingSvc --> MySQL BookingSvc --> Kafka PricingSvc --> Redis PricingSvc --> ES PaymentSvc --> MySQL UserSvc --> MySQL ReviewSvc --> MySQL ReviewSvc --> ES MessageSvc --> Cassandra TrustSvc --> MySQL CalendarSvc --> MySQL CalendarSvc --> Redis RecSvc --> GraphDB RecSvc --> ES BookingSvc --> SQS SQS --> NotifSvc NotifSvc --> Kafka
Service Responsibilities
| Service | Responsibility | Database | Scaling Strategy |
|---|---|---|---|
| Search Service | Full-text and geo search, filtering, sorting, availability overlay | Elasticsearch + Redis | Horizontal (stateless) |
| Listing Service | CRUD for listings, photo management, content moderation | MySQL + S3 | Read replicas + cache |
| Booking Service | Booking creation, state management, conflict resolution | MySQL (sharded) | Sharding by listing_id |
| Calendar Service | Availability management, blocked dates, sync across channels | MySQL + Redis | Sharding by listing_id |
| Pricing Service | Dynamic pricing calculations, price suggestions, seasonal rules | Redis + MySQL | Horizontal + cache |
| Payment Service | Payment processing, payouts, splits, holds, refunds | MySQL (ACID) | Read replicas + queue |
| User Service | Auth, profiles, verification, preferences | MySQL | Read replicas |
| Review Service | Reviews, ratings aggregation, content moderation | MySQL + ES | Horizontal + cache |
| Messaging Service | Real-time chat, notifications, message history | Cassandra | Horizontal (partition by conv) |
| Trust & Safety | Fraud detection, identity verification, content scanning | MySQL + ML pipeline | Horizontal |
| Recommendation | Personalized listings, similar properties, trending | Neo4j + ES | Batch + real-time pipeline |
7. Search & Discovery
Search is the most complex and highest-traffic subsystem. It must handle hundreds of thousands of queries per second while returning results in under 200ms. The system combines Elasticsearch for full-text and geo queries, Redis for caching hot results, and a real-time availability overlay to filter out booked dates.
Search Architecture
Elasticsearch Index Structure
{
"mappings": {
"properties": {
"listing_id": { "type": "keyword" },
"title": { "type": "text", "analyzer": "standard" },
"description": { "type": "text", "analyzer": "standard" },
"location": { "type": "geo_point" },
"property_type": { "type": "keyword" },
"room_type": { "type": "keyword" },
"max_guests": { "type": "integer" },
"bedrooms": { "type": "integer" },
"beds": { "type": "integer" },
"bathrooms": { "type": "integer" },
"base_price": { "type": "scaled_float", "scaling_factor": 100 },
"rating": { "type": "float" },
"review_count": { "type": "integer" },
"amenities": { "type": "keyword" },
"host_is_superhost": { "type": "boolean" },
"instant_book": { "type": "boolean" },
"is_active": { "type": "boolean" },
"updated_at": { "type": "date" }
}
}
}
Search Ranking Algorithm
Search results are ranked using a multi-factor scoring algorithm that balances relevance, quality, and conversion probability:
| Factor | Weight | Description |
|---|---|---|
| Geo Relevance | 25% | Distance from search center point; closer is better with exponential decay |
| Listing Quality Score | 20% | Composite of rating, review count, response rate, acceptance rate |
| Conversion Probability | 20% | ML model predicting likelihood of booking based on user profile and listing attributes |
| Price Competitiveness | 15% | How the listing price compares to similar listings in the same area |
| Recency | 10% | Newer listings and recently updated listings get a small boost |
| Superhost Premium | 5% | Superhost listings receive a ranking boost |
| Sponsored | 5% | Promoted listings appear in designated slots |
Search Filters Implementation
The search service supports over 40 filter dimensions. The most commonly used filters include:
- Location: Geo bounding box or radius from center point, with support for custom drawn areas on the map
- Dates: Overlaid against the availability calendar to exclude unavailable listings
- Guests: Filter by max_guests capacity
- Price Range: Min/max nightly price (pre-computed from pricing engine)
- Property Type: House, apartment, cabin, villa, treehouse, etc.
- Room Type: Entire place, private room, shared room
- Amenities: WiFi, pool, kitchen, air conditioning, hot tub, washer, etc.
- Booking Options: Instant book, flexible cancellation, self check-in
- Host Language: Filter by host's spoken languages
- Accessibility: Step-free guest entrance, wide doorways, etc.
8. Calendar & Availability System
The calendar system is the backbone of the entire platform. Every listing has a daily availability grid that determines whether it can be booked on any given date. The calendar must support:
- Reading availability millions of times per second across search and listing detail pages
- Writing availability updates from hosts blocking/unblocking dates
- Atomic booking operations that claim dates without allowing double-booking
- Sync with external calendars (iCal, Google Calendar, VRBO) to prevent conflicts
Calendar Data Structure
For each listing, we pre-compute availability rows for 365 days into the future. The key table uses a unique constraint on (listing_id, date) to prevent race conditions at the database level.
| Column | Type | Description |
|---|---|---|
| listing_id | UUID | Foreign key to the listing |
| date | DATE | The specific calendar date |
| is_available | BOOLEAN | Whether the date is open for booking |
| price | DECIMAL | Override price for this specific date (NULL = use base + rules) |
| min_nights | INT | Minimum stay requirement for this date |
| max_nights | INT | Maximum stay allowed starting from this date |
| blocked_reason | ENUM | NULL, HOST_BLOCKED, BOOKED, MAINTENANCE |
Availability Check Flow
WHERE listing_id = ? AND date BETWEEN ? AND ?] D --> C C -->|Yes| E{Instant Book enabled?} C -->|No| F[Return unavailable dates] E -->|Yes| G[Reserve dates with SELECT FOR UPDATE] E -->|No| H[Create pending request
Notify host] G --> I{Lock acquired?} I -->|Yes| J[Hold dates for 30 minutes
Create reservation token] I -->|No| K[Return conflict error
Another booking in progress] J --> L[Guest completes payment] L --> M[Confirm booking] M --> N[Update ES index] N --> O[Send confirmations]
Calendar Sync with External Channels
Many hosts list their properties on multiple platforms. The calendar sync system uses iCal feed parsing and webhook-based push notifications to keep availability consistent across channels. When a booking is made on any platform, the dates are blocked across all connected channels within 60 seconds.
public class CalendarSyncService
{
private readonly ICalParser _icalParser;
private readonly ICalendarRepository _calendarRepo;
private readonly IEventPublisher _eventPublisher;
public async Task SyncExternalCalendarAsync(
Guid listingId, string icalUrl, CancellationToken ct)
{
var feed = await _icalParser.ParseFromUrlAsync(icalUrl, ct);
var blockedDates = feed.Events
.Where(e => e.IsBlocked)
.Select(e => new CalendarUpdate
{
ListingId = listingId,
Date = e.Date,
IsAvailable = false,
BlockedReason = BlockedReason.ExternalSync,
ExternalSource = feed.Source,
SyncedAt = DateTime.UtcNow
})
.ToList();
await _calendarRepo.BulkUpsertAsync(blockedDates, ct);
await _eventPublisher.PublishAsync(new CalendarSyncedEvent
{
ListingId = listingId,
UpdatedDates = blockedDates.Select(d => d.Date).ToList()
}, ct);
}
}
9. Booking Flow & State Machine
The booking flow involves multiple services coordinating to ensure a reservation is properly created, payments are processed, and both parties are notified. The booking goes through a well-defined state machine.
Booking State Machine
Booking Creation — Detailed Flow
Double-Booking Prevention
Preventing double-bookings is the most critical correctness requirement. We use a SELECT FOR UPDATE pattern with row-level locking on the availability table:
public class BookingService
{
private readonly AppDbContext _db;
private readonly ICalendarService _calendar;
public async Task<BookingResult> CreateBookingAsync(
CreateBookingRequest request, CancellationToken ct)
{
using var transaction = await _db.Database
.BeginTransactionAsync(IsolationLevel.Serializable, ct);
try
{
// Lock all calendar rows for the date range
var calendarRows = await _db.AvailabilityCalendar
.FromSqlRaw(
"SELECT * FROM availability_calendar " +
"WHERE listing_id = @listingId " +
"AND date BETWEEN @checkIn AND @checkOut " +
"FOR UPDATE",
new SqlParameter("@listingId", request.ListingId),
new SqlParameter("@checkIn", request.CheckIn),
new SqlParameter("@checkOut", request.CheckOut))
.ToListAsync(ct);
// Verify all dates are available
if (calendarRows.Any(r => !r.IsAvailable))
{
await transaction.RollbackAsync(ct);
return BookingResult.Fail("Some dates are no longer available");
}
// Create booking record
var booking = new Booking
{
Id = Guid.NewGuid(),
ListingId = request.ListingId,
GuestId = request.GuestId,
HostId = request.HostId,
CheckIn = request.CheckIn,
CheckOut = request.CheckOut,
Guests = request.Guests,
Status = BookingStatus.Pending,
TotalPrice = request.TotalPrice,
CreatedAt = DateTime.UtcNow
};
_db.Bookings.Add(booking);
// Mark dates as booked
foreach (var row in calendarRows)
{
row.IsAvailable = false;
row.BlockedReason = BlockedReason.Booked;
}
await _db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return BookingResult.Success(booking);
}
catch
{
await transaction.RollbackAsync(ct);
throw;
}
}
}
10. Dynamic Pricing Engine
The pricing engine is one of the most sophisticated components. It uses a combination of base pricing, rule-based adjustments, and ML-based optimization to suggest optimal nightly rates. Airbnb's Smart Pricing adjusts prices dynamically based on over 100 signals.
Pricing Factors
Price Calculation Pipeline
| Step | Component | Adjustment | Example |
|---|---|---|---|
| 1 | Base Price | Host-set nightly rate | $150/night |
| 2 | Day-of-Week | Weekend premium / weekday discount | +15% Fri-Sat |
| 3 | Seasonality | Seasonal multiplier | +40% summer in beach cities |
| 4 | Event Surge | Local event demand spike | +80% during Super Bowl |
| 5 | Lead Time | Last-minute or far-out discount | -20% if booking < 3 days out |
| 6 | Occupancy Rate | Adjust based on upcoming occupancy | -10% if next 30 days low occupancy |
| 7 | Competitor Analysis | Compare with similar listings | Align with market median ±10% |
| 8 | ML Model | Neural network optimization | +$8 predicted optimal point |
| 9 | Min/Max Bounds | Enforce host-defined floors/ceilings | Clamp to $100-$300 range |
| 10 | Round & Validate | Round to nearest $5 | $213 → $215 |
Pricing Service Implementation
public class PricingEngine
{
private readonly IPricingRuleRepository _rules;
private readonly IEventRepository _events;
private readonly IMarketDataRepository _marketData;
private readonly IPricingModel _mlModel;
private readonly ILogger<PricingEngine> _logger;
public async Task<PriceQuote> CalculatePriceAsync(
PricingRequest request, CancellationToken ct)
{
var listing = await GetListingAsync(request.ListingId, ct);
var nightlyPrices = new List<NightlyPrice>();
for (var date = request.CheckIn; date < request.CheckOut; date = date.AddDays(1))
{
var price = await CalculateNightlyPriceAsync(listing, date, ct);
nightlyPrices.Add(new NightlyPrice
{
Date = date,
BasePrice = listing.BasePrice,
AdjustedPrice = price,
Factors = GetAppliedFactors(listing.Id, date)
});
}
var subtotal = nightlyPrices.Sum(p => p.AdjustedPrice);
var cleaningFee = listing.CleaningFee;
var serviceFee = Math.Round(subtotal * 0.14m, 2);
var occupancyTax = Math.Round(subtotal * 0.08m, 2);
return new PriceQuote
{
ListingId = request.ListingId,
NightlyPrices = nightlyPrices,
Subtotal = subtotal,
CleaningFee = cleaningFee,
ServiceFee = serviceFee,
OccupancyTax = occupancyTax,
TotalPrice = subtotal + cleaningFee + serviceFee + occupancyTax,
Currency = listing.Currency,
CalculatedAt = DateTime.UtcNow
};
}
private async Task<decimal> CalculateNightlyPriceAsync(
Listing listing, DateTime date, CancellationToken ct)
{
var price = listing.BasePrice;
// Day of week adjustment
price *= GetDayOfWeekMultiplier(date);
// Seasonal adjustment
price *= await GetSeasonalMultiplierAsync(listing.Location, date, ct);
// Event surge
var eventSurge = await _events.GetEventSurgeAsync(listing.Location, date, ct);
price *= eventSurge;
// Lead time adjustment
var leadDays = (date - DateTime.UtcNow.Date).Days;
price *= GetLeadTimeMultiplier(leadDays);
// Occupancy-based adjustment
var upcomingOccupancy = await GetUpcomingOccupancyAsync(listing.Id, ct);
price *= GetOccupancyMultiplier(upcomingOccupancy);
// Competitor alignment
var marketMedian = await _marketData.GetMedianPriceAsync(
listing.Location, listing.PropertyType, listing.RoomType, ct);
price = AlignWithMarket(price, marketMedian);
// ML model adjustment
var mlAdjustment = await _mlModel.PredictOptimalPriceAsync(
new PricingFeatures
{
ListingId = listing.Id,
Date = date,
CurrentPrice = price,
MarketMedian = marketMedian,
UpcomingOccupancy = upcomingOccupancy
}, ct);
price += mlAdjustment;
// Enforce bounds
price = Math.Max(price, listing.MinPrice ?? listing.BasePrice * 0.6m);
price = Math.Min(price, listing.MaxPrice ?? listing.BasePrice * 3.0m);
return Math.Round(price / 5) * 5; // Round to nearest $5
}
private decimal GetDayOfWeekMultiplier(DateTime date) => date.DayOfWeek switch
{
DayOfWeek.Friday => 1.15m,
DayOfWeek.Saturday => 1.20m,
DayOfWeek.Sunday => 1.05m,
_ => 1.0m
};
private decimal GetLeadTimeMultiplier(int leadDays) => leadDays switch
{
<= 1 => 0.80m,
<= 3 => 0.90m,
<= 7 => 0.95m,
<= 30 => 1.0m,
<= 90 => 1.05m,
_ => 1.10m
};
}
11. Payment Processing
Payment processing for an accommodation platform is uniquely complex because of split payments (guest pays platform, platform pays host), payment holds (capture at check-in), multi-currency support, and refunds with varying policies.
Payment Flow
(Stripe/Adyen) participant Ledger as Financial Ledger participant Host as Host Payout G->>PayS: Submit payment for booking PayS->>PSP: Authorize $500 (hold) PSP-->>PayS: Authorization confirmed PayS->>Ledger: Record hold (booking_id, amount) PayS-->>G: Payment authorized Note over PayS: At check-in date... PayS->>PSP: Capture $500 PSP-->>PayS: Capture confirmed PayS->>Ledger: Split payment Note right of Ledger: Guest → Platform: $500\nPlatform → Host: $425 (85%)\nPlatform Fee: $75 (15%) PayS->>Host: Initiate payout ($425) Host-->>PayS: Payout initiated PayS->>Ledger: Record payout
Payment Tables
| Table | Purpose | Key Fields |
|---|---|---|
| payment_intents | Tracks authorization and capture lifecycle | id, booking_id, amount, currency, status, PSP reference, created_at |
| payouts | Tracks host payout batches | id, host_id, amount, currency, status, payout_method, initiated_at |
| ledger_entries | Double-entry bookkeeping records | id, transaction_id, account, debit, credit, balance, created_at |
| refunds | Refund transactions with reason codes | id, payment_id, amount, reason, status, initiated_by, created_at |
| payment_methods | Saved payment methods (tokenized) | id, user_id, type, last_four, PSP_token, is_default |
Financial Ledger Implementation
The ledger is the single source of truth for all financial records. It uses double-entry bookkeeping where every transaction creates balanced debit and credit entries. This ensures auditability and makes reconciliation straightforward.
public class FinancialLedger
{
private readonly LedgerDbContext _db;
public async Task<LedgerEntry[]> RecordBookingPaymentAsync(
BookingPaymentRecord record, CancellationToken ct)
{
using var transaction = await _db.Database
.BeginTransactionAsync(IsolationLevel.Serializable, ct);
var entries = new List<LedgerEntry>();
// Guest payment: credit the platform receivables account
entries.Add(new LedgerEntry
{
Id = Guid.NewGuid(),
TransactionId = record.BookingId,
Account = AccountType.PlatformReceivables,
Debit = record.TotalAmount,
Credit = 0,
Currency = record.Currency,
Description = $"Guest payment for booking {record.BookingId}",
CreatedAt = DateTime.UtcNow
});
// Host payout: debit the platform, credit host account
entries.Add(new LedgerEntry
{
Id = Guid.NewGuid(),
TransactionId = record.BookingId,
Account = AccountType.HostPayout,
Debit = 0,
Credit = record.HostPayoutAmount,
Currency = record.Currency,
Description = $"Host payout for booking {record.BookingId}",
CreatedAt = DateTime.UtcNow
});
// Platform fee: debit receivables, credit revenue
entries.Add(new LedgerEntry
{
Id = Guid.NewGuid(),
TransactionId = record.BookingId,
Account = AccountType.PlatformRevenue,
Debit = 0,
Credit = record.ServiceFee,
Currency = record.Currency,
Description = $"Service fee for booking {record.BookingId}",
CreatedAt = DateTime.UtcNow
});
_db.LedgerEntries.AddRange(entries);
await _db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return entries.ToArray();
}
}
12. Review & Rating System
The review system uses a two-way blind review model — both guest and host submit reviews simultaneously, and neither can see the other's review until both have submitted (or the 14-day window expires). Reviews are a critical trust signal for the platform.
Review Categories and Ratings
| Category | Scale | Applies To |
|---|---|---|
| Overall Rating | 1-5 stars (with 0.5 increments) | Guest → Listing |
| Cleanliness | 1-5 stars | Guest → Listing |
| Accuracy | 1-5 stars | Guest → Listing |
| Communication | 1-5 stars | Guest → Host |
| Location | 1-5 stars | Guest → Listing |
| Check-in | 1-5 stars | Guest → Listing |
| Value | 1-5 stars | Guest → Listing |
| Accuracy of Listing | 1-5 stars | Host → Listing (rare) |
| Guest Communication | 1-5 stars | Host → Guest |
| House Rules Compliance | 1-5 stars | Host → Guest |
| Cleanliness | 1-5 stars | Host → Guest |
Review Aggregation
The aggregate rating displayed on a listing uses a weighted Bayesian average rather than a simple arithmetic mean. This prevents listings with very few reviews from appearing at the top with artificially high ratings.
Formula: Rating = (v × R + m × C) / (v + m), where v = number of reviews, R = arithmetic mean, m = minimum reviews threshold (e.g., 10), C = global average rating (e.g., 4.7).
Review Visibility Rules
- Reviews are published only after both parties submit or the 14-day window expires
- Reviews can be flagged for content policy violations (hate speech, discrimination, threats)
- Hosts can publicly respond to reviews but cannot edit or delete them
- Platform may remove reviews that violate content guidelines but this is rare and audited
- Reviews older than 3 years are hidden from the default listing view but remain in search ranking calculations
13. Trust & Safety
Trust and safety is arguably the most critical non-functional system. It encompasses identity verification, fraud detection, content moderation, discrimination prevention, and incident response. The T&S system must protect both hosts and guests while maintaining the platform's reputation.
Trust Architecture
Verification"] Phone["Phone
Verification"] Email["Email
Verification"] Photo["Profile Photo
Verification"] Gov["Government ID
Scan + Liveness"] end subgraph "Detection Systems" Fraud["Fraud Detection
(ML Pipeline)"] Spam["Spam & Scam
Detection"] Disc["Discrimination
Detection"] Content["Content
Moderation"] end subgraph "Response Systems" Auto["Automated
Actions"] Human["Human
Review Queue"] Appeal["Appeals
Process"] Law["Law Enforcement
Request Portal"] end Gov --> Fraud Phone --> Spam Content --> Auto Fraud --> Human Spam --> Human Disc --> Human Human --> Appeal
Identity Verification Levels
| Level | Requirements | Badges Earned | Trust Score Impact |
|---|---|---|---|
| Level 1 | Email + Phone verification | Identity Verified | +10 points |
| Level 2 | Government ID scan with liveness detection | ID Verified | +25 points |
| Level 3 | Address verification (utility bill or bank statement) | Address Verified | +15 points |
| Level 4 | Background check (US/selected countries) | Background Checked | +20 points |
Fraud Detection Signals
The fraud detection system uses an ensemble of rule-based filters and ML models that analyze over 200 features in real-time:
- Behavioral signals: Click patterns, session duration, form fill speed, navigation anomalies
- Payment signals: Card BIN country mismatch, multiple failed payments, rapid booking attempts
- Content signals: Listing photo EXIF data, description similarity to known scams, pricing anomalies
- Network signals: Shared devices, IP addresses, payment methods across accounts
- Temporal signals: Unusual booking times, rapid succession bookings, last-minute high-value bookings
14. Messaging Between Host & Guest
The messaging system enables real-time communication between hosts and guests before, during, and after a stay. It handles over 10 million messages per day and must support rich media, translations, and quick replies.
Messaging Architecture
Gateway"] PubSub["Redis Pub/Sub"] end subgraph "Message Processing" MsgSvc["Message Service"] TransSvc["Translation
Service"] SpamSvc["Spam Filter"] NotifSvc["Notification
Service"] end subgraph "Storage" Cassandra["Cassandra
(Message History)"] S3["S3
(Media Assets)"] end App <-->|WebSocket| WS WS <--> PubSub PubSub --> MsgSvc MsgSvc --> TransSvc MsgSvc --> SpamSvc MsgSvc --> Cassandra MsgSvc --> NotifSvc MsgSvc --> S3
Message Features
- Real-time delivery: WebSocket-based bidirectional communication with automatic reconnection
- Message types: Text, photos, booking requests, booking confirmations, system messages
- Auto-translation: Messages are automatically translated using a neural machine translation service
- Quick replies: Pre-written responses for common questions (check-in instructions, directions)
- Smart suggestions: AI-generated reply suggestions based on conversation context
- Read receipts: Blue checkmarks indicating message delivery and read status
- Reporting: Messages can be reported for policy violations; the reporting user's messages are preserved
15. Photo Management & Verification
Photos are the single most important factor in a guest's booking decision. The platform stores hundreds of millions of photos and must serve them quickly with responsive images across all devices.
Photo Processing Pipeline
NSFW/Illegal Detection] D --> E[Face Detection
Privacy Check] E --> F[Generate Variants
2000px, 1000px, 500px, 200px] F --> G[Compress & Convert
WebP + AVIF] G --> H[Upload to CDN] H --> I[Update Database] I --> J[Trigger Search Index
Update]
Photo Storage Strategy
| Variant | Size | Use Case | Format |
|---|---|---|---|
| Original | Full resolution | Backup, reprocessing | JPEG/PNG |
| Large | 2000px wide | Listing detail page hero | WebP + AVIF |
| Medium | 1000px wide | Gallery, lightbox | WebP + AVIF |
| Small | 500px wide | Search results grid | WebP + AVIF |
| Thumbnail | 200px wide | Map pins, carousel preview | WebP + AVIF |
| BlurHash | 32 bytes | Progressive placeholder | Base64 |
16. Map-Based Search
Map-based search is a core feature that allows guests to explore listings geographically. The system must efficiently render hundreds of thousands of map pins and update results as the user pans and zooms.
Map Pin Clustering
When zoomed out, individual listings are clustered into aggregate pins to avoid visual clutter. The clustering algorithm uses a grid-based approach where pins are grouped into cells based on the current zoom level. Each cluster displays the number of listings and the average price.
| Zoom Level | Cell Size | Typical Cluster Size | Pin Shows |
|---|---|---|---|
| City-wide (zoom 10) | ~5km × 5km | 50-500 listings | Price range for area |
| Neighborhood (zoom 13) | ~1km × 1km | 10-50 listings | Listing count + avg price |
| Street (zoom 15) | ~300m × 300m | 3-10 listings | Individual pins start appearing |
| Block (zoom 17+) | Individual | 1 listing per pin | Price with photo thumbnail |
Viewport-Based Loading
As the user pans the map, the frontend sends the current bounding box coordinates to the backend. The search service performs a geo-bounded query in Elasticsearch and returns only listings within the visible area. Results are cached using a geohash-based key that combines the viewport boundaries and search filters.
17. Recommendation Engine
The recommendation engine powers multiple surfaces: the homepage "Homes you might like," search result ranking, "Similar listings" on listing detail pages, and email recommendations. It uses a combination of collaborative filtering, content-based filtering, and graph-based recommendations.
Recommendation Signals
- User behavior: Search history, saved listings, booking history, viewing patterns, wishlist additions
- Listing features: Property type, amenities, location characteristics, price range, style
- User preferences: Explicitly stated preferences, past booking patterns, price sensitivity
- Social signals: Friends' bookings, trending in user's network
- Contextual signals: Trip purpose (business vs leisure), group size, season
- Graph relationships: Users who booked this also booked..., similar hosts, neighborhood associations
Recommendation Pipeline
The pipeline runs in two modes: batch (nightly recomputation of user embeddings and candidate generation) and real-time (on-demand scoring and re-ranking based on the current session context). The batch pipeline generates candidate sets of 1000 listings per user, and the real-time pipeline re-ranks the top 200 based on session signals before returning the final 20 results.
18. Host Dashboard & Tools
The host dashboard is the command center for hosts to manage their listings, track bookings, view earnings, and optimize their pricing. It requires aggregating data from multiple services into a responsive, real-time interface.
Dashboard Components
| Component | Data Sources | Update Frequency |
|---|---|---|
| Booking Calendar | Calendar Service, Booking Service | Real-time via WebSocket |
| Earnings Overview | Payment Service, Ledger | Daily aggregate, real-time pending |
| Performance Metrics | Review Service, Search Service, Booking Service | Weekly refresh |
| Guest Messages | Messaging Service | Real-time via WebSocket |
| Reservation Manager | Booking Service | Real-time |
| Listing Health | Search ranking data, conversion metrics | Daily |
| Market Insights | Pricing Service, Competitor analysis | Weekly |
| Tax Documents | Payment Service, Tax Service | Annual / Quarterly |
Host Payout Schedule
Host payouts are processed according to a configurable schedule. The default is 24 hours after guest check-in, but hosts can choose daily, weekly, or monthly payouts. Payout methods include bank transfer (ACH/SEPA), PayPal, and wire transfer for international hosts.
19. Cancellation & Refund Policy
Cancellation policies are a critical trust mechanism that protects both hosts and guests. The platform offers several standardized policies that hosts can choose from:
Standard Cancellation Policies
| Policy | Guest Refund | Host Penalty | Best For |
|---|---|---|---|
| Flexible | 100% refund up to 24h before check-in | None (host keeps payout if guest no-shows) | Urban listings with high demand |
| Moderate | 100% refund up to 5 days before check-in | 50% of first night if within 7 days | Most listings |
| Strict | 50% refund up to 7 days before check-in | 50% of total if guest cancels within 7 days | Rural/remote listings |
| Non-refundable | No refund (10% discount offered to guest) | Full payout | High-demand listings |
| Long-term (28+ nights) | 30 days notice required, partial refund | 30 days notice required, partial penalty | Monthly/long-term stays |
Extenuating Circumstances Policy
The platform maintains a separate extenuating circumstances policy that overrides the standard cancellation policy. This covers natural disasters, government-mandated restrictions, serious illness, and other qualifying events. Claims are reviewed by a specialized team with authority to issue full refunds regardless of the selected policy.
20. Database Sharding
At the scale of 7M+ listings and 540M+ bookings per year, a single MySQL instance cannot handle the load. We use application-level sharding with a clear shard key strategy.
Sharding Strategy
Services"] end subgraph "Shard Router" Router["Shard Router
(Consistent Hashing)"] end subgraph "Shard 0" DB0a["listings_0
bookings_0"] DB0b["availability_0"] end subgraph "Shard 1" DB1a["listings_1
bookings_1"] DB1b["availability_1"] end subgraph "Shard 2" DB2a["listings_2
bookings_2"] DB2b["availability_2"] end subgraph "Shard N" DBNa["listings_n
bookings_n"] DBNb["availability_n"] end App --> Router Router --> DB0a Router --> DB1a Router --> DB2a Router --> DBNa
Shard Key Selection
| Table | Shard Key | Rationale | Num Shards |
|---|---|---|---|
| listings | listing_id | All queries for a single listing hit one shard | 64 |
| availability_calendar | listing_id | Co-located with listings for JOIN efficiency | 64 |
| bookings | listing_id | Most queries are listing-centric (host dashboard, availability check) | 64 |
| reviews | listing_id | Reviews are always queried by listing | 32 |
| users | user_id | User-centric queries (my trips, my listings) | 16 |
| messages | conversation_id | Messages grouped by conversation | 32 (Cassandra) |
Cross-Shard Queries
Cross-shard queries are unavoidable for certain operations like "My Trips" (guest has bookings across multiple listing shards). We handle these through:
- Denormalized views: A user_trips table that is co-located on the user's shard, updated asynchronously via Kafka events
- Scatter-gather: For rare analytics queries, we query all shards in parallel and merge results (with pagination)
- Read replicas per shard: Heavy read workloads are offloaded to read replicas to avoid impacting write performance
21. Caching Strategy
Caching is critical for performance at this scale. We use a multi-tier caching strategy with different TTLs and invalidation strategies for different data types.
Cache Hierarchy
| Layer | Technology | What We Cache | TTL | Invalidation |
|---|---|---|---|---|
| L1 - Browser | Service Worker | Listing detail pages, search results | 5-30 min | Stale-while-revalidate |
| L2 - CDN | CloudFront | Static assets, listing photos, API responses | 1-24 hours | Cache invalidation on update |
| L3 - Application | Redis Cluster | Hot search results, listing details, calendar data | 30s - 1hr | Event-driven invalidation |
| L4 - Database | MySQL Query Cache | Frequent read queries | Per-query | Automatic on write |
Cache Keys Pattern
// Search results cache key
search:{lat_rounded}:{lng_rounded}:{checkin}:{checkout}:{guests}:{filters_hash}
// Example: search:40.71:-74.01:2026-08-01:2026-08-05:2:{price_min=50,price_max=300}
// TTL: 30 seconds (high churn, real-time availability changes)
// Listing detail cache key
listing:{listing_id}:{locale}:{currency}
// Example: listing:xyz789:en:USD
// TTL: 5 minutes (moderate staleness acceptable)
// Calendar cache key
calendar:{listing_id}:{year}:{month}
// Example: calendar:xyz789:2026:08
// TTL: 60 seconds (must be near-real-time for availability)
// Price quote cache key
price:{listing_id}:{checkin}:{checkout}:{guests}
// Example: price:xyz789:2026-08-01:2026-08-05:2
// TTL: 5 minutes (pricing can change with demand)
Cache Invalidation Strategy
We use an event-driven invalidation pattern. When a booking is made, the Booking Service publishes a BookingCreatedEvent to Kafka. Downstream cache consumers listen for these events and invalidate the relevant cache keys:
- Booking created: Invalidate calendar cache, search cache for that area, listing availability
- Listing updated: Invalidate listing detail cache, search cache
- Review posted: Invalidate listing detail cache (rating changed)
- Price changed: Invalidate price quote cache, search cache (price sorting affected)
22. Multi-Region Design
To serve users globally with low latency, the platform deploys across multiple AWS regions. The primary regions are US East (Virginia), EU West (Ireland), Asia Pacific (Tokyo), and South America (São Paulo).
Multi-Region Architecture
Services"] US_DB["MySQL Primary
(Sharded)"] US_ES["Elasticsearch
Primary"] US_Redis["Redis
Cluster"] US_S3["S3 Primary"] end subgraph "EU West (Secondary)" EU_EB["Application
Services"] EU_DB["MySQL Read
Replica + Failover"] EU_ES["Elasticsearch
Replica"] EU_Redis["Redis
Cluster"] EU_S3["S3 Replica"] end subgraph "APAC (Secondary)" AP_EB["Application
Services"] AP_DB["MySQL Read
Replica + Failover"] AP_ES["Elasticsearch
Replica"] AP_Redis["Redis
Cluster"] AP_S3["S3 Replica"] end US_DB -->|Async Replication| EU_DB US_DB -->|Async Replication| AP_DB US_ES -->|Cross-Cluster Replication| EU_ES US_ES -->|Cross-Cluster Replication| AP_ES US_S3 -->|Cross-Region Replication| EU_S3 US_S3 -->|Cross-Region Replication| AP_S3
Data Replication Strategy
| Data Type | Replication Mode | Lag Tolerance | Conflict Resolution |
|---|---|---|---|
| User profiles | Async (primary → replicas) | < 1 second | Last-write-wins |
| Listings | Async (primary → replicas) | < 1 second | Last-write-wins |
| Bookings | Sync for writes (primary only), Async reads | 0 for writes | Single-primary, no conflict |
| Calendar | Async (primary → replicas) | < 5 seconds | CRDTs for availability |
| Messages | Region-local (Cassandra multi-DC) | < 1 second | Eventual consistency |
| Search index | Async (ES cross-cluster) | < 30 seconds | Re-index from source |
| Photos | Async (S3 CRR) | < 15 minutes | N/A (immutable) |
Regional Routing
The global load balancer (Route 53 with latency-based routing) directs users to the nearest region. For write operations that require primary-region consistency (like booking creation), the request is routed to the primary region regardless of the user's location. This adds ~50-100ms latency for distant users but ensures correctness.
23. Cost Estimation
| Component | Specification | Monthly Cost (USD) |
|---|---|---|
| Compute (EKS) | 200+ pods across regions (c5.2xlarge) | $120,000 |
| MySQL (RDS) | 64 shards × r5.4xlarge + read replicas | $200,000 |
| Elasticsearch | 50-node cluster (r5.2xlarge) | $150,000 |
| Redis (ElastiCache) | 100-node cluster (r5.xlarge) | $60,000 |
| Cassandra | 30-node cluster (i3.2xlarge) | $80,000 |
| S3 Storage | 10PB+ photos + backups | $25,000 |
| CloudFront CDN | 500TB+ monthly transfer | $40,000 |
| Kafka (MSK) | 15-broker cluster (kafka.m5.2xlarge) | $35,000 |
| Monitoring & Logging | CloudWatch, Datadog, PagerDuty | $25,000 |
| Payment Processing | Stripe fees on GTV | $200,000+ |
| ML Infrastructure | SageMaker, feature store, training | $50,000 |
| CDN for Videos | Listing video tours | $15,000 |
| Security & Compliance | WAF, Shield, compliance tools | $20,000 |
| Disaster Recovery | Warm standby in secondary regions | $60,000 |
| Total Infrastructure | ~$1,080,000/month |
24. Interview Q&A (10+ Questions)
SELECT FOR UPDATE on the availability calendar rows for the requested date range. This acquires row-level locks that prevent concurrent modifications. The transaction uses ISOLATION LEVEL SERIALIZABLE to ensure the entire booking creation is atomic. Additionally, we implement a reservation token system where a successful lock reserves the dates for 30 minutes while the guest completes payment. If payment fails, the reservation is released. For extreme throughput, we can layer a distributed lock (Redis SETNX on listing_id) before hitting the database to reduce contention.25. Full C# Implementation (300+ Lines)
Below is a comprehensive C# implementation of the core booking engine, including the calendar service, pricing engine, booking state machine, and payment processing integration. This implementation follows clean architecture principles with dependency injection, async/await patterns, and proper error handling.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace AirbnbPlatform.Core.Entities
{
public enum BookingStatus
{
Pending,
Confirmed,
CheckedIn,
Completed,
CancelledByGuest,
CancelledByHost,
Expired,
Refunding,
Refunded
}
public enum CancellationPolicy
{
Flexible,
Moderate,
Strict,
NonRefundable,
LongTerm
}
public enum BlockedReason
{
Available,
HostBlocked,
Booked,
Maintenance,
ExternalSync
}
public class Listing
{
public Guid Id { get; set; }
public Guid HostId { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string PropertyType { get; set; } = string.Empty;
public string RoomType { get; set; } = string.Empty;
public int MaxGuests { get; set; }
public int Bedrooms { get; set; }
public int Beds { get; set; }
public int Bathrooms { get; set; }
public decimal BasePrice { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public decimal CleaningFee { get; set; }
public string Currency { get; set; } = "USD";
public double Latitude { get; set; }
public double Longitude { get; set; }
public string City { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public bool IsActive { get; set; }
public bool InstantBook { get; set; }
public CancellationPolicy CancellationPolicy { get; set; }
public decimal ServiceFeePercent { get; set; } = 0.14m;
public decimal HostPayoutPercent { get; set; } = 0.85m;
public Host Host { get; set; } = null!;
public ICollection<ListingPhoto> Photos { get; set; } = new List<ListingPhoto>();
public ICollection<Review> Reviews { get; set; } = new List<Review>();
}
public class ListingPhoto
{
public Guid Id { get; set; }
public Guid ListingId { get; set; }
public string Url { get; set; } = string.Empty;
public string ThumbnailUrl { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsVerified { get; set; }
public DateTime UploadedAt { get; set; }
}
public class Host
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public bool IsSuperhost { get; set; }
public decimal ResponseRate { get; set; }
public decimal AcceptanceRate { get; set; }
public string PreferredPayoutCurrency { get; set; } = "USD";
public ICollection<Listing> Listings { get; set; } = new List<Listing>();
}
public class Guest
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public bool IsVerified { get; set; }
public int TrustScore { get; set; }
}
public class AvailabilityCalendar
{
public Guid Id { get; set; }
public Guid ListingId { get; set; }
public DateTime Date { get; set; }
public bool IsAvailable { get; set; }
public decimal? OverridePrice { get; set; }
public int MinNights { get; set; } = 1;
public int MaxNights { get; set; } = 365;
public BlockedReason BlockedReason { get; set; }
public Listing Listing { get; set; } = null!;
}
public class Booking
{
public Guid Id { get; set; }
public Guid ListingId { get; set; }
public Guid GuestId { get; set; }
public Guid HostId { get; set; }
public DateTime CheckIn { get; set; }
public DateTime CheckOut { get; set; }
public int Guests { get; set; }
public BookingStatus Status { get; set; }
public decimal TotalPrice { get; set; }
public decimal NightlyTotal { get; set; }
public decimal CleaningFee { get; set; }
public decimal ServiceFee { get; set; }
public decimal OccupancyTax { get; set; }
public decimal HostPayout { get; set; }
public string Currency { get; set; } = "USD";
public CancellationPolicy CancellationPolicy { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ConfirmedAt { get; set; }
public DateTime? CancelledAt { get; set; }
public string? CancellationReason { get; set; }
public Listing Listing { get; set; } = null!;
public Guest Guest { get; set; } = null!;
}
public class Review
{
public Guid Id { get; set; }
public Guid BookingId { get; set; }
public Guid ReviewerId { get; set; }
public Guid ListingId { get; set; }
public int Rating { get; set; }
public string Comment { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsPublished { get; set; }
}
public class PriceQuote
{
public Guid ListingId { get; set; }
public List<NightlyPrice> NightlyPrices { get; set; } = new();
public decimal Subtotal { get; set; }
public decimal CleaningFee { get; set; }
public decimal ServiceFee { get; set; }
public decimal OccupancyTax { get; set; }
public decimal TotalPrice { get; set; }
public string Currency { get; set; } = "USD";
public DateTime CalculatedAt { get; set; }
}
public class NightlyPrice
{
public DateTime Date { get; set; }
public decimal BasePrice { get; set; }
public decimal AdjustedPrice { get; set; }
public List<string> AppliedFactors { get; set; } = new();
}
public class PaymentRecord
{
public Guid Id { get; set; }
public Guid BookingId { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; } = "USD";
public string Status { get; set; } = "Pending";
public string? PSPReference { get; set; }
public DateTime CreatedAt { get; set; }
}
}
namespace AirbnbPlatform.Core.Interfaces
{
using AirbnbPlatform.Core.Entities;
public interface ICalendarService
{
Task<List<AvailabilityCalendar>> GetAvailabilityAsync(
Guid listingId, DateTime start, DateTime end, CancellationToken ct);
Task<bool> ReserveDatesAsync(
Guid listingId, DateTime checkIn, DateTime checkOut, CancellationToken ct);
Task ReleaseDatesAsync(
Guid listingId, DateTime checkIn, DateTime checkOut, CancellationToken ct);
Task ConfirmDatesAsync(
Guid listingId, DateTime checkIn, DateTime checkOut, CancellationToken ct);
}
public interface IPricingEngine
{
Task<PriceQuote> CalculatePriceAsync(
Guid listingId, DateTime checkIn, DateTime checkOut,
int guests, CancellationToken ct);
}
public interface IPaymentService
{
Task<PaymentRecord> AuthorizePaymentAsync(
Guid bookingId, decimal amount, string currency, CancellationToken ct);
Task<PaymentRecord> CapturePaymentAsync(
Guid paymentId, CancellationToken ct);
Task<PaymentRecord> RefundPaymentAsync(
Guid paymentId, decimal amount, string reason, CancellationToken ct);
}
public interface IEventPublisher
{
Task PublishAsync<T>(T eventData, CancellationToken ct);
}
public interface INotificationService
{
Task SendBookingConfirmationAsync(Booking booking, CancellationToken ct);
Task SendCancellationNoticeAsync(Booking booking, string cancelledBy, CancellationToken ct);
}
}
namespace AirbnbPlatform.Core.Services
{
using AirbnbPlatform.Core.Entities;
using AirbnbPlatform.Core.Interfaces;
public class PricingEngine : IPricingEngine
{
private readonly ILogger<PricingEngine> _logger;
public PricingEngine(ILogger<PricingEngine> logger)
{
_logger = logger;
}
public async Task<PriceQuote> CalculatePriceAsync(
Guid listingId, DateTime checkIn, DateTime checkOut,
int guests, CancellationToken ct)
{
// In production, this fetches from repository
// Simplified for demonstration
var listing = await GetListingAsync(listingId, ct);
var nightlyPrices = new List<NightlyPrice>();
var current = checkIn;
while (current < checkOut)
{
var basePrice = listing.BasePrice;
var adjustedPrice = basePrice;
var factors = new List<string>();
// Day of week adjustment
var dowMultiplier = GetDayOfWeekMultiplier(current);
adjustedPrice *= dowMultiplier;
if (dowMultiplier != 1.0m)
factors.Add($"Weekend: x{dowMultiplier}");
// Seasonal adjustment
var seasonalMultiplier = GetSeasonalMultiplier(current);
adjustedPrice *= seasonalMultiplier;
if (seasonalMultiplier != 1.0m)
factors.Add($"Season: x{seasonalMultiplier}");
// Event surge
var eventMultiplier = GetEventMultiplier(current);
adjustedPrice *= eventMultiplier;
if (eventMultiplier != 1.0m)
factors.Add($"Event: x{eventMultiplier}");
// Lead time
var leadDays = (current - DateTime.UtcNow.Date).Days;
var leadMultiplier = GetLeadTimeMultiplier(leadDays);
adjustedPrice *= leadMultiplier;
if (leadMultiplier != 1.0m)
factors.Add($"LeadTime: x{leadMultiplier}");
// Guest count premium
if (guests > 4)
{
var guestMultiplier = 1.0m + (guests - 4) * 0.05m;
adjustedPrice *= guestMultiplier;
factors.Add($"Guests: x{guestMultiplier}");
}
// Min/Max enforcement
if (listing.MinPrice.HasValue)
adjustedPrice = Math.Max(adjustedPrice, listing.MinPrice.Value);
if (listing.MaxPrice.HasValue)
adjustedPrice = Math.Min(adjustedPrice, listing.MaxPrice.Value);
// Round to nearest $5
adjustedPrice = Math.Round(adjustedPrice / 5) * 5;
nightlyPrices.Add(new NightlyPrice
{
Date = current,
BasePrice = basePrice,
AdjustedPrice = adjustedPrice,
AppliedFactors = factors
});
current = current.AddDays(1);
}
var subtotal = nightlyPrices.Sum(p => p.AdjustedPrice);
var cleaningFee = listing.CleaningFee;
var serviceFee = Math.Round(subtotal * listing.ServiceFeePercent, 2);
var occupancyTax = Math.Round(subtotal * 0.08m, 2);
return new PriceQuote
{
ListingId = listingId,
NightlyPrices = nightlyPrices,
Subtotal = subtotal,
CleaningFee = cleaningFee,
ServiceFee = serviceFee,
OccupancyTax = occupancyTax,
TotalPrice = subtotal + cleaningFee + serviceFee + occupancyTax,
Currency = listing.Currency,
CalculatedAt = DateTime.UtcNow
};
}
private decimal GetDayOfWeekMultiplier(DateTime date) => date.DayOfWeek switch
{
DayOfWeek.Friday => 1.15m,
DayOfWeek.Saturday => 1.20m,
DayOfWeek.Sunday => 1.05m,
_ => 1.0m
};
private decimal GetSeasonalMultiplier(DateTime date)
{
var month = date.Month;
return month switch
{
6 or 7 or 8 => 1.35m, // Summer peak
12 or 1 => 1.20m, // Winter holidays
3 or 4 => 1.10m, // Spring break
10 or 11 => 0.95m, // Autumn shoulder
_ => 1.0m // Off-peak
};
}
private decimal GetEventMultiplier(DateTime date)
{
// In production, queries event database for local events
// Simplified mock logic
return 1.0m;
}
private decimal GetLeadTimeMultiplier(int leadDays) => leadDays switch
{
<= 1 => 0.80m, // Last-minute discount
<= 3 => 0.90m,
<= 7 => 0.95m,
<= 30 => 1.0m,
<= 90 => 1.05m,
_ => 1.10m // Far-out premium
};
private Task<Listing> GetListingAsync(Guid id, CancellationToken ct)
{
// Placeholder — in production, fetches from repository/cache
return Task.FromResult(new Listing
{
Id = id,
BasePrice = 150m,
CleaningFee = 35m,
Currency = "USD",
MinPrice = 90m,
MaxPrice = 450m,
ServiceFeePercent = 0.14m,
HostPayoutPercent = 0.85m
});
}
}
public class BookingService
{
private readonly ICalendarService _calendar;
private readonly IPricingEngine _pricing;
private readonly IPaymentService _payment;
private readonly IEventPublisher _events;
private readonly INotificationService _notifications;
private readonly ILogger<BookingService> _logger;
public BookingService(
ICalendarService calendar,
IPricingEngine pricing,
IPaymentService payment,
IEventPublisher events,
INotificationService notifications,
ILogger<BookingService> logger)
{
_calendar = calendar;
_pricing = pricing;
_payment = payment;
_events = events;
_notifications = notifications;
_logger = logger;
}
public async Task<BookingResult> CreateBookingAsync(
CreateBookingRequest request, CancellationToken ct)
{
_logger.LogInformation(
"Creating booking for listing {ListingId}, {CheckIn} to {CheckOut}",
request.ListingId, request.CheckIn, request.CheckOut);
// Step 1: Validate date range
if (request.CheckIn >= request.CheckOut)
return BookingResult.Fail("Check-out must be after check-in");
if ((request.CheckOut - request.CheckIn).TotalDays < 1)
return BookingResult.Fail("Minimum stay is 1 night");
if ((request.CheckOut - request.CheckIn).TotalDays > 365)
return BookingResult.Fail("Maximum stay is 365 nights");
if (request.Guests < 1)
return BookingResult.Fail("At least 1 guest required");
// Step 2: Calculate pricing
var priceQuote = await _pricing.CalculatePriceAsync(
request.ListingId, request.CheckIn, request.CheckOut,
request.Guests, ct);
// Step 3: Reserve dates (atomic operation)
var reserved = await _calendar.ReserveDatesAsync(
request.ListingId, request.CheckIn, request.CheckOut, ct);
if (!reserved)
return BookingResult.Fail(
"Selected dates are no longer available. " +
"Please choose different dates.");
try
{
// Step 4: Process payment (authorization hold)
var booking = new Booking
{
Id = Guid.NewGuid(),
ListingId = request.ListingId,
GuestId = request.GuestId,
HostId = request.HostId,
CheckIn = request.CheckIn,
CheckOut = request.CheckOut,
Guests = request.Guests,
Status = BookingStatus.Pending,
TotalPrice = priceQuote.TotalPrice,
NightlyTotal = priceQuote.Subtotal,
CleaningFee = priceQuote.CleaningFee,
ServiceFee = priceQuote.ServiceFee,
OccupancyTax = priceQuote.OccupancyTax,
HostPayout = Math.Round(
priceQuote.Subtotal * request.HostPayoutPercent, 2),
Currency = priceQuote.Currency,
CancellationPolicy = request.CancellationPolicy,
CreatedAt = DateTime.UtcNow
};
var payment = await _payment.AuthorizePaymentAsync(
booking.Id, booking.TotalPrice, booking.Currency, ct);
if (payment == null)
{
await _calendar.ReleaseDatesAsync(
request.ListingId, request.CheckIn, request.CheckOut, ct);
return BookingResult.Fail("Payment authorization failed");
}
// Step 5: Determine if instant book or request-to-book
if (request.IsInstantBook)
{
booking.Status = BookingStatus.Confirmed;
booking.ConfirmedAt = DateTime.UtcNow;
await _calendar.ConfirmDatesAsync(
request.ListingId, request.CheckIn, request.CheckOut, ct);
await _payment.CapturePaymentAsync(payment.Id, ct);
await _events.PublishAsync(new BookingConfirmedEvent
{
BookingId = booking.Id,
ListingId = booking.ListingId,
GuestId = booking.GuestId,
CheckIn = booking.CheckIn,
CheckOut = booking.CheckOut
}, ct);
await _notifications.SendBookingConfirmationAsync(
booking, ct);
}
else
{
await _events.PublishAsync(new BookingRequestedEvent
{
BookingId = booking.Id,
ListingId = booking.ListingId,
HostId = booking.HostId,
ExpiresAt = DateTime.UtcNow.AddHours(24)
}, ct);
await _notifications.SendBookingRequestAsync(
booking, ct);
}
_logger.LogInformation(
"Booking {BookingId} created successfully with status {Status}",
booking.Id, booking.Status);
return BookingResult.Success(booking);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating booking for listing {ListingId}",
request.ListingId);
await _calendar.ReleaseDatesAsync(
request.ListingId, request.CheckIn, request.CheckOut, ct);
return BookingResult.Fail(
"An error occurred while processing your booking. " +
"Please try again.");
}
}
public async Task<BookingResult> CancelBookingAsync(
Guid bookingId, Guid userId, string reason, CancellationToken ct)
{
var booking = await GetBookingAsync(bookingId, ct);
if (booking == null)
return BookingResult.Fail("Booking not found");
if (booking.Status != BookingStatus.Confirmed)
return BookingResult.Fail(
"Only confirmed bookings can be cancelled");
var isGuest = booking.GuestId == userId;
var isHost = booking.HostId == userId;
if (!isGuest && !isHost)
return BookingResult.Fail("Unauthorized");
// Calculate refund based on cancellation policy
var refundAmount = CalculateRefund(
booking, isGuest, DateTime.UtcNow);
// Update booking status
booking.Status = isGuest
? BookingStatus.CancelledByGuest
: BookingStatus.CancelledByHost;
booking.CancelledAt = DateTime.UtcNow;
booking.CancellationReason = reason;
// Release calendar dates
await _calendar.ReleaseDatesAsync(
booking.ListingId, booking.CheckIn, booking.CheckOut, ct);
// Process refund
if (refundAmount > 0)
{
booking.Status = BookingStatus.Refunding;
await _payment.RefundPaymentAsync(
booking.Id, refundAmount,
$"Cancellation by {(isGuest ? "guest" : "host")}: {reason}",
ct);
}
// Publish event
await _events.PublishAsync(new BookingCancelledEvent
{
BookingId = booking.Id,
CancelledBy = isGuest ? "guest" : "host",
RefundAmount = refundAmount,
Reason = reason
}, ct);
await _notifications.SendCancellationNoticeAsync(
booking, isGuest ? "guest" : "host", ct);
return BookingResult.Success(booking);
}
private decimal CalculateRefund(
Booking booking, bool isGuestCancellation, DateTime cancelTime)
{
var nightsCount = (booking.CheckOut - booking.CheckIn).Days;
var nightsBeforeCheckIn =
(booking.CheckIn - cancelTime).TotalDays;
if (isGuestCancellation)
{
return booking.CancellationPolicy switch
{
CancellationPolicy.Flexible =>
nightsBeforeCheckIn >= 1
? booking.TotalPrice : booking.NightlyTotal * 0.5m,
CancellationPolicy.Moderate =>
nightsBeforeCheckIn >= 5
? booking.TotalPrice : booking.NightlyTotal * 0.5m,
CancellationPolicy.Strict =>
nightsBeforeCheckIn >= 7
? booking.TotalPrice * 0.5m : 0m,
CancellationPolicy.NonRefundable => 0m,
CancellationPolicy.LongTerm =>
nightsBeforeCheckIn >= 30
? booking.TotalPrice
: booking.TotalPrice * 0.5m,
_ => 0m
};
}
else
{
// Host cancellation — always full refund + host penalty
return booking.TotalPrice;
}
}
private Task<Booking?> GetBookingAsync(
Guid id, CancellationToken ct)
{
// Placeholder for repository call
return Task.FromResult<Booking?>(null);
}
}
// Result types
public class BookingResult
{
public bool IsSuccess { get; set; }
public string? ErrorMessage { get; set; }
public Booking? Booking { get; set; }
public static BookingResult Success(Booking booking) => new()
{
IsSuccess = true,
Booking = booking
};
public static BookingResult Fail(string error) => new()
{
IsSuccess = false,
ErrorMessage = error
};
}
public class CreateBookingRequest
{
public Guid ListingId { get; set; }
public Guid GuestId { get; set; }
public Guid HostId { get; set; }
public DateTime CheckIn { get; set; }
public DateTime CheckOut { get; set; }
public int Guests { get; set; }
public bool IsInstantBook { get; set; }
public CancellationPolicy CancellationPolicy { get; set; }
public decimal HostPayoutPercent { get; set; } = 0.85m;
}
// Event types for Kafka messaging
public class BookingConfirmedEvent
{
public Guid BookingId { get; set; }
public Guid ListingId { get; set; }
public Guid GuestId { get; set; }
public DateTime CheckIn { get; set; }
public DateTime CheckOut { get; set; }
}
public class BookingRequestedEvent
{
public Guid BookingId { get; set; }
public Guid ListingId { get; set; }
public Guid HostId { get; set; }
public DateTime ExpiresAt { get; set; }
}
public class BookingCancelledEvent
{
public Guid BookingId { get; set; }
public string CancelledBy { get; set; } = string.Empty;
public decimal RefundAmount { get; set; }
public string Reason { get; set; } = string.Empty;
}
}
This implementation totals over 300 lines and covers the core domain entities, interfaces, the pricing engine with multi-factor calculations, the booking creation flow with double-booking prevention, cancellation with policy-based refund calculation, and event publishing for async workflows.
26. Conclusion
Designing an accommodation platform like Airbnb is a masterclass in distributed systems engineering. The system must balance the conflicting requirements of real-time availability (no double-bookings) with massive read throughput (millions of search queries per second), while operating across 220+ countries with varying payment methods, regulations, and languages.
The key architectural insights from this design are:
- Microservices with clear domain boundaries allow independent scaling of high-traffic services (search, calendar) from lower-traffic ones (reviews, user profiles)
- Event-driven architecture with Kafka enables loose coupling and eventual consistency where appropriate, while synchronous calls are reserved for operations requiring immediate consistency
- Multi-tier caching with Redis and CDN absorbs the majority of read traffic, protecting the database from overload
- Database sharding by listing_id ensures that the most common query patterns (listing detail, calendar, host bookings) hit a single shard
- The pricing engine combines rule-based and ML-based approaches, pre-computing prices to keep API latency low
- Trust and safety is woven into every layer — from identity verification to fraud detection to content moderation
For system design interviews, focus on the booking state machine, calendar availability with atomic reservations, search with geo-filtering and real-time availability overlay, and the trade-offs between consistency and availability. These are the areas where interviewers will probe deepest.