Otel ve bilet rezervasyon sistemi
Problem
"Booking.com benzeri bir otel rezervasyon sistemi tasarla. Kullanıcı otel ve tarih aralığı seçer, uygun oda tiplerini görür, rezervasyon yapar ve öder. Aynı odanın aynı geceye iki kez satılmaması gerekir; ama oteller iptal ve gelmeme oranlarını telafi etmek için kontrollü fazla satış (overbooking) da isteyebilir. Ardından aynı tasarımı numaralı koltuklu bir konser bilet sistemine uyarlayalım."
Bu soru düşük trafikli ama yüksek eşzamanlılık riskli bir sistemdir. Mülakatçının dinlemek istediği konular: envanterin "oda tipi x gece" olarak modellenmesi, çok geceli bir rezervasyonun atomik yapılması, eşzamanlılık seçeneklerinin (pessimistic, optimistic, koşullu güncelleme, veritabanı constraint'i) karşılaştırılması, ödeme sürerken odanın süreli tutulması (hold) ve süresinin dolması, idempotency ve overbooking'in bir iş kuralı olarak veri modeline yansıması.
Kitap Bölüm 20 (flash sale) ve e-ticaret sepet ve stok case'iyle aynı aileden bir problemdir; fark, envanterin zamana bağlı olmasıdır: bir oda, bir gece için satılır ve o gece geçince envanter de yok olur.
Netleştirme soruları ve varsayımlar
| Soru | Neden önemli | Varsayım |
|---|---|---|
| Kullanıcı belirli bir odayı mı, oda tipini mi seçiyor? | Envanter modeli | Oda tipi (Standart, Deluxe); oda numarası check-in'de atanır |
| Ölçek? | Kapasite | 5.000 otel, toplam 1 milyon oda |
| Rezervasyon en fazla kaç gün önceden? | Envanter satır sayısı | 2 yıl |
| Overbooking? | Kısıt modeli | Otel bazında ayarlanabilir, ör. oda tipinin %10 fazlası |
| Ödeme ne zaman? | Hold süresi | Rezervasyon anında; ödeme sürerken oda 10 dakika tutulur |
| İptal? | Envanter iadesi | Politika bazlı ücretsiz/ücretli iptal |
| Arama sonuçları ne kadar güncel olmalı? | Okuma yolu | Birkaç saniye bayatlık kabul; rezervasyon anında kesin kontrol |
| Otel başka kanallardan da satış yapıyor mu? | Envanter senkronizasyonu | Evet, kanal yöneticisi entegrasyonu var (sonda tartışılacak) |
Kapasite tahmini
Rezervasyon:
1M oda x %70 doluluk / ortalama 3 gece konaklama ≈ 233.000 rezervasyon/gün
233.000 / 86.400 ≈ 2,7 /s ortalama, tepe (x10) ≈ 27 /s
Arama ve detay görüntüleme (bakma/alma oranı ~100):
233.000 x 100 ≈ 23M /gün ≈ 270 /s, tepe ≈ 2.700 /s
Envanter satırı (oda tipi x gece):
5.000 otel x 20 oda tipi x 730 gün = 73M satır x ~100 B ≈ 7,3 GB
Rezervasyon kaydı:
233.000 x ~1 KB ≈ 233 MB/gün ≈ 85 GB/yıl
Teşhis: Yazma trafiği çok küçük (tepede saniyede onlar). Bu sistem için Redis, Kafka ya da sharding ilk gün şart değildir; tek bir PostgreSQL kümesi (primary + replikalar) rahatça yeter. Asıl risk, aynı otelin aynı gecesine aynı anda gelen rezervasyonlardır: kampanya, tatil sezonu ya da bir etkinlik nedeniyle belirli bir otelin belirli gecesi sıcaklaşır. Problem ölçek değil, doğruluk ve eşzamanlılıktır. Mülakatta bunu açıkça söylemek, gereksiz bir mimari kurmaktan çok daha güçlü bir sinyaldir.
API
GET /api/v1/hotels/search?city=antalya&checkIn=2026-07-10&checkOut=2026-07-13&guests=2
GET /api/v1/hotels/{hotelId}/availability?checkIn=...&checkOut=...
POST /api/v1/reservations
Idempotency-Key: res_7f3c... # rezervasyon sayfası açılırken üretilir
{ "hotelId": 12, "roomTypeId": 3, "checkIn": "2026-07-10", "checkOut": "2026-07-13",
"rooms": 1, "guests": 2, "ratePlanId": "nonrefundable" }
-> 201 { "reservationId": "res_7f3c...", "status": "HOLD", "holdExpiresAt": "...", "totalPrice": "14250.00" }
-> 409 { "code": "NOT_AVAILABLE", "nights": ["2026-07-12"] }
POST /api/v1/reservations/{id}/payment # Idempotency-Key
-> 200 { "status": "CONFIRMED" }
-> 410 { "code": "HOLD_EXPIRED" }
POST /api/v1/reservations/{id}/cancel
GET /api/v1/reservations/{id}
- Idempotency key rezervasyon sayfası açılırken üretilir ve rezervasyon id'si olarak da kullanılır. Kullanıcı "Rezervasyon yap" düğmesine iki kez basarsa ya da ağ hatası sonrası istemci tekrar gönderirse aynı rezervasyon döner, ikinci oda ayrılmaz.
- Rezervasyon iki adımlıdır: önce hold (oda süreli tutulur), sonra ödeme ve onay. Ödeme sürerken (3D Secure dakikalar sürebilir) başkası aynı son odayı alamaz.
Veri modeli
CREATE TABLE room_type_inventory (
hotel_id BIGINT NOT NULL,
room_type_id BIGINT NOT NULL,
stay_date DATE NOT NULL,
total_rooms INT NOT NULL CHECK (total_rooms >= 0),
max_sellable INT NOT NULL, -- overbooking dahil satılabilir üst sınır
reserved INT NOT NULL DEFAULT 0 CHECK (reserved >= 0),
PRIMARY KEY (hotel_id, room_type_id, stay_date),
CONSTRAINT inventory_not_oversold CHECK (reserved <= max_sellable),
CONSTRAINT inventory_overbook_bound CHECK (max_sellable >= total_rooms)
);
CREATE TABLE reservations (
id VARCHAR(64) PRIMARY KEY, -- istemcinin ürettiği idempotency key
user_id BIGINT NOT NULL,
hotel_id BIGINT NOT NULL,
room_type_id BIGINT NOT NULL,
check_in DATE NOT NULL,
check_out DATE NOT NULL,
rooms INT NOT NULL CHECK (rooms > 0),
status VARCHAR(16) NOT NULL, -- HOLD, CONFIRMED, CANCELLED, EXPIRED
hold_expires_at TIMESTAMPTZ,
total_price NUMERIC(19,4) NOT NULL,
request_hash CHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT reservations_dates CHECK (check_out > check_in)
);
CREATE INDEX reservations_hold_expiry_idx ON reservations (hold_expires_at) WHERE status = 'HOLD';
- Envanter oda tipi x gece satırıdır. 3 gecelik bir rezervasyon üç satırı etkiler. Kontrol "her gece için" yapılır; ortalama müsaitlik değil.
max_sellable, overbooking politikasını veriye çevirir: 100 odalı bir tip için %10 overbookingmax_sellable = 110demektir. Politika değişince sadece bu kolon güncellenir; kod değişmez.- Fiyatlar ayrı bir
rates(hotel_id, room_type_id, rate_plan_id, stay_date, price)tablosundadır; rezervasyon, hold anındaki toplam fiyatın kopyasını saklar. - Veri otel bazında bölünebilir (
hotel_idshard anahtarı): bir rezervasyonun dokunduğu tüm satırlar aynı oteldedir, transaction'lar tek shard içinde kalır.
Yüksek seviye tasarım
flowchart LR
U[Kullanıcı] --> GW[API gateway]
GW --> SS[Arama servisi]
SS --> ES[(Arama indeksi ve müsaitlik özeti)]
GW --> RS[Rezervasyon servisi]
RS --> DB[(PostgreSQL envanter ve rezervasyon)]
RS --> PAY[Ödeme servisi]
DB -->|outbox ve CDC| K[(Kafka)]
K --> ES
K --> NOTIF[Bildirim servisi]
K --> CM[Kanal yöneticisi entegrasyonu]
HOTEL[Otel yönetim paneli] --> HS[Otel ve fiyat servisi]
HS --> DB
SW[Hold süpürücüsü] --> DB- Arama okuma ağırlıklıdır ve bayatlığa toleranslıdır: müsaitlik özeti envanter değişikliklerinden asenkron güncellenen bir indeksten okunur.
- Rezervasyon servisi envanterin tek yazıcısıdır; hold, onay, iptal ve süre dolumu onun API'leri ve job'larıdır.
- Outbox ile rezervasyon olayları bildirim, arama indeksi ve otelin diğer satış kanalları için yayınlanır.
Derinlemesine
Eşzamanlılık: dört seçenek
Aynı otelin aynı oda tipinde 12 Temmuz için 1 oda kaldı ve iki kullanıcı aynı anda rezervasyon yapıyor.
| Seçenek | Nasıl | Artı | Eksi |
|---|---|---|---|
| Pessimistic lock | SELECT ... FOR UPDATE ile gecelerin satırlarını kilitle, kontrol et, güncelle | Anlaşılır; karmaşık kurallar uygulamada yazılabilir | Kilit süresi uzarsa bekleme; çok satırda kilit sırası gerekir |
| Optimistic lock | version ile oku, yazarken versiyonu kontrol et | Kilit tutulmaz | Sıcak satırda çakışma ve retry fırtınası (Kitap Bölüm 22'deki ölçüm) |
| Koşullu güncelleme | UPDATE ... SET reserved = reserved + n WHERE reserved + n <= max_sellable | Kontrol ve değişiklik tek ifadede; kilit sadece commit'e kadar | Kural tek SQL koşuluna sığmalı |
| Veritabanı constraint'i | CHECK (reserved <= max_sellable) | Uygulama hatasına karşı son savunma | Tek başına lost update'i önlemez |
Seçim: koşullu güncelleme + constraint. Yük küçük olduğu için performans farkı belirleyici değildir; belirleyici olan basitlik ve doğruluktur.
Çok geceli rezervasyonu atomik yapmak
Üç gecelik rezervasyon ya üç geceyi birden alır ya hiçbirini. Bunu tek bir UPDATE ile yapmanın bir püf noktası vardır: koşulu tutan satırlar güncellenir, tutmayanlar güncellenmez; etkilenen satır sayısı gece sayısına eşit değilse transaction geri alınır.
BEGIN;
INSERT INTO reservations (id, user_id, hotel_id, room_type_id, check_in, check_out,
rooms, status, hold_expires_at, total_price, request_hash)
VALUES (:id, :userId, :hotelId, :roomTypeId, :checkIn, :checkOut,
:rooms, 'HOLD', now() + interval '10 minutes', :price, :hash);
-- aynı id zaten varsa unique violation: ROLLBACK ve mevcut rezervasyonu dön
UPDATE room_type_inventory
SET reserved = reserved + :rooms
WHERE hotel_id = :hotelId AND room_type_id = :roomTypeId
AND stay_date >= :checkIn AND stay_date < :checkOut
AND reserved + :rooms <= max_sellable;
-- uygulama: etkilenen satır sayısı (checkOut - checkIn) gün değilse ROLLBACK ve 409
COMMIT;
Neden doğru: UPDATE her satırı kilitler; aynı satırları isteyen ikinci transaction bekler ve READ COMMITTED'da birinci commit olunca satırların güncel halinde koşulu yeniden değerlendirir (Kitap Bölüm 10 ve 22). Son oda için ikinci transaction 12 Temmuz satırında koşulu tutturamaz, etkilenen satır sayısı 3 değil 2 olur ve geri alınır.
İki ince nokta:
- Kilit sırası: Bir rezervasyon 10-13 Temmuz'u, diğeri 12-15 Temmuz'u alıyorsa kesişen satırları farklı sırada kilitlerlerse deadlock oluşabilir. Tek bir
UPDATEsatırları planın tarama sırasına göre kilitler; deterministik sıra garantisi istiyorsan önceSELECT ... WHERE ... ORDER BY stay_date FOR NO KEY UPDATEile sıralı kilitleyip sonra güncellemek daha açıktır. - Idempotency kaydı aynı transaction'da:
reservations.idistemcinin anahtarıdır; aynı anahtarla ikinci istek primary key çakışmasıyla düşer ve uygulama mevcut rezervasyonu döner (gövde farklıysarequest_hashile422).INSERT'ünUPDATE'ten önce gelmesi bu yüzdendir: tersi olsaydı son oda ilk istekle dolduğunda tekrar gelen istekINSERT'e varmadan 0 satırla409alırdı. Anahtarın ayrı bir sistemde (ör. Redis) tutulması, "anahtar yazıldı ama oda ayrılmadı" gibi ayrışmalara yol açar.
Hold ve süre dolumu
Kullanıcı ödeme sayfasındayken oda HOLD durumunda 10 dakika tutulur:
HOLD ──ödeme başarılı──────> CONFIRMED
HOLD ──süre doldu──────────> EXPIRED (envanter iade)
HOLD ──kullanıcı vazgeçti──> CANCELLED (envanter iade)
CONFIRMED ──iptal──────────> CANCELLED (politikaya göre iade + envanter iade)
- Geçişler koşulludur:
UPDATE reservations SET status = 'CONFIRMED' WHERE id = :id AND status = 'HOLD' AND hold_expires_at > now(). Etkilenen satır 0 ise hold süresi dolmuştur. - Süpürücü: Birkaç saniyede bir süresi dolmuş hold'ları
FOR UPDATE SKIP LOCKEDile alır (PostgreSQL dokümantasyonu bu seçeneğin genel amaçlı sorgulara uygun olmadığını ama kuyruk benzeri tablolarda birden fazla tüketicinin çekişmesini önlemek için kullanılabileceğini söyler),EXPIREDyapar ve aynı transaction'da envanter satırlarındanreserved'i düşer. - Ödeme ile süre dolumu yarışı: Ödeme provizyonu alınır, sonra hold koşullu olarak onaylanır; onay başarılıysa tahsilat yapılır, başarısızsa provizyon iptal edilir. Böylece "para alındı ama oda başkasına gitti" durumu oluşmaz (aynı sıra Kitap Bölüm 20'de).
- Hold süresi bir trade-off'tur: Çok kısa olursa 3D Secure'u yavaş tamamlayan kullanıcı odayı kaybeder; çok uzun olursa terk edilen ödemeler sıcak tarihlerde envanteri gereksiz tutar. Ödeme akışının p99 süresine göre seçilir.
Overbooking
Oteller, iptal ve gelmeme (no-show) oranlarını telafi etmek için oda sayısından biraz fazlasını satar. Bu bir iş kuralıdır ve veri modelinde max_sellable ile ifade edilir:
- Koşul
reserved + n <= max_sellable'dır,total_roomsdeğil. %10 overbooking için 100 odalı tiptemax_sellable = 110. - Oran otel ve oda tipi bazında, hatta tarih bazında ayarlanır: sezon dışında 0, yüksek iptal oranlı dönemde daha yüksek.
- Bedeli: gelen misafir sayısı oda sayısını aşarsa otel misafiri başka bir otele yerleştirmek zorunda kalır. Bu yüzden overbooking kararı geçmiş iptal oranları ve gelmeme oranlarıyla verilir ve izlenir.
- Mülakatta önemli olan, overbooking'i "kodda bir istisna" değil, constraint'i değiştiren bir parametre olarak modellemektir.
Belirli oda ataması: aralık çakışmasını veritabanına bırakmak
Kullanıcı belirli bir odayı seçiyorsa (ya da check-in'de oda atanırken) aynı odanın çakışan tarihlerde iki rezervasyona atanmaması gerekir. PostgreSQL'de bu, aralık tipleri ve exclusion constraint ile veritabanı seviyesinde garanti edilebilir:
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE room_assignments (
room_id BIGINT NOT NULL,
reservation_id VARCHAR(64) NOT NULL,
stay DATERANGE NOT NULL, -- [check_in, check_out)
EXCLUDE USING gist (room_id WITH =, stay WITH &&)
);
Aynı oda için çakışan (&&) bir aralık eklenmeye çalışılırsa veritabanı reddeder. btree_gist eklentisi, düz bir kolondaki eşitliği (room_id WITH =) aralık çakışmasıyla aynı constraint'te birleştirmeyi sağlar. Yarı açık aralık ([giriş, çıkış)) sayesinde bir misafirin çıktığı gün diğerinin girmesi çakışma sayılmaz.
Arama ve müsaitlik
Arama trafiği rezervasyonun yüz katıdır ve birkaç saniyelik bayatlık kabul edilir. Envanter değişiklikleri outbox/CDC ile bir arama indeksine taşınır: otel, oda tipi ve gece bazında "müsait mi" bilgisi. Arama sonuçları buradan gelir. Kullanıcı bir oteli seçip detay sayfasına geçtiğinde müsaitlik replikadan okunabilir; rezervasyon anında karar primary'de koşullu güncellemeyle verilir. Arada müsaitlik değişmişse kullanıcıya hangi gecenin dolduğu söylenir.
Kanal yöneticisi ve dış envanter
Oteller aynı odaları birden fazla sitede ve kendi resepsiyonlarında satar. Bir kanal yöneticisi envanteri kanallar arasında senkronize eder; senkronizasyon asenkron olduğu için iki kanalda aynı anda son oda satılabilir. Bu dağıtık bir problemdir ve tam çözümü yoktur: satışlar kanal yöneticisine hızlıca bildirilir, kanallara stokun bir kısmı tahsis edilir (allotment) ve ortaya çıkan fazla satış overbooking gibi operasyonel olarak çözülür. Mülakatta bu konuyu açmak, gerçek dünyadaki envanterin tek bir veritabanında yaşamadığını bildiğini gösterir.
Bilet sistemine uyarlama: numaralı koltuklar
Konser bilet sisteminde envanter "oda tipi x gece" sayacı değil, tek tek koltuklardır ve talep çoğu zaman stoğu çok aşar.
CREATE TABLE seats (
event_id BIGINT NOT NULL,
seat_id VARCHAR(16) NOT NULL, -- "A-12"
status VARCHAR(16) NOT NULL, -- AVAILABLE, HELD, SOLD
hold_owner BIGINT,
hold_expires_at TIMESTAMPTZ,
PRIMARY KEY (event_id, seat_id)
);
-- Koltuğu tutmak: süresi dolmuş hold'u da müsait sayan koşullu güncelleme
UPDATE seats
SET status = 'HELD', hold_owner = :userId, hold_expires_at = now() + interval '8 minutes'
WHERE event_id = :eventId AND seat_id = ANY(:seatIds)
AND (status = 'AVAILABLE' OR (status = 'HELD' AND hold_expires_at < now()));
-- etkilenen satır sayısı istenen koltuk sayısına eşit değilse ROLLBACK
Bu desenin güzel yanı tembel süre dolumudur: süresi dolmuş bir hold, süpürücünün çalışmasını beklemeden bir sonraki istekte müsait sayılır. Doğruluk süpürücüye bağlı değildir; süpürücü sadece koltuk haritasının güncel görünmesi içindir.
Bilet satışında asıl problem trafiktir: popüler bir konserde satış açıldığı an yüz binlerce kişi aynı birkaç bin koltuğa yüklenir. Burada otel tasarımı yetmez; Kitap Bölüm 20'deki katmanlar gelir: CDN, bot koruması, kullanıcıyı sistemin kaldırabileceği hızda içeri alan sanal bekleme odası ve kişi başı bilet limiti. Koltuk haritası ise kısa TTL ile cache'lenen ve hold'lar değiştikçe güncellenen bir görünümdür; birkaç saniyelik bayatlık, "bu koltuk az önce tutuldu" mesajıyla telafi edilir.
Darboğazlar, hatalar ve trade-off'lar
| Senaryo | Etki | Önlem |
|---|---|---|
| Popüler otel ve gece (sıcak satır) | Kilit beklemesi | Kısa transaction, koşullu güncelleme; gerekirse bekleme odası |
| Çok geceli rezervasyonlarda deadlock | Transaction'lar düşer | Satırları tarih sırasıyla kilitlemek |
| Çift tıklama, ağ hatası sonrası tekrar | Çift rezervasyon | İstemci kaynaklı rezervasyon id'si, aynı transaction'da |
| Hold süpürücüsü durdu | Süresi dolmuş hold'lar envanteri tutar | Süpürücü alarmı; koltuklarda tembel süre dolumu |
| Ödeme hold süresinden sonra geldi | Para alındı, oda yok riski | Provizyon, koşullu onay, başarısızsa provizyon iptali |
| Kanallar arası çift satış | Fiili overbooking | Hızlı senkronizasyon, allotment, operasyonel süreç |
| Arama indeksi geride | Dolu otel müsait görünür | Rezervasyon anında kesin kontrol, açık hata mesajı |
Trade-off'lar:
- Basit mimari: Düşük yazma hacmi için tek PostgreSQL kümesi; Redis ve Kafka sadece gerektiği yerde (outbox ile olay yayını).
- Hold süresi: Kullanıcının ödeme rahatlığı ile envanterin boşa tutulması arasında.
- Overbooking: Gelir ve doluluk karşılığında misafir yerleştirme maliyeti riski.
- Arama bayatlığı: Ucuz okuma karşılığında rezervasyon anında "doldu" sürprizi.
Üç seviyede cevap
Soru: "Aynı oda tipinin son odasına iki kullanıcı aynı anda rezervasyon yaparsa ne olur?"
seviye: Orta etiketler: [concurrency, locking] kaynak: Tahmini
Müsaitliği kontrol ederim, müsaitse rezervasyonu oluştururum. Metodu synchronized yaparım ki aynı anda iki istek işlenmesin.
synchronized birden fazla pod'da işe yaramaz ve transaction commit'inden önce kilidi bırakır. Kontrolü ve güncellemeyi tek bir koşullu UPDATE ile yaparım: reserved + 1 <= max_sellable koşuluyla. İkinci transaction satır kilidini bekler, sonra koşulu güncel değerle değerlendirir ve 0 satır döner; ona müsait değil derim. Arkada bir CHECK constraint'i de koyarım.
Envanteri oda tipi x gece olarak modellediğim için soru aslında "her gece için" soruluyor. Tek bir UPDATE ile tarih aralığındaki bütün satırları koşullu artırırım; etkilenen satır sayısı gece sayısına eşit değilse transaction'ı geri alır ve hangi gecenin dolu olduğunu dönerim. Satır kilidi ve READ COMMITTED'daki yeniden değerlendirme, ikinci kullanıcının güncel değeri görmesini sağlar. Kesişen tarih aralıklı rezervasyonlar arasında deadlock olmasın diye satırları tarih sırasıyla kilitlerim. Rezervasyon kaydı aynı transaction'da, istemcinin ürettiği rezervasyon id'siyle yazılır; çift tıklama aynı rezervasyonu döner. CHECK (reserved <= max_sellable) son savunma ve overbooking'i de aynı kolonla ifade ediyor. Yük tepede saniyede onlar olduğu için Redis ya da dağıtık kilit gerekmiyor; sıcak bir tarih için bile tek satır kilidi bu hızı rahatça kaldırır.
Junior JVM içi kilitle dağıtık bir problemi çözmeye çalışıyor (Kitap Bölüm 22'de neden yanlış olduğu gösterildi). Mid doğru mekanizmayı biliyor. Senior çok geceli atomikliği tek bir ifadeyle ve etkilenen satır sayısı kontrolüyle çözüyor, deadlock'u, idempotency'yi ve overbooking'i aynı modele bağlıyor ve kapasite hesabına dayanarak gereksiz altyapıyı reddediyor.
Takip sorusu: Kullanıcı aynı rezervasyonda iki farklı oda tipinden birer oda istiyor. Değişen ne olur?
Soru: "Kullanıcı ödeme yaparken odayı nasıl tutarsın? Ödemeyi tamamlamazsa ne olur?"
seviye: Orta etiketler: [reservation, ttl] kaynak: Tahmini
Ödeme başarılı olunca rezervasyonu oluştururum; o zamana kadar oda boşta kalır.
Rezervasyonu HOLD durumunda, 10 dakikalık bir bitiş zamanıyla oluşturur ve envanteri hemen ayırırım. Ödeme gelirse CONFIRMED yaparım. Bir job süresi dolan hold'ları bulup EXPIRED yapar ve envanteri geri verir.
Hold'u envanteri hemen ayıran ve bitiş zamanı taşıyan bir durum olarak modellerim. Durum geçişleri koşullu: onay status = 'HOLD' AND hold_expires_at > now() koşuluyla yapılır. Süpürücü süresi dolanları SKIP LOCKED ile alır, böylece birden fazla instance çakışmadan çalışır, ve envanter iadesini aynı transaction'da yapar. Ödeme ile süre dolumu yarışını ödeme sırasıyla kapatırım: önce provizyon, sonra koşullu onay, onay başarılıysa tahsilat, değilse provizyon iptali; böylece para alınıp oda verilmemesi olmaz. Hold süresini ödeme akışının p99 süresine göre seçerim; 3D Secure dakikalar sürebilir. Koltuklu bilet senaryosunda süre dolumunu tembel yaparım: süresi dolmuş hold bir sonraki istekte müsait sayılır, doğruluk süpürücüye bağlı kalmaz.
Junior ödeme süresince yarış penceresi bırakıyor. Mid hold ve süpürücüyü biliyor. Senior geçişleri koşullu yapıyor, süpürücüyü çoklu instance'a uygun kuruyor, ödeme ile süre dolumu yarışını kapatıyor ve tembel süre dolumuyla doğruluğu süpürücüden bağımsız hale getiriyor.
Takip sorusu: Süpürücü 20 dakika çalışmadı ve tatil sezonunun en popüler gecesinde 300 oda hold'da kaldı. Bunu nasıl fark eder, etkisini nasıl sınırlarsın?
Soru: "Overbooking'i nasıl modellersin?"
seviye: İleri etiketler: [overbooking, inventory] kaynak: Tahmini
Overbooking yapılmamalı, çünkü misafir geldiğinde oda olmayabilir. Kontrolü toplam oda sayısıyla yaparım.
Overbooking otelin iş kararı; iptal ve gelmeme oranını telafi eder. Envanter satırında toplam oda sayısının yanında bir satılabilir üst sınır tutarım, örneğin %10 fazlası. Kontrolü bu üst sınırla yaparım. Oran otel ve oda tipi bazında ayarlanabilir.
Overbooking'i kodda bir istisna olarak değil, envanter satırında bir parametre olarak modellerim: max_sellable. Koşullu güncelleme ve constraint bu kolonu kullanır, total_rooms sadece fiziksel kapasiteyi gösterir; oran değişince kod değil veri değişir. Oranı otel, oda tipi ve tarih bazında tutarım, çünkü iptal davranışı sezona ve fiyat planına göre değişir; iade edilemez fiyat planıyla yapılan rezervasyonlar daha az iptal edilir. Oranın etkisini ölçerim: gece başına gerçekleşen doluluk, gelmeme oranı ve misafiri başka otele yerleştirme sayısı. Bir de dış envanter boyutu var: kanallar arası senkronizasyon gecikmesi fiili bir overbooking yaratır, bunu da aynı operasyonel süreçle ele alırım.
Junior iş gereksinimini teknik bir hata olarak görüyor. Mid doğru modeli kuruyor. Senior modeli veri odaklı yapıyor, oranın neye göre değiştiğini (sezon, fiyat planı) biliyor, kararın sonucunu ölçülebilir metriklere bağlıyor ve kasıtsız overbooking kaynağını (kanallar arası senkronizasyon) da aynı çerçeveye koyuyor.
Takip sorusu: Otel overbooking oranını %10'dan %0'a düşürdü ama o gece için zaten 108 rezervasyon var. Ne olur?
Mülakatçı için
- Kapasite hesabıyla yazma yükünün küçük olduğunu, asıl riskin eşzamanlılık olduğunu tespit ediyor ve gereksiz altyapı kurmuyor.
- Envanteri oda tipi x gece olarak modelliyor.
- En az üç eşzamanlılık seçeneğini karşılaştırıp gerekçeli seçim yapıyor.
- Çok geceli rezervasyonu atomik yapıyor (tek transaction, etkilenen satır sayısı kontrolü veya eşdeğeri).
- Kilit sırasını ve deadlock riskini ele alıyor.
- Idempotency anahtarını rezervasyonla aynı transaction'da saklıyor.
- Hold'u süreli bir durum olarak modelliyor; süre dolumunu ve ödeme yarışını ele alıyor.
- Overbooking'i parametre olarak veri modeline yansıtıyor.
- Arama yolunu rezervasyon yolundan ayırıp bayatlığı kabul ediyor, kararı kaynakta veriyor.
- Bilet varyantında koltuk bazlı modele ve yüksek talep katmanlarına (bekleme odası) geçebiliyor.
Takip soruları:
- Bir otelin fiyatı rezervasyon sırasında değişti. Kullanıcı hangi fiyatı öder?
- Çok bölgeli bir kurulumda rezervasyon yazmaları nereye gider?
- Kullanıcı 10-13 Temmuz rezervasyonunu 11-14 Temmuz'a değiştirmek istiyor. Atomik nasıl yaparsın?
- Belirli oda atamasında exclusion constraint yerine uygulama kontrolü kullansaydık hangi yarış oluşurdu?
Seviye beklentisi: Mid seviyede koşullu güncelleme, hold ve süpürücü, idempotency beklenir. Senior'dan çok geceli atomiklik, kilit sırası, ödeme yarışı, overbooking modellemesi ve "bu ölçek için basit kal" kararının sayıyla savunulması beklenir.
Hızlı tekrar
S: Otel envanteri hangi granülerlikte tutulur?
C: Oda tipi x gece: (hotel_id, room_type_id, stay_date) satırı, reserved ve max_sellable ile.
S: Üç gecelik rezervasyon tek UPDATE ile nasıl atomik yapılır?
C: Tarih aralığındaki satırlar reserved + n <= max_sellable koşuluyla güncellenir; etkilenen satır sayısı gece sayısına eşit değilse transaction geri alınır.
S: Overbooking veri modeline nasıl yansır?
C: max_sellable kolonu (ör. 100 oda için 110); koşul ve constraint bu kolonu kullanır, oran değişince kod değişmez.
S: Hold onayı neden koşullu güncellemedir?
C: status = 'HOLD' AND hold_expires_at > now() koşulu, süresi dolmuş ya da iade edilmiş bir hold'un onaylanmasını engeller; süpürücüyle yarışta sadece biri kazanır.
S: Rezervasyon için idempotency key ne zaman üretilir?
C: Rezervasyon sayfası açılırken; rezervasyon id'si olarak kullanılır ve rezervasyonla aynı transaction'da yazılır.
S: Aynı odanın çakışan tarihlere atanmasını PostgreSQL'de nasıl engellersin?
C: DATERANGE + EXCLUDE USING gist (room_id WITH =, stay WITH &&); eşitlik için btree_gist eklentisi.
S: Koltuk hold'unda tembel süre dolumu nedir?
C: Koşullu güncelleme süresi dolmuş HELD koltuğu da müsait sayar; doğruluk süpürücüye bağlı kalmaz.
Kaynaklar
- Alex Xu, Sahn Lam, System Design Interview: An Insider's Guide (Cilt 2): "Hotel Reservation System" bölümü.
- Martin Kleppmann, Designing Data-Intensive Applications: Bölüm 7 (lost update, write skew ve materializing conflicts).
- PostgreSQL: Range Types: Exclusion constraint ile çakışan aralıkları engelleme ve
btree_gist. - PostgreSQL: SELECT, The Locking Clause:
SKIP LOCKEDve kuyruk benzeri tablolar. - PostgreSQL: Explicit Locking: Satır kilitleri ve deadlock.