Online terapi platformu nasıl kurulur: PsyBank mimarisinden dersler
Randevudan görüşmeye, ödemeden panele — bir sağlık platformunu uçtan uca ayakta tutan mimari kararların tamamı.
Bir online terapi platformu, teknik olarak bir video görüşme uygulaması değildir. Öyle sanıp yola çıkan ekiplerin çoğu, üç ay sonra elinde çalışan bir WebRTC demosu ve çözülmemiş her şeyle kalır: eşleştirme mantığı, takvim çakışmaları, ödeme iadeleri, terapist hakedişleri, ölçek skorlaması ve bunların hepsinin üzerinde KVKK. PsyBank'ı kurarken ilk günden bildiğimiz şey buydu — görüşme, buzdağının görünen ucuydu; asıl kütle suyun altındaydı.
Bu yazıda PsyBank'ın mimarisini uçtan uca geziyoruz. Altı ana modülü neden tam da o sınırlardan böldüğümüzü, neden moda olan mikroservis furyasına kapılmayıp monolit artı goroutine modelinde kaldığımızı, PostgreSQL ile Redis arasındaki iş bölümünü, KVKK'nın kod tabanını daha ilk commit'ten nasıl şekillendirdiğini ve tüm bunların nasıl 14 haftada yayına çıktığını tek tek anlatacağız. Rakamlar gerçek: bugün platformda 6.443 uzman terapist ve 101.446 danışan var, danışan memnuniyeti %98, destek ticket sistemi üzerinden yürüyor.
Otuz yılda öğrendiğimiz bir şey varsa o da şu: mimari kararların çoğu, tek satır kod yazılmadan önce, "bu veri nerede yaşayacak ve kim görebilecek" sorusuna verdiğiniz cevapta gizlidir. Gerisi büyük ölçüde ayrıntıdır. Şimdi o ayrıntılara inelim.
Altı modül, altı sınır: sistemi nereden böldük
Bir ürünü modüllere bölmek teknik bir karardan çok bir muhasebe kararıdır: her modülün tek bir sahibi, tek bir veri kümesi ve tek bir değişme nedeni olmalı. PsyBank'ı altı modüle böldük ve bu sınırları rastgele değil, "hangi kod aynı sebeple birlikte değişir" sorusuna göre çizdik.
- Eşleştirme (matching) — Danışanı; uzmanlık alanı, terapi yaklaşımı, dil, cinsiyet tercihi ve bütçeye göre uygun terapistlerle buluşturur. Girdisi bir tercih profili, çıktısı sıralı bir terapist listesidir. 24 saat içinde eşleşme garantisini tutan yer burasıdır.
- Randevu ve takvim (scheduling) — Terapistin müsaitlik pencerelerini, danışanın rezervasyonunu, hatırlatmaları ve iptal/erteleme akışlarını yönetir. Zaman dilimleri, çakışmalar ve çifte rezervasyon bu modülün kâbuslarıdır.
- Görüşme (session) — WebRTC sinyalleşmesi, oda yaşam döngüsü ve bağlantı sağlığı. Medyanın kendisi buraya uğramaz; modül yalnızca "kim, kiminle, hangi odada" bilgisini bilir.
- Ödeme (billing) — Seans başı ve paket ödemeleri, terapist hakediş hesabı, iade ve iptal muhasebesi. Para dokunan her yer ayrı bir modül olmayı hak eder.
- Ölçek motoru (assessment) — Terapistin atadığı psikolojik ölçek ve testlerin doldurulması, skorlanması ve raporlanması. Beck, SCL-90 gibi araçların puanlama kuralları burada, tek yerde yaşar.
- Paneller (account) — Danışan ve terapist için iki ayrı rol, iki ayrı deneyim: seans geçmişi, notlar, gelir raporları, profil ve kayıt yönetimi.
Bu altı kutunun ikisi üzerinde biraz durmak gerekir, çünkü ürünün kalbi orada atıyor. Eşleştirmeyi başta saf bir SQL filtresi sandık: uzmanlık eşleşsin, dil tutsun, fiyat aralığına girsin, listeyi döndür. İki hafta içinde bunun yetmediğini gördük — filtre size bir küme verir, oysa danışanın ihtiyacı sıralı bir öneridir. Filtreyi bir skorlama motoruna çevirdik: uygunluğu "eşleşti/eşleşmedi" değil, ağırlıklı bir puan olarak hesaplıyoruz. Terapistin doluluk oranı, geçmiş danışan memnuniyeti ve en erken müsait randevusu bu puana giriyor; çünkü "mükemmel ama üç hafta sonrasına randevusu olan" terapist, çoğu danışan için "çok iyi ve yarın müsait" olandan kötü bir öneridir. 24 saat içinde eşleşme garantisini tutan da bu mantık: hiçbir otomatik öneri yeterince güçlü değilse iş bir insan operatöre düşüyor — garanti bir algoritma vaadi değil, bir süreç vaadi.
Ölçek motoru ise sessiz ama tehlikeli bir modül. Beck Depresyon Envanteri, SCL-90 gibi araçların puanlama kuralları klinik olarak kesindir; bir eşik değerini yanlış hesaplamak, koca bir raporu yanlış yorumlatır. Buradaki en büyük anti-pattern'i baştan reddettik: skorlama mantığını arayüze koymak. Puanlama yalnızca sunucuda, tek bir yerde ve sürümlenerek yaşıyor. Bir ölçeğin puanlama kuralı güncellenirse eski sonuçlar hangi sürümle hesaplandıysa o sürümle kalıyor; çünkü iki yıl önceki bir danışanın skorunu bugünün kuralıyla yeniden yorumlamak, veriyi sessizce bozmaktır.
Paneller modülünde ise en sık yapılan hatadan kaçındık: tek bir kullanıcı tipini alıp "terapist mi?" bayrağıyla iki role büründürmek. Danışan ile terapist, aynı sistemde yaşayan iki farklı iş dünyasıdır — birinin derdi seans geçmişi ve kişisel ilerleme, diğerininki takvim doluluğu, gelir raporu ve danışan notları. Bu iki deneyimi ayrı ele almak her ekranı sadeleştirdi ve yetkilendirmeyi netleştirdi; çünkü bir rolün asla görmemesi gereken şeyi o rolün arayüzünde hiç render etmiyorsanız, yanlışlıkla sızdırma ihtimali de kökten ortadan kalkar.
Bu altı modül, tek bir Go binary'sinin içinde ayrı paketler olarak yaşar. Fiziksel olarak ayrılmış değiller; mantıksal olarak ayrılmışlar. Ve aralarındaki en önemli kural şu: bir modül başka bir modülün tablosuna asla doğrudan dokunmaz. Eşleştirme modülü terapistin müsaitliğini bilmek istiyorsa, scheduling paketinin tablosundan SELECT çekmez; scheduling'in dışarıya açtığı arayüzü çağırır. Bu disiplin olmadan monolit, altı ay içinde herkesin her şeyi çektiği bir "büyük çamur topu"na dönüşür — biz bunu daha önceki hayatlarımızda defalarca gördük.
psybank/
cmd/api/main.go
internal/
matching/ // eşleştirme motoru
scheduling/ // randevu & takvim
session/ // WebRTC sinyalleşme, oda yönetimi
billing/ // ödeme, hakediş, iade
assessment/ // ölçek-test motoru, skorlama
account/ // danışan + terapist panelleri
platform/ // db, redis, auth, denetim izi (ortak)
Sınırı kodla zorunlu kılmak için modüller birbirini yalnızca arayüz üzerinden görür. Bir modülün dışarıya açtığı yüzey daraldıkça, o modülü değiştirmek de o kadar ucuzlar:
// Her modül dışarıya yalnızca arayüzünü açar; tablosunu değil.
type Scheduling interface {
Availability(ctx context.Context, therapist ID) ([]Slot, error)
Reserve(ctx context.Context, slot ID, client ID) (Appointment, error)
}
// Eşleştirme, scheduling'in içini bilmez; yalnızca bu arayüzü çağırır.
func (m *matcher) Suggest(ctx context.Context, pref Preferences) ([]Therapist, error) {
// ... uygun terapistleri skorla, en erken müsaitliği sched'den sor ...
}
Modül sınırı, iki takımın birbirinden habersiz çalışabildiği yerdir. Sınırı doğru çizdiyseniz, arayüzün arkasını gece yarısı baştan yazabilirsiniz ve çağıran taraf hiçbir şey fark etmez.
Neden monolit ve goroutine, neden mikroservis değil
2020'lerin refleksi şu oldu: "ölçeklenecek bir şey mi kuruyorsun? O halde her modülü ayrı servise böl, aralarına ağ koy." Biz bu refleksin faturasını çok gördük. On kişilik bir ekip için mikroservis; dağıtık transaction'lar, servisler arası sürüm uyumu, ağ üzerinden gelen gizemli gecikmeler ve her deploy'da artan operasyonel yük demektir. PsyBank'ın ilk gün sorusu "1000 mühendisle nasıl ölçekleniriz" değildi; "14 haftada nasıl yayına çıkarız ve gece 2'de tek kişi nasıl nöbet tutar" idi.
Cevabımız modüler monolit oldu: yukarıdaki altı paket, tek bir Go servisi olarak derlenip tek imaj olarak dağıtılıyor. Eşzamanlılığı ağ üzerinden değil, sürecin içinden — goroutine'lerle — çözüyoruz. Binlerce eşzamanlı video seansını tek servis üzerinde yönetebilmemizin sebebi bu. Her seans için üç hafif goroutine açıyoruz: sinyalleşme, bağlantı sağlığı izleme ve KVKK denetim izi. Bir goroutine birkaç KB yığınla başlar; bu yüzden on binlerce eşzamanlı oturum, thread-per-connection modellerinin bellek duvarına toslamadan yaşayabilir.
Bu izolasyonun bedavaya gelen bir yan faydası daha var: bir seansta oluşan hata diğerlerine bulaşmaz. Her oturum kendi goroutine'lerinde yaşadığı için, birinin bağlantısı koptuğunda ya da beklenmedik bir panic aldığında o seans temizce kapanır ve komşu binlerce seans hiçbir şey hissetmez. Bağlantı düşmelerini de sessizce yutmuyoruz; jitter'lı bir exponential backoff ile yeniden bağlanma deniyoruz, ki sunucu yeniden başladığında binlerce istemci aynı anda kapıya yüklenip "bağlantı fırtınası" yaratmasın. Bir terapi seansının teknik bir aksaklık yüzünden yarıda kesilmesi, sıradan bir uygulamadaki kopmadan çok daha ağır bir andır — mimariyi tam da bu hassasiyetle kurduk.
Bu kararın somut kazançları şunlar oldu:
- Tek transaction sınırı. "Randevu oluştur, ödemeyi çek, terapist takvimini işaretle" adımları aynı süreçte, aynı veritabanı transaction'ında yürüyor. Mikroservislerde bunun için saga desenleri, telafi işlemleri ve idempotency anahtarları kurmanız gerekirdi. Biz bir BEGIN/COMMIT ile hallettik.
- Tek deploy, tek gözlem noktası. Tek binary, tek log akışı, tek metrik panosu. Bir hatayı kovalarken yedi servisin trace'ini birbirine dikmiyorsunuz.
- Ölçeklenme hâlâ mümkün. Monolit demek "tek makine" demek değil. Aynı imajı yatay olarak birden çok düğüme kopyalıyoruz; durumu Redis'e taşıdığımız için düğümler eş değerli. Yük testinde tek 4 vCPU'luk düğüm, p99 gecikme 38ms'de 12.400 eşzamanlı bağlantı taşıdı; üretimde iki düğümle en yoğun akşam saatlerinde bile %40 yedek kapasiteyle çalışıyoruz.
Bunun operasyonel karşılığını bir örnekle somutlaştıralım. Mikroservis dünyasında "randevu oluşturuldu ama ödeme servisi yanıt vermedi" senaryosu, telafi işlemleri ve elle mutabakat gerektiren klasik bir baş ağrısıdır. Bizde böyle bir ara durum yok: ödeme çekilmediyse transaction geri sarılır ve randevu hiç var olmamış gibi olur. Nöbetçi mühendisin gece yarısı "yarım kalmış" kayıtları elle temizlemesi diye bir iş, mimaride hiç tanımlı değil. Karmaşıklığı azaltmanın en iyi yeri üretim değil, tasarım masasıdır.
Yatay ölçeklenmeyi mümkün kılan şey ise düğümlerin durumsuz olması. Uygulama sürecinde hiç kalıcı durum tutmuyoruz; bir danışanın isteği hangi düğüme düşerse düşsün aynı cevabı alır. Oturum Redis'te, gerçek Postgres'te olduğu için "yapışkan oturum" (sticky session) numaralarına ihtiyaç duymuyoruz. Bir düğüm çökerse yük dengeleyici trafiği ötekilere kaydırır ve kimse görüşmesinin ortasında yarıda kalmaz. Durumsuzluk, ölçeklenmenin sıkıcı ama en sağlam yoludur.
Yanlış anlaşılmasın: mikroservis kötü değil, erken. Bir sınırın gerçekten ayrı ölçeklenmesi, ayrı bir dille yazılması ya da ayrı bir takıma devredilmesi gerektiğinde onu servise çıkarırsınız — ama bunu kanıt geldiğinde yaparsınız, moda diye değil. Modüler monolitin güzelliği, bu göçün ucuz olmasıdır: sınırlarınız zaten arayüzse, bir paketi servise terfi ettirmek mekanik bir iştir. Sınırı yanlış çizip erken bölerseniz, o ağı geri sökmek çok daha pahalıdır.
Dağıtık sistem, kazandığınız bir ödül değil, ödediğiniz bir bedeldir. Bedeli, ancak tek süreç gerçekten yetmediğinde ödeyin.
PostgreSQL ve Redis: hangi veri nerede yaşar
Bir platformun sağlığı, çoğu zaman "kalıcı gerçek nerede, geçici durum nerede" ayrımını ne kadar net yaptığınıza bakar. Biz bu ayrımı sert bir kuralla kestik: PostgreSQL kayıt defteridir, Redis ise çalışma belleğidir. İkisini karıştıran ekipler ya Redis'i tek gerçek kaynağı sanıp veri kaybeder, ya da PostgreSQL'i milisaniyelik durum sorgularıyla dövüp boğar.
PostgreSQL'de ne var: danışan ve terapist hesapları, şifrelenmiş danışan kayıtları, randevular, ödeme ve hakediş defteri, ölçek sonuçları, denetim izi. Yani kaybolduğunda geri getiremeyeceğiniz her şey. Burada ödün vermediğimiz üç şey: alan bazlı erişim kontrolü (herkes her sütunu göremez), transaction bütünlüğü (para ve randevu ya birlikte olur ya hiç) ve değiştirilemez denetim logları. Ölçek motorunun sonuçları da burada, çünkü bir danışanın zaman içindeki skor eğrisi klinik olarak anlamlı bir veridir — geçici değil.
Para tarafında küçük ama pahalı bir kuralımız var: tutarları asla ondalıklı kayan noktalı sayı (float) ile tutmuyoruz, kuruş cinsinden tam sayı olarak saklıyoruz. Float ile para tutan sistemlerde toplama-çıkarma zamanla kuruş kayması yaratır; terapist hakediş defterinde bu, ayın sonunda tutmayan bir bakiye demektir. Hakediş, komisyon ve iade hesabının tamamı tam sayı aritmetiğiyle ve tek transaction içinde yürüyor — bir seans ücretlendirildiğinde danışanın ödemesi, terapistin hakedişi ve platform komisyonu ya birlikte yazılır ya hiç. Muhasebede "yaklaşık doğru" diye bir şey yoktur; ya kuruşu kuruşuna tutar ya da sistem değildir.
Redis'te ne var: oturum jetonları, terapist müsaitlik durumu (gerçek zamanlı "şu an çevrimiçi/görüşmede/uygun"), bildirim kuyruğu, WebRTC oda durumunun sunucular arası senkronizasyonu için pub/sub kanalları ve randevu rezervasyonundaki kısa ömürlü kilitler. Bunların ortak özelliği: hızlı olması gerekir ve kaybolursa dünya durmaz. Redis düşerse müsaitlik göstergesi birkaç saniye eskir; ama tek bir randevu, tek bir ödeme kaybolmaz — çünkü onlar Redis'te yaşamaz.
Redis'in en sevdiğimiz işi çifte rezervasyonu önlemek oldu. İki danışan aynı saniyede aynı boş slotu tıkladığında, PostgreSQL'e iki INSERT birden gitmeden önce Redis'te atomik bir kilit alıyoruz:
func (s *scheduler) Reserve(ctx context.Context, slot ID, client ID) error {
key := fmt.Sprintf("slot:lock:%s", slot)
ok, err := s.rdb.SetNX(ctx, key, client, 30*time.Second).Result()
if err != nil {
return err
}
if !ok {
return ErrSlotTaken // aynı saniyede ikinci danışan; kaybeden temiz bir hata alır
}
return s.db.InsertAppointment(ctx, slot, client) // kalıcı gerçek Postgres'te
}
Dikkat edin: kilit Redis'te, ama randevunun kendisi Postgres'te. Redis burada bir hız katmanı; gerçeğin sahibi değil. Redis'in TTL'i (burada 30 saniye) bize bedava bir güvenlik ağı da veriyor — süreç ödeme adımında çökerse kilit kendiliğinden düşer, slot sonsuza dek asılı kalmaz. Bu tür küçük kararlar, üretimde geceleri sizi uyandıran çağrıların çoğunu baştan siler.
Bildirimler de Redis'in sırtında. Randevu hatırlatmaları, seans başlangıç uyarıları ve terapiste düşen yeni danışan bildirimleri bir kuyrukta birikip arka planda işleniyor; kullanıcının isteğini yavaşlatan senkron bir e-posta ya da SMS çağrısı yok. Kullanıcıyı bekleten her dış servis çağrısını istek yolundan çıkarıp kuyruğa almak, algılanan hızın en ucuz kaynağıdır — kullanıcı, arka planda ne olduğunu değil, ekranın ne kadar çabuk cevap verdiğini hisseder.
KVKK'yı sonradan yamamadık, mimariyi ona göre kurduk
Sağlık verisi, KVKK'nın "özel nitelikli kişisel veri" dediği en hassas sınıftır; bir terapi seansının içeriği ise bu sınıfın da en mahrem köşesidir. Çoğu ekip uyumu bir kontrol listesi, en sona bırakılan bir "compliance" görevi sanır. Biz tersini yaptık: KVKK'yı bir kısıt değil, mimarinin ilk şekillendirici kuvveti olarak aldık. Çünkü gizliliği sonradan eklenen bir katman olarak düşünürseniz, o katmanın delikleri veri modelinizin ta kendisinde açılır.
En radikal kararımız görüşme katmanındaydı: seans içeriği hiçbir zaman sunucuya uğramaz. WebRTC'de medya uçtan uca akar; sunucumuzun görevi yalnızca tarafları tanıştırmak, yani SDP tekliflerini ve ICE adaylarını taşımaktır. Video, ses ve mesaj, danışanla terapist arasında şifreli olarak doğrudan gider. Bu bir performans değil, bir sorumluluk kararıydı: sunucuda olmayan veriyi ne biz sızdırabiliriz, ne bir saldırgan çalabilir, ne de bir mahkeme celbi bizden isteyebilir. Elinizde tutmadığınız veri, koruması gerekmeyen veridir.
En güvenli veri, hiç toplamadığınız veridir. Bir alanı veritabanına eklemeden önce sorun: bu olmadan ürün çalışmıyor mu? Cevap "hayır"sa, o alanı eklemeyin.
Toplamak zorunda olduğumuz veride ise mimariyi şu ilkelerle dokuduk:
- Veri minimizasyonu. Her tabloyu tasarlarken "bu alan gerçekten gerekli mi" filtresinden geçirdik. Toplamadığınız veri, sızdıramayacağınız veridir; en ucuz güvenlik önlemi bu.
- Alan bazlı erişim kontrolü. Danışan kayıtları PostgreSQL'de şifreli tutuluyor ve "kullanıcı okuyabilir" gibi kaba bir yetki yok; hangi rolün hangi sütunu hangi bağlamda görebileceği sütun düzeyinde tanımlı. Bir terapist yalnızca kendi danışanının, yalnızca kendi seanslarıyla ilgili alanını görür.
- Değiştirilemez denetim izi. Hassas veriye her erişim — kim, ne zaman, hangi kaydı, hangi amaçla — ayrı bir denetim tablosuna yazılıyor. Hatırlayın, her seans goroutine'inin üç bacağından biri buydu. Bu log, sorun çıktığında "ne olduğunu" kanıtlayabildiğiniz tek yerdir.
- Rıza ve silme, birinci sınıf vatandaş. Danışanın rızası ve "unutulma hakkı" veri modelinin sonradan eklenen bir bayrağı değil, ilk günden şemanın parçası. Bir kaydı gerçekten silebilmek, onu nerelere kopyaladığınızı en baştan bilmeyi gerektirir — bu yüzden veriyi dağıtmadık.
Saklama süresi de mimarinin parçası oldu, bir hukuk metninin dipnotu değil. KVKK, veriyi "gerektiği kadar" tutmayı emreder; biz de her veri sınıfına bir ömür biçtik ve bu ömrü kodda, elle silmeye bel bağlamadan uyguladık. Süresi dolan geçici veriler ve denetim kayıtları otomatik olarak temizleniyor. Silmeyi bir insanın hatırlamasına bırakan her sistem, er ya da geç unutulan bir veriyle ihlale yürür; biz unutmayı makineye devrettik, çünkü makine unutmaz — sadece programlandığı gibi yapar.
Bu kararların hepsi mimariyi kısıtladı, evet. Ama aynı kısıtlar bize sadelik hediye etti: veriyi az yerde, az kopyayla, net sahiplerle tuttuğumuz için sistemin tamamını kafamızda tutabiliyoruz. Güvenlik ve sadelik, sanılanın aksine, çoğu zaman aynı yöne çeker.
14 haftada nasıl teslim edildi
Bir sağlık platformunu 14 haftada yayına çıkarmak kulağa iddialı gelir; öyle de. Ama bu bir kahramanlık hikâyesi değil, disiplinli sınır çizmenin doğal sonucuydu. Şunları doğru yaptığımız için oldu:
- Bir hafta keşif, gerçek keşif. Kod yazmadan önceki hafta paydaş görüşmelerine, rakip analizine ve — en önemlisi — bir KVKK gereksinim haritasına gitti. "Hangi veri, neden, ne kadar süre" sorusunu en başta cevapladığımız için sonra mimariyi söken bir sürprizle karşılaşmadık.
- İki haftalık sprintler, her sprint sonunda çalışan demo. Yedi sprint boyunca her iki haftada bir gerçekten çalışan bir şey gösterdik. Bu, "%80 bitti ama hiçbir şey çalışmıyor" tuzağının panzehiridir; bir şey erken çalışıyorsa, gerçekten çalışıyordur.
- Modül sınırları paralel çalışmayı mümkün kıldı. Eşleştirme, ödeme ve ölçek motoru arayüzlerle ayrıldığı için farklı geliştiriciler birbirini beklemeden ilerledi. Arayüz kararlaştırıldığı an, arkasını dolduran iş bağımsızlaşır.
- Doğru soyutlama seviyesinde hazır parçalar kullandık. WebRTC'yi sıfırdan yazmadık; sinyalleşmeyi biz kurduk ama medya taşımayı standartlara bıraktık. Ödeme entegrasyonunu, kimlik doğrulamayı benzer şekilde kanıtlanmış bileşenlerle çözdük. Enerjiyi ürünün kendine özgü zor kısımlarına — eşleştirme ve ölçek skorlaması — sakladık.
- Aynı stack, iki üründe pişmişti. Go, Svelte, PostgreSQL ve Redis bizim için yeni değildi; kararların çoğunu daha önce verip bedelini ödemiştik. Yeni bir dili öğrenerek değil, bildiğimiz araçları isabetle kullanarak hız kazandık.
Svelte tarafını da unutmamak lazım: hafif ve hızlı açılan bir arayüz, düşük bant genişliğinde bile akıcı bir danışan deneyimi verdi. Terapiye başvuran birinin ilk temas anında karşısına ağır, takılan bir arayüz çıkması, ürünün vaadiyle çelişirdi. Frontend'in hafifliği bir estetik tercih değil, erişilebilirlik kararıydı.
Yayın öncesi son iki sprintte iki tür teste ağırlık verdik: güvenlik ve yük. Güvenlik tarafında erişim kontrollerini rolleri değiştirerek dövdük — bir terapist, başka bir terapistin danışanına ait tek bir alanı bile göremiyor mu, bunu bilerek deneyip doğruladık. Yük tarafında hedefimiz tek düğümde 10.000 eşzamanlı bağlantıydı; ölçtük, aştık ve ancak ondan sonra kademeli yayına geçtik. "Çalışıyor gibi görünüyor" ile "ölçtük, çalışıyor" arasındaki fark, bir sağlık ürününde asla pazarlık konusu değildir. Testi yayından önce yapmazsanız, kullanıcılarınız onu sizin yerinize ve en kötü anda yapar.
14 haftanın en büyük sırrı ise şu: neyi yapmayacağımıza da erken karar verdik. Kapsamı modül bazında yazılı bir teklife bağladık; "şunu da ekleyelim" baskısı geldiğinde ölçüt hazırdı. Bir ürünü zamanında bitiren şey, ne yaptığınız kadar, ne yapmamaya söz verdiğinizdir.
Sonuç: benzer bir platform kuracaklara yol haritası
PsyBank'ın hikâyesinden çıkan ders, bir teknoloji seçimi değil, bir öncelik sıralaması. Benzer bir sağlık ya da iki taraflı pazar yeri platformu kuracaksanız, sıralamayı şöyle öneririz:
- Önce veriyi haritalayın, sonra kod yazın. Hangi veri kalıcı gerçek (PostgreSQL), hangisi geçici durum (Redis), hangisini hiç tutmamalısınız (seans içeriği). Bu üç kovayı netleştirmeden ekran tasarlamaya başlamayın.
- Gizliliği ilk commit'e koyun. KVKK'yı sona bıraktığınız her gün, sökülmesi pahalanan bir borç biriktirirsiniz. En hassas veriyi hiç toplamayacak şekilde tasarlayın; toplamak zorunda olduğunuzu şifreleyin, sütun düzeyinde yetkilendirin ve her erişimi loglayın.
- Modüler monolitle başlayın. Sınırları arayüzlerle net çizin ama tek binary olarak dağıtın. Mikroservise geçişi, kanıt geldiğinde yapacağınız ucuz bir terfi olarak saklayın; peşinen ödemeyin.
- Eşzamanlılığı süreç içinde çözün. Uzun ömürlü, çok sayıda bağlantı için goroutine benzeri hafif eşzamanlılık modelleri, ağ üzerinden dağıtmaktan hem ucuz hem basittir. İzolasyonu tasarlayın: her oturum kendi küçük dünyasında yaşasın, çöksün, kapansın.
- Kapsamı erken kesin. Her modülün tek sahibi, tek veri kümesi, tek değişme nedeni olsun. Neyi yapmayacağınızı yazılı hale getirin; teslim tarihinizi kurtaran şey bu satırlar olacak.
Bu adımların ortak paydası tek bir cümlede toplanır: kararı geciktirin, ama sınırı geciktirmeyin. Hangi veritabanını, hangi bulut sağlayıcısını, hangi ödeme entegrasyonunu kullanacağınız değiştirilebilir kararlardır ve zaman içinde rahatça değişebilir; modül sınırlarınız ile gizlilik modeliniz ise sistemin iskeletidir ve sonradan değiştirmek pahalıya patlar. İskeleti ilk gün doğru dökün, kasları sonra ekleyin. PsyBank'ta işe yarayan da buydu: doğru iskelet, 14 haftalık kaslanmayı taşıyacak kadar sağlamdı.
Son bir söz. Bu işi 30 yıldır yapanlar olarak en çok gördüğümüz hata, mimariyi zenginleştirmekle güçlendirmeyi karıştırmaktır. Daha çok servis, daha çok teknoloji, daha çok katman; genellikle daha çok güç değil, daha çok kör nokta demektir. PsyBank'ı ayakta tutan şey akıllı numaralar değil, birkaç sağlam sınırın inatla korunması oldu. Siz de öyle yapın: az sayıda doğru sınır çizin, onları savunun, gerisini ürün büyüdükçe kanıta bakarak ekleyin. İyi mimari, eklediklerinizle değil, direndiklerinizle ölçülür.