// Backend — 2026-10-09 — 8 min
SaaS'ta "Multi-Tenant" Mimari Nedir? Müşteri Verileri Nasıl Birbirine Karışmadan Ayrık Tutulur
Tek kod tabanı yüzlerce müşteriye hizmet verirken bir satırlık sorgu hatası verilerini karıştırabilir. Multi-tenant mimarinin üç izolasyon modelini anlatıyorum.
Randevu yönetimi yapan bir SaaS'ın destek ekibine bir sabah garip bir e-posta düşüyor: bir kuaför salonu sahibi, panelinde bir anlığına kendi işletmesine ait olmayan randevuları gördüğünü yazıyor. Ekip paniklemeden önce logları açıyor ve sorunu buluyor — raporlama ekranındaki yeni bir sorgu `tenant_id` filtresini uygulamayı unutmuş; sonuç olarak birkaç saniyeliğine tüm müşterilerin randevuları aynı tabloda karışmış görünmüş. Kimsenin verisi silinmemiş, değişmemiş ama güven bir anda sarsılmış. Bu, multi-tenant mimarinin en çok korkulan senaryosu: tek bir kod tabanının yüzlerce farklı işletmeye hizmet vermesi, bir satırlık bir hatanın da aynı anda yüzlerce müşteriyi etkileyebilmesi demek. Bu yazıda multi-tenant mimarinin gerçekte ne olduğunu, hangi izolasyon modelinin ne zaman seçilmesi gerektiğini ve bu tür bir sızıntıyı mimari seviyede nasıl imkansız hale getirebileceğini anlatıyorum.
##Multi-Tenant Mimari Nedir, Single-Tenant'tan Farkı Ne?
“Tenant” (kiracı), bir SaaS'ı kullanan her bir müşteri işletmedir — randevu yönetimi örneğindeki her kuaför salonu, her diyetisyen, her küçük klinik ayrı bir tenant'tır. Single-tenant mimaride her müşteri için ayrı bir uygulama kopyası ve genelde ayrı bir veritabanı kurulur; müşteri sayısı arttıkça sunucu sayısı da doğrudan artar. Multi-tenant mimaride ise tek bir kod tabanı ve tek (ya da mantıksal olarak bölünmüş) bir altyapı, aynı anda onlarca, yüzlerce, hatta binlerce tenant'a hizmet verir. SaaS'ın ekonomisi büyük ölçüde buna dayanır: sunucu, bakım ve geliştirme maliyeti binlerce müşteriye bölünür, bu yüzden aylık 200 TL'ye bir ürün satmak mümkün olur — tek bir sunucu kümesi 500 müşteriye hizmet verdiğinde kişi başına düşen altyapı maliyeti neredeyse sıfıra iner; 500 ayrı kurulum olsaydı bu oran hiçbir zaman tutmazdı. Ama bunun bir bedeli var: hiçbir şey otomatik olarak tenant'ları birbirinden ayırmaz. Bu ayrımı kurmak mimarinin sorumluluğundadır; kurulmazsa, bir geliştiricinin unuttuğu tek bir `WHERE` koşulu bütün müşterileri aynı anda etkileyebilir.
##Üç İzolasyon Modeli: Paylaşılan Tablo, Ayrı Şema, Ayrı Veritabanı
Pratikte üç ana model var ve her biri farklı bir izolasyon-maliyet dengesi sunuyor:
- Paylaşılan veritabanı, paylaşılan şema (tenant_id kolonu): Bütün tenant'ların verisi aynı tablolarda durur, her satırda hangi tenant'a ait olduğunu belirten bir tenant_id kolonu bulunur. Kurulumu en ucuz ve en hızlı model budur — yeni bir müşteri eklemek sadece yeni bir tenant_id değeri demektir. Riski de burada: her sorgunun, her view'ın, her arka plan işinin tenant_id filtresini doğru uygulaması gerekir; bir tanesi unutulursa veri karışır.
- Paylaşılan veritabanı, tenant başına ayrı şema: Her tenant kendi şemasına sahiptir ama hepsi aynı sunucuda, aynı bağlantı havuzunda yaşar. İzolasyon daha güçlüdür çünkü bir sorgu yanlışlıkla başka bir tenant'ın şemasına bakamaz. Bedeli operasyoneldir: her migration'ı tek tek her şemaya uygulamak gerekir — 300 tenant'lık bir sistemde bu 300 migration demektir.
- Tenant başına ayrı veritabanı: En güçlü izolasyon. Bir tenant'ın verisi fiziksel olarak başka bir veritabanında durur; yanlış bir sorgu teorik olarak bile başka bir tenant'a erişemez. Kurumsal müşteriler ve regülasyona tabi sektörler (sağlık, finans) genelde bunu talep eder. Bedeli yüksektir: bağlantı havuzu limitleri, yedekleme sayısı ve operasyon yükü tenant sayısıyla doğrudan çarpılır.
Bağlantı havuzu sınırları da bu kararı doğrudan etkiler: bir PostgreSQL sunucusu tipik olarak birkaç yüz eşzamanlı bağlantıyla rahat çalışır; tenant başına ayrı bir bağlantı havuzu açarsanız 300-400 tenant'tan sonra bu sınıra yaklaşırsınız — bu yüzden çoğu ekip ayrı şema modelinde bile tek bir paylaşılan bağlantı havuzu kullanıp hangi şemaya bakılacağını sorgu zamanında `search_path` ile değiştirir. Çoğu SaaS büyüdükçe hibrit bir yola geçer: uzun kuyruktaki küçük/orta müşteriler paylaşılan şemada kalır, en büyük veya en hassas müşteriler ayrı şema ya da ayrı veritabanına taşınır. Bu “hepsi ya da hiçbiri” bir karar değil — tenant'ın büyüklüğüne ve sözleşme gereksinimine göre değişen bir spektrumdur.
##Gerçek Bir Senaryo: 10 Müşteriden 500 Müşteriye Büyüyen Bir Randevu SaaS'ı
Bir kurucunun randevu yönetimi SaaS'ını 10 müşteriyle paylaşılan tablo + tenant_id modeliyle başlattığını düşünelim — basit, hızlı, o ölçekte doğru karar. 150 tenant'a ulaştığında bir şey fark ediliyor: büyük bir zincirin sahip olduğu 40 şubeli bir müşteri, ayın sonunda ağır bir raporlama sorgusu çalıştırdığında, aynı veritabanını paylaşan küçük kuaför salonlarının normalde 200 milisaniyede açılan randevu alma ekranı birkaç saniyeliğine 2-3 saniyeye kadar yavaşlıyor. Bu, “gürültülü komşu” (noisy neighbor) problemi — bir tenant'ın yoğun kullanımı, aynı altyapıyı paylaşan diğerlerini etkiliyor. Ekip iki şeyi aynı anda yapıyor: önce raporlama sorgularını bir read-replica'ya yönlendiriyor, böylece ağır okuma işlemleri canlı trafiği etkilemiyor. Sonra, en büyük 20 müşteriyi (toplam trafiğin %60'ını oluşturan) kendi şemalarına taşımaya karar veriyorlar.
- Yeni şema oluşturulur ve geçmiş veri tek seferlik bir script ile kopyalanır.
- Uygulama birkaç gün boyunca hem eski hem yeni şemaya çift yazar (dual-write) ve arka planda iki tarafın satır sayıları ile checksum'ları karşılaştırılarak tutarlılık doğrulanır.
- Bir feature flag ile okuma trafiği tek tek yeni şemaya kaydırılır — her tenant ayrı ayrı açılıp birkaç saat izlenir.
- Tutarlılık doğrulandıktan sonra eski şemadaki satırlar silinir.
Kod tarafında değişen tek şey bir “tenant router” katmanı: istek geldiğinde tenant_id'ye bakıp hangi şemanın kullanılacağına karar veren ince bir ara katman. Küçük 480 müşteri paylaşılan şemada kalıyor, en büyük 20'si izole ediliyor. Bu geçiş, bir geliştiricinin tam zamanlı çalışmasıyla yaklaşık 6-8 hafta sürüyor; kalan tenant'lar için hiçbir kesinti yaşanmıyor çünkü taşıma trafiğin düşük olduğu saatlerde, tenant bazında tek tek yapılıyor. Taşıma bittikten sonra ağır raporlama sorgusu artık izole şemada çalıştığı için küçük salonların randevu ekranı bir daha hiç yavaşlamıyor.
##Veri Sızıntısını Önlemek: Row-Level Security ve Pratik Kurallar
Uygulama kodunun her sorguya tenant_id filtresi eklemesi gerekli ama tek başına yeterli değildir — bir geliştiricinin bir gün bunu unutması zaman meselesidir, özellikle ekip büyüdükçe ve yeni katılan birinin bu kuralı henüz içselleştirmediği dönemlerde. Bu yüzden ikinci bir güvenlik katmanı, veritabanının kendisinde kurulmalı. PostgreSQL'de Row-Level Security (RLS) politikaları tam olarak bunu yapar: `CREATE POLICY tenant_isolation ON appointments USING (tenant_id = current_setting('app.tenant_id')::uuid);` gibi tek bir komutla, “bu oturumun tenant_id'si neyse sadece o tenant'a ait satırları göster” kuralını veritabanı seviyesinde tanımlarsınız. Uygulama kodu tenant_id filtresini unutsa bile veritabanı o satırları zaten döndürmez. Bu, “uygulama doğru yazılırsa güvenliyiz” yerine “uygulama yanlış yazılsa bile güvenliyiz” demek — savunma derinliği (defense in depth) dediğimiz şey tam olarak bu.
- Her veritabanı sorgusunun tenant_id ile sınırlandığını zorunlu kılan bir ORM katmanı veya middleware kullanın — geliştiricinin her seferinde hatırlamasına güvenmeyin.
- PostgreSQL kullanıyorsanız RLS politikalarını açın; bu, uygulama katmanındaki bir hatayı veritabanı seviyesinde durdurur.
- tenant_id'yi her zaman oturumdan (JWT/session) türetin, asla URL parametresinden veya form verisinden güvenmeyin.
- Otomatik testlerinize kasıtlı “çapraz tenant erişimi” senaryoları ekleyin: tenant A'nın oturumuyla tenant B'nin verisine erişmeyi deneyen bir test, bu tür bir regresyonu üretime çıkmadan yakalar.
- Büyük veya regülasyona tabi müşteriler için izolasyon seviyesini (paylaşılan şema mı, ayrı veritabanı mı) sözleşmeye açıkça yazın — “multi-tenant” demek her müşteri için aynı izolasyon seviyesi demek değildir.
- Üretim ortamında tenant_id'si olmayan veya beklenmedik bir tenant_id ile çalışan sorguları loglayıp alarm kurun; bu tür bir hata genelde sorun büyümeden önce loglarda kendini belli eder.
Şema/DB başına izolasyonun zorlaşmaya başladığı tenant eşiği
50–200 tenant
Paylaşılan şemadan hibrit izolasyona geçiş süresi
4–10 hafta
Baştan doğru tasarlanırsa ek mühendislik yükü
%10–20
##Sık Sorulan Sorular
>Multi-tenant mimariye en baştan mı karar vermeliyim, yoksa sonradan mı eklenebilir?
tenant_id disiplinini en baştan kurmak ucuzdur — her tabloya bir kolon eklemek, her sorguya bir filtre koymak. Bunu sonradan, veri zaten karışmışken eklemek çok daha pahalı ve risklidir: mevcut milyonlarca satırı geriye dönük etiketlemeniz, bu sırada üretimi durdurmamanız gerekir. Tam izolasyon modelleri (ayrı şema, ayrı veritabanı) ise ölçek gerektirdiğinde eklenebilir — bir numaralı müşteriniz için ayrı veritabanı kurmanıza gerek yok, ama tenant_id kolonunu ilk günden kurun.
>Tek bir tenant'ın verisi diğerine asla karışmayacağı garanti edilebilir mi?
Sadece uygulama koduyla hayır, %100 garanti yoktur — insan hata yapar. Ama katman katman savunarak (uygulama filtresi + veritabanı seviyesinde RLS + otomatik çapraz-tenant testleri) riski pratik olarak ihmal edilebilir seviyeye indirebilirsiniz. Tek bir katmana güvenmek asıl hata; üç katmanın aynı anda başarısız olması gerçekten nadirdir.
>Multi-tenant mimari küçük bir işletme yazılımı için gerekli mi, yoksa sadece büyük SaaS'lar için mi?
Yazdığınız yazılım gelecekte birden fazla müşteriye hizmet verecekse — bugün 2 müşteri olsa bile — tenant_id disiplinini en baştan kurmanın maliyeti neredeyse sıfırdır. Gerçekten tek bir müşteriye özel, asla çoğaltılmayacak bir yazılımsa, bu karmaşıklığı hiç eklemeyin; single-tenant basitçe daha az kod demektir.
>Tenant başına ayrı veritabanı her zaman “daha güvenli” seçenek midir?
Daha izole olduğu doğru ama “daha güvenli” otomatik olarak “daha iyi” anlamına gelmez — bir takas (trade-off). 500 tenant'lık ayrı veritabanları demek, 500 ayrı migration, 500 ayrı yedekleme ve bağlantı havuzu limitlerine çarpma riski demek. Çoğu ürün için doğru cevap, en büyük/en hassas müşterileri izole edip geri kalanını paylaşılan bir modelde tutan hibrit bir yaklaşımdır.
>Bağlantı havuzu her tenant için ayrı mı olmalı, yoksa paylaşılan mı kullanılmalı?
Çoğu ekip paylaşılan bir havuz kullanıp şemayı sorgu zamanında değiştirir, çünkü tenant başına ayrı havuz açmak bağlantı sayısını hızla veritabanı sunucusunun kaldırabileceği sınırın üzerine çıkarır. Ayrı havuz genelde sadece en büyük birkaç kurumsal müşteri için anlamlı olur.
Mimarinin doğrusu büyüme planınıza, müşteri profilinize ve regülasyon gereksinimlerinize göre değişir — tek bir “doğru” model yok. Kendi projende bu kararı nasıl vereceğini konuşmak istersen /contact üzerinden yazabilirsin; veritabanı performansı tarafında benzer bir konuyu merak ediyorsan veritabanı indeksleri üzerine yazdığım yazıya da göz atabilirsin.
// BİRLİKTE ÇALIŞALIM
Benzer bir SaaS projesi mi planlıyorsun?
Kapsam, MVP sıralaması ve teslim takvimi için birlikte net bir yol haritası çıkarabiliriz.
> İLETİŞİME GEÇ