Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi System Design — #01

System Design Nedir, Nasıl Çalışılır? Requirement'lardan Deep Dive'a 4 Adımlı Yöntem

Mert Özen 8 Kas 2026 10 dk 4 görüntülenme
System Design Nedir, Nasıl Çalışılır? Requirement'lardan Deep Dive'a 4 Adımlı Yöntem

System design'a dört adımla yaklaşılır: requirement'ları ve scope'u netleştir, high-level design'ı çizip onay al, problemi taşıyan component'te deep dive yap, trade-off'ları söyleyip toparla. Kampanya günü çöken Bookstore örneğinde Little's law (L = λ × W) darboğazın CPU değil connection pool olduğunu kâğıt üstünde gösteriyor ve gerçek SqlClient çıktısı bunu doğruluyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir; connection pool örneği Microsoft.Data.SqlClient 7.1.1 ile, SQL (Structured Query Language) Server 2025 üzerinde koşturuldu.

Önce Tahmin Et

Okul açılışı kampanyasının ilk saati. Bookstore'un katalog araması normal günde saniyede 20 istek alıyor, bugün 200 istek geliyor. Uygulama tek bir ASP.NET Core instance'ı ve tek bir SQL Server; CPU (Central Processing Unit) %40'ta, bellek rahat. Kampanya banner'ındaki "en çok satanlar" sorgusu index'siz bir kolona baktığı için veritabanı yavaşlıyor ve aramanın ortalama süresi 300 ms'den 1,5 saniyeye çıkıyor. On dakika sonra kullanıcılar arama yerine hata sayfası görmeye başlıyor; loglarda tek bir satır tekrar ediyor.

Sence ilk tükenen kaynak hangisi: CPU mu, ASP.NET Core'un thread'leri mi, yoksa başka bir şey mi? Ve bunu, sistem çökmeden önce, bir kâğıt kalemle bulabilir miydin?

System Design Nedir?

System design, bir yazılımın hangi parçalardan oluşacağına, bu parçaların nasıl haberleşeceğine ve yük, hata ve büyüme karşısında nasıl davranacağına karar verme işi. Class ve method seviyesindeki design'dan farkı ölçeği: burada konuştuğumuz birimler load balancer, cache, veritabanı replica'sı, queue ve bunların arasındaki network. Mimariye Giriş serisinin "Yazılım Mimarisi Nedir? Design'dan Farkı ve Architect'in Rolü" yazısındaki ayrımla söylersek, system design mimarinin runtime ve altyapı yüzü: hangi kutu nerede çalışır, istek hangi yoldan geçer, bir kutu düşünce ne olur.

Bu seri iki şeyi birlikte hedefliyor. Birincisi system design interview'ları: 45–60 dakikada, beyaz tahta başında "bir URL (Uniform Resource Locator) shortener tasarla" sorusuna yapılandırılmış bir cevap vermek. İkincisi gerçek projeler: Bookstore'un kampanya günü neden çöktüğünü bulmak ve bir sonrakine hazırlanmak. İkisinin kası aynı; fark, gerçek projede ölçebilmen ve kararlarını yazılı bırakman gerekmesi.

Interview tarafında en çok başvurulan yapı, Alex Xu'nun System Design Interview (Vol. 1) kitabındaki dört adım: problemi anla ve scope'u belirle, high-level design öner ve onay al, deep dive yap, toparla. Donne Martin'in açık kaynak System Design Primer'ı da benzer bir sırayı öneriyor: use case ve kısıtları yaz, high-level design çiz, temel component'leri tasarla, ölçekle. Aşağıda bu dört adımı kampanya günü problemine uyguluyoruz.

Adım 1: Requirement'ları Netleştirmek ve Scope'u Daraltmak

İlk adım bir şey çizmek değil, soru sormak. Xu, 45 dakikalık bir görüşmede bu adıma 3–10 dakika ayırmayı öneriyor; gerçek projede bu birkaç toplantı ve bir doküman demek. İki tür requirement ayırıyoruz.

Functional requirement'lar sistemin ne yaptığı: kullanıcı kitap arar, sepete ekler, ödeme yapar, sipariş durumunu görür. Non-functional requirement'lar (NFR) bunu ne kadar iyi yaptığı: kaç kullanıcıya, hangi gecikmeyle, hangi availability ile, hangi tutarlılıkla. Mimariye Giriş serisinin "Quality Attribute Senaryoları: "Hızlı Olsun"u Ölçülebilir Bir Gereksinime Çevirmek" yazısı NFR'ları kaynak, uyaran, ortam ve ölçü ile yazmayı anlatıyordu. Interview hızında aynı şeyi tek satırlık cümlelerle yaparsın: "katalog araması p95 (isteklerin %95'inin altında kaldığı süre) 300 ms altında, saniyede 200 istekte".

