
Ustura
Randevu Prototipi
Bir berber için kurduğum randevu sistemi prototipi. Müşteri projesi değil; aynı saatin iki müşteriye verilmesini çalışanın dikkatine değil, veritabanının kendisine engelletmeyi denediğim bir çalışma.
Kullanılan Teknolojiler
Öne Çıkan Özellikler
- Randevu çakışması veritabanı kısıtıyla imkânsız — uygulama katmanında kontrol yok
- Zaman UTC saklanıyor, Europe/Istanbul gösteriliyor
- Uygunluk hesabı saf fonksiyon — veritabanına dokunmadan test ediliyor
- Ortam değişkenleri Zod ile derleme anında doğrulanıyor
- Personel, hizmet, çalışma saati, mola ve blok tanımları panelden yönetiliyor
- Müşteri randevusunu tek kullanımlık bağlantıyla iptal edebiliyor
Geliştirme Süresi
Problem
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.
Kullanıcılar
İ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.
Rolüm
Tek geliştirici, prototipin tamamı. Veri modeli ve kısıtlar, uygunluk hesabı, rezervasyon ve iptal akışları, yönetim paneli, bildirim katmanı.
Mimari
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.
Kritik Kararlar
Çakışma engeli veritabanında, uygulamada 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 hesabı saf bir fonksiyon
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.
Saat dilimi kararı tek cümleye indirildi
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.
Ortam değişkenleri derleme anında doğrulanıyor
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.
Teknoloji Seçimi
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.
Güvenlik
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.
Performans
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.
Sonuçlar
- 2026-07 · 13 commit
- 133
- 8 — uygunluk, tarih, telefon, yerel saat, eşzamanlı rezervasyon, iptal, erteleme, onay
- 9 tablo · 4 migration
- 16 sayfa — site ve yönetim paneli
- Aynı personel için kesişen iki onaylı randevu veritabanı seviyesinde imkânsız
- Zorunlu ortam değişkeni eksikse üretim derlemesi başarısız olur
Bugün Ne Farklı Yapardım
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.
Nereden başlayacağınızı birlikte bulalım
Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve en mantıklı ilk adımı baştan söylerim.