
Randevu Prototipi
Bir berber işletmesi için yazdığım randevu sistemi prototipi. Müşteri projesi değil — randevu çakışmasının nerede engellenmesi gerektiğini denemek için kurdum ve kuralı uygulama katmanından çıkarıp Postgres exclusion constraint'ine taşıdım.
Randevu sistemleri tek bir noktada bozulur: iki kişi aynı personelin aynı saatini aynı anda alır. Bu, senaryo yazarak keşfedilecek bir hata değil; iki isteğin araya girmesiyle oluşuyor ve tek kullanıcıyla test edildiğinde asla ortaya çıkmıyor. Yaygın çözüm "önce oku, boşsa yaz" — ve bu çözüm yanlış, çünkü okuma ile yazma arasında kalan sürede başka bir istek aynı yeri alabiliyor. İkinci mesele zaman: bir randevu sistemi yaz saati geçişini, kullanıcının cihaz saat dilimini ve sunucunun saat dilimini aynı anda doğru ele almak zorunda. Bu proje bir müşteri işi değil; iki sorunun da nereye konulması gerektiğini denemek için yazdığım bir prototip.
İki taraf. Müşteri: telefondan giriyor, hizmeti seçiyor, uygun saati görüyor ve üyelik açmadan randevu bırakıyor; sonradan iptal etmek isterse elinde bir bağlantı olması gerekiyor. İşletme: personel, hizmet süresi, çalışma saatleri, mola ve kapalı gün tanımlarını panelden yönetiyor; günü takvim görünümünde takip ediyor ve elle randevu ekleyebiliyor.
Tek geliştirici, prototipin tamamı. Veri modeli ve kısıtlar, uygunluk hesabı, rezervasyon ve iptal akışları, yönetim paneli, bildirim katmanı.
Next.js 16 App Router, Neon Postgres ve Drizzle. Yazma yolu Server Action → Zod → transaction. Sistemin merkezindeki karar veri modelinde: `appointments` tablosunda personel kimliği ve randevu aralığı üzerinde bir exclusion constraint var; kısıt yalnız onaylı randevular için çalışıyor, böylece iptal edilmiş bir kayıt yeri tutmuyor. Uygunluk hesabı ise veritabanından tamamen ayrı: çalışma saati, mola, personel randevuları, işletme blokları, slot adımı, minimum ihbar süresi ve rezervasyon penceresi girdi olarak veriliyor, çıktı uygun başlangıç saatlerinin listesi. Fonksiyon saf olduğu için testleri veritabanı ayağa kaldırmadan çalışıyor. Zaman tarafında kural tek: veritabanında her şey zaman dilimli damga olarak UTC'de duruyor, arayüzde Europe/Istanbul'a çevriliyor; günün sınırları da yerel güne göre hesaplanıyor, sunucunun saat dilimine göre değil.
Kural bir exclusion constraint olarak tanımlı: aynı personel için kesişen iki zaman aralığı aynı anda var olamıyor. Kontrol yazma anında veritabanı tarafından yapıldığı için okuma ile yazma arasındaki yarış penceresi ortadan kalkıyor. Bunun asıl kazancı yalnız eşzamanlılık değil kapsam: web formu, panelden elle eklenen randevu, seed script'i ve elle atılan bir SQL — hepsi aynı kurala tabi. Uygulama katmanındaki bir kontrol yalnız kendi yolunu korur.
ÖdünleşimHata artık bir doğrulama mesajı olarak değil, veritabanı kısıt ihlali olarak geliyor; çağıran tarafın bu ihlali yakalayıp kullanıcıya anlamlı bir cümleye çevirmesi gerekiyor. Kısıt ayrıca aralık türü üzerinde bir indeks uzantısı istiyor ve kuralın kendisi migration dosyasında duruyor — kodu okuyan biri kuralı uygulama katmanında aradığında bulamıyor.
Uygunluk, sistemin en çok kenar durumu olan yeri: kapalı gün, mola, personel bazlı doluluk, geçmiş saat, minimum ihbar süresi ve rezervasyon penceresinin sonu. Bunları veritabanı sorgusunun içine gömmek, her senaryoyu ancak gerçek veriyle test edilebilir hâle getirirdi. Hesap girdi/çıktı olarak dışarı alındı; testler tek bir çağrıyla senaryo kuruyor.
ÖdünleşimVeritabanından okunan aralıkların fonksiyona doğru biçimde taşınması çağıran tarafın sorumluluğu; fonksiyon kendisine eksik veri verildiğini fark edemez, yalnız verilenle hesap yapar. Yani hesabın doğruluğu test edilmiş olsa da, hesabın doğru girdiyle çağrıldığı ayrıca doğrulanmak zorunda.
UTC sakla, Europe/Istanbul göster. Randevu aralıkları zaman dilimli damga olarak yazılıyor, gün sınırları yerel güne göre hesaplanıyor ve arayüzde gösterim sabit bir dilim üzerinden yapılıyor. Kuralın tek cümle olması, her yeni ekranın kendi çevrimini icat etmesini engelliyor.
ÖdünleşimSabit bir görüntüleme dilimi, işletme başka bir dilimde çalışmaya başlarsa yetmiyor; dilim bugün bir sabit ve işletme ayarı değil. Testlerde de saat farkının açıkça verilmesi gerekiyor, aksi halde test çalıştığı makinenin dilimine bağımlı hâle geliyor.
Yapılandırma şeması yapılandırma dosyasından import edildiği için hatalı bir ortam çalışma anında değil, derleme veya başlangıç anında patlıyor. Zorunlu değişkenler yalnız uygulamanın ayağa kalkması için şart olanlar; e-posta, hatırlatma ve bildirim gibi özellikler opsiyonel ve anahtarları girilene kadar kapalı kalıyor. Böylece gizli anahtar olmadan da üretim derlemesi alınabiliyor.
ÖdünleşimOpsiyonel bir anahtarın eksikliği sessizce özelliği kapatıyor; yanlış girilen bir anahtar ile hiç girilmemiş anahtar dışarıdan aynı görünüyor. Ayrıca kaçış yolu olarak bir doğrulama atlama anahtarı var ve bu anahtar üretimde açık bırakılırsa şemanın sağladığı garanti ortadan kalkıyor.
Postgres bu projede bir tercih değil şart: exclusion constraint ve aralık türleri bu kararın merkezinde ve başka bir veritabanında aynı garanti aynı ucuzlukta kurulamıyor. Drizzle, kısıtı migration dosyasında düz SQL olarak yazmaya izin verdiği için seçildi — kural ORM soyutlamasının arkasında kalmıyor, okunabiliyor. date-fns-tz, yerel gün ve zaman dilimli damga arasındaki çevrimi tek yerde topluyor. Auth.js yalnız yönetim tarafında; müşteri akışında hesap yok, iptal tek kullanımlık bir bağlantı ile yapılıyor.
Yönetim paneli oturum korumalı; müşteri tarafında hesap yok, dolayısıyla saklanan kimlik verisi de minimum. Randevu iptali, tahmin edilemeyen tek kullanımlık bir belirteç taşıyan bağlantı üzerinden yapılıyor — randevu kimliği adres çubuğunda görünmüyor. Onay metni kaydın üzerinde saklanıyor: kimin, ne zaman, hangi metni onayladığı sonradan tartışmaya açık kalmıyor ve bu ayrı bir testle korunuyor. Yazma yolları tek transaction içinde çalışıyor; eşzamanlı rezervasyon denemesi için yazılmış bir test, ikinci isteğin veritabanı kısıtına takıldığını doğruluyor.
Bu bir prototip ve yayınlanmış bir kurulumu yok; tarih damgalı bir Lighthouse ölçümü de bulunmadığı için buraya sayfa hızı skoru yazılmadı. Ölçülebilir olan taraf uygunluk hesabı: veritabanına yalnız o günün randevuları ve blokları için tek sorgu atılıyor, slot üretimi bellekte yapılıyor. Bu sayede takvimde gün değiştirmek yeni bir tur sorgu üretmiyor.
Bu prototipin asıl çıktısı kod değil, bir alışkanlık: bir kuralın uygulama katmanında mı yoksa veritabanında mı durması gerektiğini sormak. Buradaki cevabı sonraki işlere taşıdım — Bergaz Gıda'da ödeme callback'inin idempotency'sini benzersizlik kısıtına, Bergaz Operasyon'da mükerrer numune kontrolünü kısmi benzersiz indekse indirdim. Ters yönde de bir kanıt var: Taxilos'ta yetki kapısı fonksiyonların içine dağıtıldığı için tek bir predicate'in üç değerli mantığa düşmesi 45 kapıyı birden açtı. Bugün farklı yapacağım şey ise durum tarafı: randevu bir durum makinesi taşıyor ve izinli geçişler hâlâ kodda dağınık; onları da tek bir tanıma indirir, çakışma kuralında yaptığımın aynısını yapardım.
Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve nereden başlanacağını baştan söylerim.