Kampanya günü problemi için soracağım sorular şunlar: Hangi akış kritik, arama mı checkout mu? Zirve trafik normalin kaç katı ve ne kadar sürüyor? Kullanıcı hata görmektense biraz eski veri görmeyi kabul eder mi? Ödeme sağlayıcısı yavaşlarsa sipariş beklemeye alınabilir mi? Bütçe ve ekip ne kadar; üç developer'lık bir ekip için multi-region bir çözüm konuşmanın anlamı yok.

Cevapları varsayım olarak yazıyorum, çünkü sonraki her hesap bunlara dayanıyor:

Assumptions (campaign day, Bookstore)
- normal: ~15,000 unique visitors/day, ~800 orders/day
- peak: 10x normal, ~2 hours; search ~200 req/s, checkout ~120 orders/min (2/s)
- targets: search p95 < 300 ms, checkout p95 < 800 ms, checkout availability 99.9%/month
- payment provider slower than 5 s -> order becomes PaymentPending, no error to user
- out of scope: multi-region, mobile app, recommendations

Son satır en az ilki kadar önemli. Scope'u daraltmak, interview'da kalan 35 dakikayı gerçekten önemli olana harcamanı sağlıyor; gerçek projede de "bunu şimdi yapmıyoruz" kararının yazılı olması, altı ay sonra "neden multi-region değil?" sorusunun cevabı oluyor.

Adım 2: High-Level Design ve Onay Almak

İkinci adımda kutuları ve okları çiziyorsun: client, (varsa) CDN (Content Delivery Network), load balancer, uygulama sunucuları, veritabanı, cache, queue, dış sistemler. Amaç detay değil, karşındakiyle aynı resme bakmak. Bookstore'un bugünkü hâli çok sade:

Browser -> Bookstore.Web (1 instance, ASP.NET Core)
             |-> SQL Server (catalog, orders, stock)
             |-> payment provider (HTTP, sync, 5 s timeout)
             |-> e-mail provider (SMTP, via in-process Channel<T> queue)

Bu çizimi gösterip "akış böyle, darboğazı şurada arayacağım" demek, onay almanın kendisi. Interview'da bu noktada API'yi (Application Programming Interface) ve veri modelini de kabaca yazarsın: GET /api/books?q=, POST /api/orders ve tablolar. Back-of-the-envelope hesaplar da bu adıma girer; bir sonraki yazının konusu tam olarak bu.

Adım 3: Deep Dive: Darboğazı Hesapla

Deep dive'da bütün kutuları değil, problemi taşıyan bir ya da iki kutuyu açıyorsun. Kampanya gününde kritik soru şu: arama yavaşlayınca sistemde aynı anda kaç istek bekler? Bunun cevabı queueing teorisinin en kullanışlı sonucu olan Little's law: sistemde ortalama bekleyen iş sayısı = geliş hızı × her işin sistemde kaldığı süre (L = λ × W). Formül, gelişlerin dağılımından bağımsız ve kararlı durumdaki her sisteme uyuyor.

// Little's law: aynı anda bekleyen istek = geliş hızı x sistemde kalma süresi
Endpoint[] campaignPeak =
[
    new("search", 200, 0.30),
    new("search", 200, 1.50),
    new("checkout", 2, 0.80),
    new("checkout", 2, 7.00),
];

Console.WriteLine("Campaign peak, requests in flight (L = lambda x W)");
foreach (Endpoint e in campaignPeak)
    Console.WriteLine($"  {e.Name,-9}{e.RequestsPerSecond,5} req/s x {e.LatencySeconds:0.00} s = {e.InFlight,6:0.#} in flight");

record Endpoint(string Name, double RequestsPerSecond, double LatencySeconds)
{
    public double InFlight => RequestsPerSecond * LatencySeconds;
}
Campaign peak, requests in flight (L = lambda x W)
  search     200 req/s x 0.30 s =     60 in flight
  search     200 req/s x 1.50 s =    300 in flight
  checkout     2 req/s x 0.80 s =    1.6 in flight
  checkout     2 req/s x 7.00 s =     14 in flight

