Airbnb
Hosts, listings, bookings, and a calendar that never double-books. The nuance hotel management misses: each property is unique with its own availability, price, and rules.
Key Abstractions
A property a host offers — address, amenities, nightly price, house rules
Per-listing availability. Tracks booked ranges with O(log n) conflict detection.
Guest-side reservation with state machine (requested, confirmed, checked-in, completed, cancelled)
Base price plus dynamic rules — weekend premium, minimum stay, last-minute discount
Two-way: guest reviews host, host reviews guest. Both revealed only after both submit.
Class Diagram
The Key Insight
Hotel management and Airbnb look similar until the calendar model. A hotel books a room type — any of N identical rooms. Airbnb books a specific listing — there's exactly one of it. That makes per-listing availability the atom of the whole design, and search becomes "find listings whose calendars say yes for these dates."
The calendar is an interval tree keyed by checkin date. For any date range request, two neighbor lookups (floor and ceiling) reveal conflicts in O(log n). Half-open intervals [checkin, checkout) keep the edges clean — one guest's checkout morning is the next guest's checkin afternoon, and the math agrees.
Pricing is the part most candidates skip. A nightly rate isn't a number; it's a function of the night (weekend/weekday, peak season, minimum stay, last-minute discount, length-of-stay discount). PricingStrategy pulls that out so the listing stays clean and Airbnb's real pricing experiments live in one class.
Requirements
Functional
- Hosts list properties with address, capacity, amenities, and price
- Guests search by location, dates, guest count
- Instant-book or host-approval flow
- Cancel bookings with status transitions
- Two-way reviews revealed only after both parties submit
Non-Functional
- O(log n) availability check per listing
- No double-booking under concurrent requests
- Price computation is extensible without editing
Listing - Search returns only actually-available listings (not post-filter)
Design Decisions
Why per-listing calendar vs global booking table?
Global table means "show me San Francisco listings available next weekend" scans every booking. Per-listing means each listing's calendar is small (hundreds of bookings max) and conflict-check is O(log n). Search filters the listing set first, then checks each calendar.
Why half-open date intervals?
A guest checking out Sunday morning doesn't conflict with a guest checking in Sunday afternoon. Closed intervals force either "checkout = next-day's checkin - 1" (surprising) or explicit "same day OK" logic (easy to forget). Half-open just works.
Why PricingStrategy and not a nightlyPrice column?
Airbnb's business is dynamic pricing. Weekend premiums, peak seasons, length-of-stay discounts, last-minute deals. Encoding each as a strategy means the listing doesn't know or care; a new pricing experiment is a new strategy class deployed to a test cohort.
Why two-blind reviews?
If Bob writes a bad review first and Alice can see it, she writes a retaliatory bad review. The bad review is then seen as "warranted" by future guests. Hiding both until both submit (or a deadline passes) breaks the retaliation loop — this is policy that shapes the data model.
Why reserve at request time?
Without it, two guests racing to book the same dates both see "available" and both commit. One gets a rejection after they've entered payment details. Reserving at request time (even for host-approval flow) holds the dates; host rejection releases the hold. Similar to the airline two-phase pattern.
Interview Follow-ups
- "How does Superhost status work?" Aggregate per-host metrics (response rate, review score, cancellation rate). A
HostBadgeentity evaluated nightly. Not part of the booking path — purely observed from review and booking data. - "What about cleaning fees and taxes?" Extend
PricingStrategyto return aPriceBreakdown(base, cleaning, taxes, service fee). Invariant: breakdown sums to total. Easier for display and refund math. - "How do you handle concurrent host pricing edits?" Version on
PricingStrategyper listing. Edits are copy-on-write; in-flight bookings keep their quoted price. - "What about multi-currency?"
Moneystores currency. Pricing and payment both parameterize by it. Guest pays in their currency via FX at booking time; host receives in their payout currency. - "How would you prevent discrimination-based host rejection?" Hide guest profile (name, photo) until after host approval, or shift to instant-book. Design choice with legal weight.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3 from dataclasses import dataclass, field
4 from datetime import date, datetime, timedelta
5 from enum import Enum
6 from threading import RLock
7 from sortedcontainers import SortedDict # pip install sortedcontainers
8 import uuid
9
10
11 class BookingStatus(Enum):
12 REQUESTED = "requested"
13 CONFIRMED = "confirmed"
14 CHECKED_IN = "checked_in"
15 COMPLETED = "completed"
16 CANCELLED = "cancelled"
17
18
19 class CancelledBy(Enum):
20 GUEST = "guest"
21 HOST = "host"
22 SYSTEM = "system"
23
24
25 @dataclass(frozen=True)
26 class Money:
27 cents: int
28 currency: str = "USD"
29
30 def __add__(self, other: "Money") -> "Money":
31 if self.currency != other.currency:
32 raise ValueError("currency mismatch")
33 return Money(self.cents + other.cents, self.currency)
34
35 def __mul__(self, n: int | float) -> "Money":
36 return Money(int(self.cents * n), self.currency)
37
38
39 @dataclass(frozen=True)
40 class Address:
41 street: str
42 city: str
43 country: str
44
45
46 @dataclass
47 class User:
48 id: str
49 name: str
50 email: str
51
52
53 class PricingStrategy(ABC):
54 @abstractmethod
55 def price(self, checkin: date, checkout: date) -> Money: ...
56
57
58 class DynamicPricing(PricingStrategy):
59 """Base + weekend premium. Weekends = Fri & Sat nights."""
60
61 def __init__(self, base_price: Money, weekend_multiplier: float = 1.25, minimum_stay: int = 1):
62 if base_price.cents <= 0:
63 raise ValueError("base price must be positive")
64 if minimum_stay < 1:
65 raise ValueError("minimum stay must be >= 1 night")
66 self._base = base_price
67 self._weekend_mult = weekend_multiplier
68 self._minimum_stay = minimum_stay
69
70 def price(self, checkin: date, checkout: date) -> Money:
71 nights = (checkout - checkin).days
72 if nights < self._minimum_stay:
73 raise ValueError(f"Minimum stay is {self._minimum_stay} night(s)")
74 total_cents = 0
75 current = checkin
76 while current < checkout:
77 # Friday = 4, Saturday = 5 in Python's weekday().
78 mult = self._weekend_mult if current.weekday() in (4, 5) else 1.0
79 total_cents += int(self._base.cents * mult)
80 current += timedelta(days=1)
81 return Money(total_cents, self._base.currency)
82
83
84 @dataclass
85 class DateRange:
86 start: date
87 end: date # exclusive — half-open interval [start, end)
88
89 def overlaps(self, other: "DateRange") -> bool:
90 return self.start < other.end and other.start < self.end
91
92
93 class Calendar:
94 """Availability per listing. Sorted map keyed by checkin date."""
95
96 def __init__(self):
97 self._bookings: SortedDict[date, DateRange] = SortedDict()
98 self._booking_index: dict[str, date] = {} # booking_id -> checkin key
99 self._blocked: list[DateRange] = []
100 self._lock = RLock()
101
102 def is_available(self, checkin: date, checkout: date) -> bool:
103 if checkout <= checkin:
104 raise ValueError("checkout must be after checkin")
105 with self._lock:
106 # Blocked ranges: linear scan is fine; hosts rarely block more than a few ranges.
107 for blk in self._blocked:
108 if blk.overlaps(DateRange(checkin, checkout)):
109 return False
110
111 # Prior booking that could still overlap.
112 idx = self._bookings.bisect_right(checkin) - 1
113 if idx >= 0:
114 prior = self._bookings.peekitem(idx)[1]
115 if prior.end > checkin:
116 return False
117 idx_next = self._bookings.bisect_left(checkin)
118 if idx_next < len(self._bookings):
119 nxt = self._bookings.peekitem(idx_next)[1]
120 if nxt.start < checkout:
121 return False
122 return True
123
124 def reserve(self, booking_id: str, checkin: date, checkout: date) -> None:
125 with self._lock:
126 if not self.is_available(checkin, checkout):
127 raise RuntimeError("dates unavailable")
128 self._bookings[checkin] = DateRange(checkin, checkout)
129 self._booking_index[booking_id] = checkin
130
131 def release(self, booking_id: str) -> None:
132 with self._lock:
133 key = self._booking_index.pop(booking_id, None)
134 if key is not None:
135 del self._bookings[key]
136
137 def block(self, start: date, end: date) -> None:
138 self._blocked.append(DateRange(start, end))
139
140
141 class Listing:
142 def __init__(self, host: User, address: Address, max_guests: int,
143 pricing: PricingStrategy, amenities: set[str] | None = None):
144 if max_guests < 1:
145 raise ValueError("max_guests must be >= 1")
146 self.id = str(uuid.uuid4())[:8]
147 self.host = host
148 self.address = address
149 self.max_guests = max_guests
150 self.pricing = pricing
151 self.amenities = amenities or set()
152 self.calendar = Calendar()
153
154
155 @dataclass
156 class Booking:
157 id: str
158 listing_id: str
159 guest_id: str
160 checkin: date
161 checkout: date
162 guest_count: int
163 total_price: Money
164 status: BookingStatus = BookingStatus.REQUESTED
165
166
167 @dataclass
168 class Review:
169 booking_id: str
170 author_id: str
171 rating: int # 1..5
172 text: str
173 submitted_at: datetime = field(default_factory=datetime.utcnow)
174 visible: bool = False # flipped to True when both submit OR the timeout elapses
175
176
177 @dataclass
178 class BookingRequest:
179 listing: Listing
180 guest: User
181 checkin: date
182 checkout: date
183 guest_count: int
184
185
186 class AirbnbService:
187 def __init__(self):
188 self._listings: dict[str, Listing] = {}
189 self._bookings: dict[str, Booking] = {}
190 self._reviews: dict[str, list[Review]] = {} # booking_id -> up to two reviews
191
192 def add_listing(self, listing: Listing) -> None:
193 self._listings[listing.id] = listing
194
195 def search(self, city: str, checkin: date, checkout: date,
196 guests: int) -> list[Listing]:
197 out = []
198 for listing in self._listings.values():
199 if listing.address.city != city: continue
200 if listing.max_guests < guests: continue
201 if listing.calendar.is_available(checkin, checkout):
202 out.append(listing)
203 return out
204
205 def request_booking(self, req: BookingRequest) -> Booking:
206 if req.guest_count < 1 or req.guest_count > req.listing.max_guests:
207 raise ValueError("invalid guest count")
208 price = req.listing.pricing.price(req.checkin, req.checkout)
209 booking_id = str(uuid.uuid4())[:8]
210 # Reserve now — prevents race during "instant book" flow. Hosts can reject later.
211 req.listing.calendar.reserve(booking_id, req.checkin, req.checkout)
212 booking = Booking(
213 id=booking_id,
214 listing_id=req.listing.id,
215 guest_id=req.guest.id,
216 checkin=req.checkin,
217 checkout=req.checkout,
218 guest_count=req.guest_count,
219 total_price=price,
220 status=BookingStatus.REQUESTED,
221 )
222 self._bookings[booking_id] = booking
223 return booking
224
225 def confirm_booking(self, booking_id: str) -> None:
226 booking = self._bookings[booking_id]
227 if booking.status != BookingStatus.REQUESTED:
228 raise RuntimeError(f"cannot confirm a {booking.status.value} booking")
229 booking.status = BookingStatus.CONFIRMED
230
231 def cancel_booking(self, booking_id: str, by: CancelledBy) -> None:
232 booking = self._bookings[booking_id]
233 if booking.status in (BookingStatus.CANCELLED, BookingStatus.COMPLETED):
234 return
235 booking.status = BookingStatus.CANCELLED
236 listing = self._listings[booking.listing_id]
237 listing.calendar.release(booking_id)
238 # In reality the policy module decides refund amount based on `by` and timing.
239
240 def check_in(self, booking_id: str) -> None:
241 booking = self._bookings[booking_id]
242 if booking.status != BookingStatus.CONFIRMED:
243 raise RuntimeError("Can only check in a confirmed booking")
244 booking.status = BookingStatus.CHECKED_IN
245
246 def submit_review(self, booking_id: str, author_id: str, rating: int, text: str) -> None:
247 if not 1 <= rating <= 5:
248 raise ValueError("rating 1..5")
249 booking = self._bookings.get(booking_id)
250 if booking is None or booking.status not in (BookingStatus.CHECKED_IN, BookingStatus.COMPLETED):
251 raise RuntimeError("review only after stay")
252 bucket = self._reviews.setdefault(booking_id, [])
253 if any(r.author_id == author_id for r in bucket):
254 raise RuntimeError("duplicate review")
255 bucket.append(Review(booking_id, author_id, rating, text))
256 # Double-blind: reveal all reviews once both sides have submitted.
257 if len(bucket) == 2:
258 for r in bucket:
259 r.visible = True
260
261 def reviews_for(self, booking_id: str) -> list[Review]:
262 return [r for r in self._reviews.get(booking_id, []) if r.visible]
263
264 def sweep_reviews(self, now: datetime | None = None,
265 timeout: timedelta = timedelta(days=14)) -> int:
266 """Reveal any single-sided review whose submission is older than `timeout`.
267 Prevents a silent counterparty from hiding a review forever."""
268 at = now or datetime.utcnow()
269 revealed = 0
270 for bucket in self._reviews.values():
271 if not bucket or all(r.visible for r in bucket):
272 continue
273 earliest = min(r.submitted_at for r in bucket)
274 if at >= earliest + timeout:
275 for r in bucket:
276 if not r.visible:
277 r.visible = True
278 revealed += 1
279 return revealed
280
281
282 if __name__ == "__main__":
283 svc = AirbnbService()
284 host = User("h1", "Alice", "alice@host.com")
285 guest = User("g1", "Bob", "bob@guest.com")
286
287 listing = Listing(
288 host=host,
289 address=Address("123 Ocean Rd", "San Francisco", "USA"),
290 max_guests=4,
291 pricing=DynamicPricing(Money(15_000), weekend_multiplier=1.5, minimum_stay=2),
292 )
293 svc.add_listing(listing)
294
295 # Thursday 2026-04-16 to Sunday 2026-04-19 → 3 nights (Thu, Fri, Sat).
296 # Fri & Sat are weekend multiplier: 150 + 225 + 225 = $600.
297 req = BookingRequest(
298 listing=listing,
299 guest=guest,
300 checkin=date(2026, 4, 16),
301 checkout=date(2026, 4, 19),
302 guest_count=2,
303 )
304 booking = svc.request_booking(req)
305 print(f"Booking {booking.id}: ${booking.total_price.cents / 100:.2f}")
306
307 # Any concurrent booking attempt on the same dates fails.
308 try:
309 svc.request_booking(BookingRequest(listing, guest, date(2026, 4, 17), date(2026, 4, 19), 1))
310 except RuntimeError as e:
311 print(f"Overlapping booking rejected: {e}")
312
313 # Back-to-back booking (checkin == previous checkout) is fine.
314 adjacent = svc.request_booking(BookingRequest(listing, guest, date(2026, 4, 19), date(2026, 4, 21), 1))
315 print(f"Adjacent booking OK: {adjacent.id}")
316
317 svc.confirm_booking(booking.id)
318 svc.check_in(booking.id)
319 svc.submit_review(booking.id, guest.id, 5, "Lovely stay!")
320 print(f"Reviews visible after 1 submission: {len(svc.reviews_for(booking.id))}")
321 svc.submit_review(booking.id, host.id, 4, "Great guest.")
322 print(f"Reviews visible after 2 submissions: {len(svc.reviews_for(booking.id))}")
323
324 # Single-sided review stays hidden until the timeout sweep catches it.
325 svc.confirm_booking(adjacent.id)
326 svc.check_in(adjacent.id)
327 svc.submit_review(adjacent.id, guest.id, 3, "Fine place.")
328 assert len(svc.reviews_for(adjacent.id)) == 0 # host hasn't reviewed yet
329 revealed = svc.sweep_reviews(now=datetime.utcnow() + timedelta(days=15))
330 print(f"Sweep revealed {revealed} single-sided review(s)")
331 assert len(svc.reviews_for(adjacent.id)) == 1Common Mistakes
- ✗Using a global booking table keyed only by listing_id. Conflict check becomes a range scan over millions of rows.
- ✗Storing prices as a single number on the listing. Weekends, peak season, and last-minute rules live nowhere.
- ✗Letting hosts cancel confirmed bookings freely. Policies and penalties are load-bearing business logic.
- ✗Modeling availability as 'free/booked' only. Real listings have blocked-by-host dates, maintenance windows, minimum stays.
Key Points
- ✓Per-listing calendar with sorted interval tree. Booking check is O(log n), not a scan of all reservations.
- ✓Half-open date intervals [checkin, checkout). One night's checkout equals the next night's checkin without conflict.
- ✓Price is a strategy, not a scalar. Airbnb's real revenue is in dynamic pricing rules.
- ✓Reviews are two-blind — neither party sees the other's review until both submit, preventing retaliation.