Trafik değişmedi, yalnız süre beş katına çıktı ve aynı anda içeride bekleyen arama isteği 60'tan 300'e çıktı. Bu isteklerin her biri sorgu boyunca bir veritabanı bağlantısı tutuyor. Microsoft.Data.SqlClient'ın connection pool'u varsayılan olarak en fazla 100 bağlantı açıyor (Max Pool Size=100) ve boş bağlantı bekleyen istek, Connect Timeout kadar (varsayılan 15 saniye) bekledikten sonra exception alıyor. Checkout ise aynı ödeme sağlayıcısı 7 saniyeye yavaşlasa bile yalnız 14 eşzamanlı isteğe çıkıyor; kampanya gününde sistemi düşüren checkout değil, arama.

Hesabı gerçek bir pool üzerinde doğrulayalım. Beş saniyelik yavaş sorguyu WAITFOR DELAY ile taklit ediyoruz; pool'u 5 bağlantıyla, bekleme süresini 3 saniyeyle sınırlayıp aynı anda 8 istek gönderiyoruz. Production'daki 100'lük pool'un 300 istek karşısındaki hâlinin küçük ölçekli kopyası:

string cs = "Server=localhost;Database=Bookstore;Trusted_Connection=True;TrustServerCertificate=True;" +
            "Max Pool Size=5;Connect Timeout=3";

Stopwatch clock = Stopwatch.StartNew();
Task<string>[] calls = [.. Enumerable.Range(1, 8).Select(i => RunSearchAsync(i, cs, clock))];
foreach (string line in await Task.WhenAll(calls))
    Console.WriteLine(line);

static async Task<string> RunSearchAsync(int id, string cs, Stopwatch clock)
{
    try
    {
        await using SqlConnection connection = new(cs);
        await connection.OpenAsync();
        // yavaş arama sorgusunun yerine 5 saniyelik bekleme
        await using SqlCommand command = new("WAITFOR DELAY '00:00:05'", connection);
        await command.ExecuteNonQueryAsync();
        return $"  request {id}: ok after {clock.Elapsed.TotalSeconds:0.0} s";
    }
    catch (InvalidOperationException ex)
    {
        return $"  request {id}: failed after {clock.Elapsed.TotalSeconds:0.0} s -> {ex.Message}";
    }
}
  request 1: ok after 5.2 s
  request 2: ok after 5.3 s
  request 3: ok after 5.3 s
  request 4: ok after 5.4 s
  request 5: ok after 5.5 s
  request 6: failed after 3.0 s -> Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached.
  request 7: failed after 3.0 s -> Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached.
  request 8: failed after 3.0 s -> Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached.

(Koşturduğumuz kopyada connection string kendi test container'ımızı gösteriyor; geri kalan kod aynı.) Açılıştaki tahminin cevabı bu: ilk tükenen kaynak CPU ya da thread değil, veritabanı connection pool'u, ve loglarda tekrar eden satır bu mesaj. Beş istek bağlantı alıp sorgusunu bitirdi; diğer üçü sorguya hiç başlayamadan 3. saniyede düştü. CPU'nun %40'ta durması da şaşırtıcı değil: bekleyen istekler await içinde, iş yapmıyorlar.

Deep dive'ın değeri burada: "daha büyük sunucu alalım" önerisi bu problemi çözmez, çünkü darboğaz sunucunun gücü değil, yavaş sorgunun bağlantıyı tutma süresi. Çözüm seçenekleri hesaptan doğrudan çıkıyor: sorgunun süresini düşürmek (index, sorguyu kampanya öncesi hazırlamak), aynı sorguyu her istekte çalıştırmamak (cache; bu serinin "Caching Stratejileri: Cache-Aside, Write-Through, Write-Behind ve Cache Stampede" yazısı), pool'u büyütmek (veritabanının kaldırabileceği kadar; bedava değil) ve fazla yükü erken reddetmek (load shedding; serinin reliability yazısında).

Adım 4: Trade-off'ları Söylemek ve Toparlamak

Son adımda yaptığın seçimleri ve bedellerini yüksek sesle söylüyorsun. Kampanya günü için önerim şöyle olurdu: "en çok satanlar" listesini veritabanından değil, dakikada bir yenilenen bir cache'ten oku; bedeli, listenin en fazla bir dakika eski olması ve pazarlama ekibi bunu kabul ediyor. Arama sorgusuna eksik index'i ekle; bedeli, yazma işlemlerinde küçük bir ek maliyet. Pool'u ancak veritabanı tarafında yer varsa büyüt. Ödeme tarafına dokunma; hesap orada sorun olmadığını gösteriyor.

Toparlarken bir de sistemin sonraki darboğazını söylemek iyi bir alışkanlık: trafik 10 değil 30 katına çıkarsa tek SQL Server'ın yazma kapasitesi ve tek uygulama instance'ı sorun olur; bunlar serinin "Tek Sunucudan Milyonlarca Kullanıcıya: Vertical ve Horizontal Scaling, Stateless Tier" ve "Veri Katmanını Ölçeklemek: Replication, Sharding ve SQL mi NoSQL mi?" yazılarının konusu. Tek doğru cevap yok; Mimariye Giriş serisinin "Trade-off Düşüncesi: Mimaride Doğru Cevap Yok, En Az Kötü Seçenek Var" yazısındaki gibi, en az kötü seçeneği gerekçesiyle seçiyorsun.

Interview ile Gerçek Proje Arasındaki Fark

Interview'da kararlarını 45 dakika içinde konuşarak verirsin ve sayıların çoğu varsayımdır. Gerçek projede üç şey değişir. Birincisi ölçmek: varsayımın yerini production metrikleri alır; kampanya öncesi bir load testing, Little's law'un söylediğini ölçerek doğrular. İkincisi yazmak: high-level design ve trade-off'lar bir design doc'a, tek tek kararlar ADR'lara (Architecture Decision Record) girer; ilerideki Architecture Decision'lar serisi bunu anlatıyor. Üçüncüsü zaman: interview'da tek seferde "son hâli" çizersin, gerçekte sistem adım adım büyür ve her adım bir öncekinin darboğazını çözer.

Bir benzerlik de var: iki ortamda da önce soru sormak, sonra çizmek. Interview'da soru sormayan aday "requirement'ı anlamadı" puanı alır; projede soru sormayan ekip yanlış problemi çözen güzel bir mimari kurar.

Sık Düşülen Tuzaklar

Requirement sormadan çizmeye başlamak. "Bir e-ticaret sitesi tasarla" cümlesi duyulur duyulmaz Kafka ve Kubernetes çizilen bir tahta, kampanya gününün asıl problemi olan tek bir yavaş sorguyu hiç görmez.

Tek doğru cevap aramak. Aynı soruya iki iyi cevap verilebilir; değerlendirilen şey seçtiğin cevap değil, alternatifleri bilmen ve bedelini söyleyebilmen.

Her kutuya eşit zaman harcamak. Deep dive'ın konusu problemi taşıyan component; load balancer algoritmasını beş dakika anlatıp veritabanını hiç açmamak, zamanı yanlış yere koymak demek.

Sayısız tasarlamak. "Çok trafik var" bir requirement değil. Saniyedeki istek, latency hedefi ve availability hedefi olmadan cache mi yoksa replica mı sorusunun cevabı verilemez.

Ortalama ile konuşmak. Kullanıcıyı kızdıran ortalama değil, yavaş taraftaki istekler; hedefleri p95 ya da p99 ile yazmak gerekiyor. Bir sonraki yazıda bu farkı sayılarla göreceğiz.

Küçük Bir Deneme

★☆☆ Örnekte Max Pool Size'ı 8'e çıkar ve aynı 8 isteği gönder. Sonra 16 istek gönder. Hangi istekler düştü ve ilk denemeye göre toplam süre nasıl değişti? Sonucu Little's law ile açıkla.

★★☆ Bookstore'un checkout akışı için Adım 1'deki gibi bir varsayım listesi yaz: functional requirement'lar, üç NFR ve "out of scope" satırı. Sonra checkout'un ödeme sağlayıcısını 5 saniyelik timeout ile senkron çağırmasının zirvede kaç eşzamanlı istek ürettiğini hesapla.

★★★ Kampanya günü için iki alternatif çözüm yaz (ör. "en çok satanlar"ı cache'ten okumak ve okuma için ayrı bir read replica eklemek). Her biri için çözdüğü darboğazı, bedelini ve geri dönüş maliyetini tek paragrafla ADR formatında yaz.

Bu yazıda hesabın kendisi tek bir formüldü. Bir sonraki yazıda back-of-the-envelope estimation'ı sistematik hale getiriyoruz: DAU'dan (Daily Active Users) peak QPS'e (queries per second), storage'dan bandwidth'e ve availability'nin "nines" hesabına kadar, iki gerçek ürün için uçtan uca bir tablo.