Sistem tasarımı

URL kısaltıcı

Orta~45 dkAmazonGoogleMetaMicrosoftLinkedIn
Cevaplar:

Problem

"bit.ly benzeri bir servis tasarla: kullanıcı uzun bir URL verir, sistem kısa bir kod üretir. Kısa koda gelen istek uzun URL'ye yönlendirilir. Link sahibi tıklama istatistiklerini de görmek istiyor."

Soru masum görünür; çoğu aday beş dakikada "hash al, veritabanına yaz, cache koy" diyip bitirir. Mülakatçının asıl ölçtüğü şeyler daha incedir:

  • ID üretimi: Kısa, benzersiz, tahmin edilemeyen kodu çok sunuculu bir ortamda koordinasyonsuz ama çakışmasız nasıl üretirsin?
  • Okuma ağırlıklı sistem: Yönlendirme yolu milisaniyeler içinde ve çok yüksek erişilebilirlikle cevap vermeli; cache katmanlarını, hot key'i ve negatif cache'i bilmek.
  • HTTP semantiği: 301 ile 302 arasındaki fark bir "detay" değil, analytics ve link yönetimi özelliklerinin var olup olmamasını belirleyen bir karardır.
  • Kapsam yönetimi: Analytics, yazma yolunun on katı büyüklüğünde bir veri akışıdır; yönlendirmeyi yavaşlatmamalıdır.

Netleştirme soruları ve varsayımlar

SoruNeden önemliVarsayım
Günde kaç yeni kısa link oluşturuluyor?Kod uzunluğu, depolama, ID üretim hızı100 milyon/gün
Okuma/yazma oranı nedir?Cache boyutu, okuma kapasitesi10:1 (günde 1 milyar yönlendirme)
Kod ne kadar kısa olmalı, hangi karakterler?Kod uzayı ve uzunluk hesabıMümkün olan en kısa; [0-9A-Za-z] (base62)
Kullanıcı kendi alias'ını seçebilir mi?Benzersizlik kontrolü ve üretilen kodlarla çakışmaEvet, opsiyonel
Aynı uzun URL iki kez kısaltılırsa aynı kodu mu almalı?Dedup maliyeti, kampanya bazlı istatistikHayır; her istek yeni kod (istenirse sahip bazında dedup)
Linkler silinebilir, hedefi değiştirilebilir, süresi dolabilir mi?301/302 kararı, cache invalidationEvet; opsiyonel expiresAt
Linkler ne kadar saklanacak?Depolama hesabı10 yıl
İstatistik gerçek zamanlı mı olmalı?Senkron mu asenkron muBirkaç dakika gecikme kabul
Kodlar tahmin edilebilir olursa sorun mu?Sıralı sayaç kullanılabilir miEvet sorun: özel dokümanlara verilen linkler taranmamalı

Fonksiyonel olmayan gereksinimler: yönlendirme yolu için %99,99 erişilebilirlik ve sunucu tarafında p99 birkaç on milisaniyenin altında; oluşturma yolu için %99,9 yeterli. Yönlendirme, analytics'ten daha önemlidir: analytics bozulursa yönlendirme çalışmaya devam etmeli.

Kapsam dışı: kullanıcı yönetimi, faturalandırma, QR kod, link önizleme sayfası. Bunları sesli söylemek "unuttum" değil "kapsamı yönetiyorum" mesajı verir.

Kapasite tahmini

text
Yazma   : 100.000.000 / 86.400 s        ≈ 1.160 /s ortalama, tepe (x3) ≈ 3.500 /s
Okuma   : 10 x 100M = 1 milyar /gün     ≈ 11.600 /s ortalama, tepe (x3) ≈ 35.000 /s
10 yılda: 100M x 365 x 10               = 365 milyar kayıt

Kod uzayı (base62):
  62^6 ≈ 56,8 milyar      -> 365 milyara yetmez
  62^7 ≈ 3,52 trilyon     -> 365 milyarın ~9,6 katı, 7 karakter yeter

Kayıt boyutu: kod 7 B + uzun URL ort. 100 B + sahip, zaman damgaları ~50 B
              + depolama motoru ve index yükü  ≈ 250 B
Depolama   : 365 milyar x 250 B ≈ 91 TB, replikasyon faktörü 3 ile ≈ 274 TB

Cache      : günlük okumaların %20'sini oluşturan URL'ler için üst sınır
             1 milyar x 0,2 x 250 B ≈ 50 GB (aynı URL tekrar okunduğu için gerçekte daha az)
Analytics  : 1 milyar tıklama/gün x ~200 B ≈ 200 GB/gün ham event
Bant       : 11.600 /s x ~500 B (302 cevabı + header) ≈ 5,8 MB/s

Bu sayılardan çıkan teşhis, mülakatta söylenecek asıl cümledir:

  1. Okuma ağırlıklı ve okumalar tek anahtarla. Join yok, aralık sorgusu yok; anahtar-değer erişimi. Cache isabet oranı her şeydir.
  2. Yazma hızı küçük (binler/s), ama 10 yıllık veri yüzlerce TB. Tek bir PostgreSQL instance'ı değil, yatay bölünebilen bir depo gerekir.
  3. En büyük yazma akışı analytics'tir (günde 1 milyar event, link oluşturmanın 10 katı). Senkron yazılırsa yönlendirme yolunu boğar; asenkron olmalı.

API

http
POST /api/v1/urls
Idempotency-Key: 6f1c...
Content-Type: application/json

{ "longUrl": "https://www.ornek.com/kampanya?utm_source=x", "customAlias": "kasim-indirim", "expiresAt": "2027-01-01T00:00:00Z" }

-> 201 Created   { "code": "aZ3kP9q", "shortUrl": "https://kisa.lt/aZ3kP9q" }
-> 400 Bad Request          geçersiz URL, desteklenmeyen şema
-> 409 Conflict             alias kullanımda
-> 429 Too Many Requests    oluşturma limiti (bkz. rate-limiter case'i)

GET /{code}
-> 302 Found        Location: https://www.ornek.com/kampanya?utm_source=x
-> 404 Not Found    böyle bir kod yok
-> 410 Gone         silinmiş veya süresi dolmuş

GET    /api/v1/urls/{code}/stats?from=2026-10-01&to=2026-10-31&granularity=day
DELETE /api/v1/urls/{code}

Tasarım notları:

  • Yönlendirme endpoint'i ayrı, kısa bir domain'de (kisa.lt) ve path'in kökünde durur. Kimlik doğrulama yoktur; her milisaniye önemlidir.
  • Oluşturma POST'u idempotency key alır: mobil istemci timeout sonrası tekrar denerse ikinci bir kod üretilmez (Kitap Bölüm 18).
  • 410 Gone, "bu link vardı ama artık yok" bilgisini 404'ten ayırır; tarayıcı ve arama motorları için anlamlıdır.

Veri modeli

Erişim deseni tek anahtarla okuma olduğu için doğal seçim geniş sütunlu ya da anahtar-değer bir depodur (Cassandra, DynamoDB benzeri). Ekip PostgreSQL'de uzmansa, kod üzerinden hash ile bölünmüş PostgreSQL shard'ları da işi görür; önemli olan partition anahtarının code olmasıdır.

sql
-- Cassandra (CQL)
CREATE TABLE urls (
    code        text PRIMARY KEY,      -- partition key: yük kodlar arasında eşit dağılır
    long_url    text,
    owner_id    bigint,
    created_at  timestamp,
    expires_at  timestamp,
    status      text                    -- ACTIVE, DELETED
);

-- "Linklerim" sayfası için ayrı bir sorgu tablosu (denormalizasyon)
CREATE TABLE urls_by_owner (
    owner_id    bigint,
    created_at  timestamp,
    code        text,
    long_url    text,
    PRIMARY KEY ((owner_id), created_at, code)
) WITH CLUSTERING ORDER BY (created_at DESC, code ASC);
  • Üretilen kodlar yapısı gereği benzersizdir (aşağıda); düz INSERT yeterlidir.
  • Custom alias ise kullanıcıdan gelir ve çakışabilir. Cassandra'da koşullu yazma (INSERT ... IF NOT EXISTS) Paxos tabanlı lightweight transaction kullanır ve normal yazmadan belirgin şekilde pahalıdır; ama sadece alias yolunda kullanıldığı için kabul edilir.
  • Üretilen kodlarla alias'ların çakışmaması için alias'lar ayrı bir biçimle sınırlanır (ör. en az 8 karakter ya da - içermeli). Yine de koşullu yazma son savunmadır.
  • Tıklama event'leri bu depoya yazılmaz; analytics için ayrı, kolon tabanlı bir OLAP deposu (ClickHouse, Druid benzeri) kullanılır.

Yüksek seviye tasarım

diyagram yükleniyor…
flowchart LR
    C[İstemci] --> EDGE[CDN ve edge]
    EDGE --> LB[Load balancer]
    LB --> RS[Redirect servisi]
    LB --> WS[Kısaltma servisi]
    WS --> SAFE[Zararlı URL kontrolü]
    WS --> IDS[ID blok servisi]
    WS --> DB[(URL deposu)]
    RS --> LC[Pod içi cache]
    RS --> RC[(Redis cache)]
    RS --> DB
    RS -->|click event, asenkron| K[(Kafka)]
    K --> AGG[Analytics consumer]
    AGG --> OLAP[(OLAP deposu)]

Yazma yolu: İstek doğrulanır (şema sadece http/https, uzunluk sınırı), zararlı URL kontrolünden geçer, kısaltma servisi kendi belleğindeki ID bloğundan bir sayı alır, base62'ye çevirir, depoya yazar ve cevabı döner.

Okuma yolu: Redirect servisi önce pod içi cache'e, sonra Redis'e, en son depoya bakar; bulduğu hedefle 302 döner ve tıklama event'ini bloklamadan Kafka'ya bırakır.

Okuma ve yazma servisleri ayrı deploy edilir: farklı ölçeklenirler (okuma yazmanın 10 katı), farklı erişilebilirlik hedefleri vardır ve yazma tarafındaki bir hata yönlendirmeyi etkilememelidir.

Derinlemesine

ID üretimi: dört seçenek

1. Uzun URL'nin hash'i. SHA-256(longUrl)'nin ilk bitlerini base62'ye çevirip 7 karakter almak. Artısı: aynı URL aynı kodu alır (dedup bedava). Eksileri: 7 karaktere kesilmiş hash çakışır; her yazmada "bu kod başka bir URL'ye ait mi?" diye okumak, çakışırsa tuz ekleyip tekrar denemek gerekir. Ayrıca iki farklı kampanyanın aynı URL için ayrı istatistik istemesi mümkün olmaz.

2. Rastgele kod + koşullu ekleme. 7 rastgele base62 karakter üret, IF NOT EXISTS ile ekle, çakışırsa tekrar dene. Tahmin edilemez, koordinasyon gerektirmez. Ama çakışma olasılığı doluluk oranıyla büyür: 365 milyar kod kullanılmışken yeni bir denemenin çakışma olasılığı 365 milyar / 3,52 trilyon ≈ %10,4'tür. Her yazma da pahalı koşullu yazma olur. 8 karaktere çıkarsan (62^8 ≈ 218 trilyon) olasılık ≈ %0,17'ye düşer; bedeli bir karakter daha uzun link.

3. Sayaç + base62. Her yeni link için artan bir sayı al, base62'ye çevir. 62^7 uzayı tam doldurulabilir, çakışma imkansızdır. Sorun, sayacın kendisidir: tek bir veritabanı sequence'ine her istekte gitmek hem darboğaz hem tek hata noktasıdır. Çözüm blok tahsisi: bir ID servisi (ya da veritabanında tek satırlık bir sayaç) her kısaltma pod'una 10.000'lik bloklar verir; pod bloğu bellekte tüketir.

sql
-- ID blok servisinin tek işi: atomik olarak bir blok ayırmak
UPDATE id_blocks
   SET next_value = next_value + 10000
 WHERE name = 'short-url'
RETURNING next_value - 10000 AS block_start;

Tepe yazma 3.500/s iken 10.000'lik bloklarla tüm filoda yaklaşık 3 saniyede bir blok istenir: sayaç artık darboğaz değildir. Pod bloğun yarısını tükettiğinde bir sonrakini önceden alır; ID servisi birkaç dakika çökse bile yazma devam eder. Pod çökerse bloğun kalanı kaybolur; kodlarda boşluk olması sorun değildir.

Sıralı sayacın kalan sorunu tahmin edilebilirliktir: aZ3kP9q'yu gören biri aZ3kP9r'yi deneyerek başkalarının linklerini tarayabilir. Çözüm, sayacı 62^7 uzayında gizli anahtarlı bir permütasyondan geçirmektir: her sayı uzayda başka bir sayıya, çakışmasız (bijektif) eşlenir. Basit bir afin dönüşüm (a * x + b) mod 62^7 ancak a modülle aralarında asalsa bijektiftir (62^7 = 2^7 x 31^7 olduğu için a tek ve 31'e bölünmeyen bir sayı olmalı), ama kolay tersine çevrilir. Daha sağlamı, anahtarlı birkaç turluk bir Feistel ağı gibi format koruyan bir şifrelemedir. Mülakatta "sayaç + gizli permütasyon" demek yeterli; matematiğine inmek bonus.

4. Snowflake tarzı 64 bit ID. Twitter'ın 2010'da duyurduğu Snowflake, ID'yi zaman damgası, worker numarası ve sıra numarasından oluşturur; amaç saniyede on binlerce ID'yi koordinasyonsuz, kabaca zamana göre sıralı ve 64 bite sığacak şekilde üretmektir. Yaygın bilinen düzen: 41 bit milisaniye (yaklaşık 69 yıl), 10 bit makine (1.024 worker), 12 bit sıra (worker başına milisaniyede 4.096 ID). Kısa link için uygun değildir: 63 bitlik bir sayı base62'de 11 karakter tutar (62^10 ≈ 8,4 x 10^17, 2^63 ≈ 9,2 x 10^18). Ayrıca saat geri giderse (NTP düzeltmesi) aynı ID üretilebilir; implementasyonlar bu durumda bekler ya da hata verir. Snowflake, sistemin iç ID'leri (tıklama event'i, kullanıcı) için iyi bir seçimdir.

YöntemUzunlukÇakışmaKoordinasyonTahmin edilebilir mi
Hash + kesme7Var, doluluğa göre artarYok, ama her yazmada okumaHayır
Rastgele + koşullu ekleme7-8Var, retry ile çözülürYok, koşullu yazma pahalıHayır
Blok sayaç + base627YokBlok başına bir çağrıEvet, permütasyonla hayır
Snowflake11Yok (saat doğruysa)Worker id atamasıKısmen (zaman sızar)

Seçimim: blok tahsisli sayaç + gizli permütasyon + base62, sabit 7 karakter. Kod uzayının tamamı kullanılır, koşullu yazma gerekmez, ID servisi kritik yolda değildir.

java
public final class Base62 {

    private static final char[] ALPHABET =
            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();

    private Base62() {
    }

    /** value'yu tam olarak length karakterlik base62 dizgesine çevirir (baştaki sıfırlar korunur). */
    public static String encode(long value, int length) {
        if (value < 0) {
            throw new IllegalArgumentException("negatif değer: " + value);
        }
        char[] out = new char[length];
        for (int i = length - 1; i >= 0; i--) {
            out[i] = ALPHABET[(int) (value % 62)];
            value /= 62;
        }
        if (value != 0) {
            throw new IllegalArgumentException("değer " + length + " karaktere sığmıyor");
        }
        return new String(out);
    }
}

Yönlendirme: 301 mi, 302 mi?

301 Moved Permanently302 Found
AnlamKaynak kalıcı olarak taşındıGeçici yönlendirme
Tarayıcı cache'iKalıcı yönlendirme olarak cache'lenebilir; açık Cache-Control olmasa da HTTP spesifikasyonu 301'i sezgisel (heuristic) olarak cache'lenebilir sayarVarsayılan olarak her tıklamada sunucuya sorulur
Sunucu yüküTekrarlayan tıklamalar sunucuya hiç gelmeyebilirHer tıklama sunucuya gelir
AnalyticsTekrarlayan tıklamalar görünmezHer tıklama sayılır
Hedefi değiştirmek, silmek, süre sonuCache'lemiş tarayıcılarda etkisizAnında etkili
SEOArama motorları hedefi kanonik URL kabul eder, link değerini hedefe aktarırGeçici sayılır; kanonik olarak genelde kısa URL tutulur

POST isteğine gelen 301/302'de tarayıcılar metodu GET'e çevirebilir; metodun korunması gerekiyorsa 308/307 kullanılır. Kısa link tıklaması zaten GET olduğu için bu fark burada önemsizdir.

Karar: Gereksinimde analytics, silme ve süre sonu var; bu yüzden 302 (ya da 307) ve Cache-Control: private, max-age=0. Sadece trafik maliyetini düşürmek isteyen ve analytics istemeyen bir ürün için 301 mantıklıdır. Arada bir seçenek: CDN'de 60 saniyelik kısa TTL ile yönlendirmeyi edge'de cache'lemek ve tıklamaları CDN loglarından saymak. Bu, origin yükünü dramatik düşürür ama silme en fazla 60 saniye gecikir ve analytics hattı CDN log formatına bağlanır.

Okuma yolu: üç katmanlı cache

  1. Pod içi cache (Caffeine): En sıcak birkaç yüz bin kod; mikro saniyeler. Viral bir link Redis'teki tek bir shard'ı boğmadan burada emilir.
  2. Redis (cache-aside): Kod → {longUrl, expiresAt, status}. Eşleme neredeyse hiç değişmediği için TTL uzun tutulur (ör. 24 saat + jitter); değişiklikte (silme, hedef güncelleme) anahtar commit sonrası silinir (Kitap Bölüm 11). Bellek politikası allkeys-lru ya da allkeys-lfu: cache olarak kullanılan Redis'te noeviction yazmaları hataya düşürür.
  3. Depo: Cache ıskalamasında tek anahtar okuması.

Dikkat edilecek üç durum:

  • Negatif cache: Bot'lar ve yanlış yazılmış linkler var olmayan kodlar için gelir; her biri cache'i ıskalayıp depoya gider (cache penetration). "Yok" sonucu kısa TTL ile (ör. 60 saniye) cache'lenir. Alternatif: var olan kodların Bloom filter'ı.
  • Süre sonu: expiresAt cache'teki değerin içinde durur; redirect servisi kontrol edip 410 döner. Süresi dolan kayıtların fiziksel silinmesi ayrı bir temizlik işidir. Cassandra'da satır TTL'i kullanılabilir, ama silinen her satırın tombstone ürettiğini ve compaction'a kadar okuma maliyeti yarattığını unutma.
  • Cache-miss fırtınası: Redis düşerse tüm okuma depoya iner (11.600/s ortalama, tepe 35.000/s). Depo bu yükü kaldıracak şekilde boyutlandırılır ya da aynı kod için eşzamanlı ıskalamalar tek bir okumada birleştirilir (request coalescing).

Yazma yolu

  • URL doğrulama: Sadece http ve https; javascript: gibi şemalar reddedilir. Uzunluk sınırı (ör. 2.048 karakter).
  • Zararlı URL kontrolü: Bilinen phishing/malware listelerine karşı kontrol; şüpheli linkler oluşturulur ama bir uyarı sayfasına yönlendirilir. Liste sonradan güncellenirse var olan kodlar da işaretlenir (yine 302'nin bir faydası).
  • Idempotency: Idempotency-Key + kullanıcı kapsamında unique kayıt; aynı anahtarla gelen ikinci istek ilk kodu döner.
  • Dedup (opsiyonel): Aynı sahip aynı URL'yi tekrar kısaltırsa eski kodu dönmek için (owner_id, hash(long_url)) üzerinde bir index. Global dedup istenmez: farklı kişilerin istatistikleri karışır.

Analytics: yönlendirmeyi yavaşlatmadan saymak

Redirect servisi her tıklamada bir event üretir: code, zaman, ülke (IP'den türetilmiş), cihaz tipi, referrer. Ham IP saklanmaz ya da kısaltılarak/hash'lenerek saklanır; kişisel veri (KVKK, GDPR) en baştan tasarıma girer.

  • Asenkron gönderim: Kafka producer zaten batch'leyerek ve asenkron gönderir; uygulama send() sonucunu beklemez. Ama dikkat: tampon (buffer.memory) dolarsa ya da metadata alınamazsa send() varsayılan olarak max.block.ms kadar (60 saniye) bloklar. Bu yüzden max.block.ms çok küçük tutulur ve hata yakalanır: tampon dolunca event düşürülür ve bir metrik artar; yönlendirme bloklanmaz. Analytics best-effort'tur; yönlendirme değildir. Daha sıkı bir gereksinim varsa pod'da lokal disk tamponu kullanılır.
  • Partition anahtarı: Sıra gerekmediği için anahtar null ya da rastgele. code anahtar olursa viral link tek bir partition'ı ve tek bir consumer'ı ezer.
  • Toplama: Bir stream consumer event'leri dakika bazında (code, dakika, ülke) sayaçlarına toplar ve OLAP'a yazar; ya da OLAP Kafka'dan ham event'leri doğrudan alıp kendi rollup'larını yapar. Tekil ziyaretçi sayısı için HyperLogLog gibi olasılıksal bir yapı (Redis'te %0,81 standart hatayla, kod başına en fazla 12 KB) yeterlidir.
  • Bot filtreleme: Link önizleme bot'ları (sohbet uygulamaları linki paylaşınca otomatik açar) tıklama sayısını şişirir; user-agent ve davranış kurallarıyla ayrılır.
  • Idempotency gerekmez mi? Kafka en az bir kez teslim eder; aynı event iki kez sayılabilir. İstatistik için ±%0,1 hata çoğu zaman kabul edilir; faturalandırma bu sayılara bağlıysa event'e Snowflake ID verilip toplama sırasında tekilleştirilir.

Çok bölgeli dağıtım

Yönlendirme okuma yoludur ve kod → URL eşlemesi neredeyse değişmez; her bölgede bir okuma kopyası ve cache tutmak kolaydır. Yazmalar tek bir "ev" bölgesine gidebilir; asenkron replikasyon nedeniyle yeni oluşturulan link birkaç yüz milisaniye diğer bölgede görünmeyebilir. Çözüm: oluşturan kullanıcıya dönen ilk yönlendirmeyi ev bölgesinden okumak ya da diğer bölgede ıskalama olursa ev bölgesine sormak (read-repair benzeri). ID blokları bölge başına ayrı aralıklardan verilir; bölgeler arası koordinasyon gerekmez.

Darboğazlar, hatalar ve trade-off'lar

SenaryoEtkiÖnlem
Redis cluster düşerTüm okuma depoya inerPod içi cache, depoyu cache'siz tepe yüke göre boyutlandırmak, request coalescing, circuit breaker
Viral link (hot key)Tek Redis shard'ı ve tek depo partition'ı ısınırPod içi cache, CDN'de kısa TTL, gerekirse anahtarı kopyalamak
ID blok servisi düşerYeni link oluşturulamazPod'larda önceden alınmış ikinci blok; dakikalarca tampon
Kafka yavaşlarProducer tamponu dolarEvent düşür + metrik; yönlendirme etkilenmez
Bot'lar var olmayan kodları tararCache penetrationNegatif cache, Bloom filter, IP bazlı rate limit
Saat geri gider (Snowflake)Tekrarlanan IDSaat geriye giderse üretimi durdur/bekle; kısa kod için zaten sayaç kullanılıyor
Zararlı link yayılırMarka ve kullanıcı güvenliğiOluşturmada kontrol, sonradan işaretleme, 302 sayesinde anında devre dışı bırakma

Bilinçli trade-off'lar:

  • 302 ve sunucu yükü: Analytics ve link yönetimi için her tıklamayı sunucuda karşılamayı seçtik; bedeli daha fazla trafik.
  • Sayaç ve tahmin edilebilirlik: Kod uzayını verimli kullanmak için sayaç seçtik; tahmin edilebilirliği permütasyonla kapattık.
  • Analytics best-effort: Yönlendirmenin erişilebilirliği için event kaybını kabul ettik ve ölçüyoruz.
  • Tek ev bölgesi: Yazma tutarlılığı basit; yeni linkin diğer bölgede görünmesi birkaç yüz milisaniye gecikebilir.

Üç seviyede cevap

Soru: "Kısa kodu nasıl üretirsin? İki sunucu aynı kodu üretmesin."

Uzun URL'nin MD5 hash'ini alıp ilk 7 karakterini kullanırım. Aynı URL hep aynı kodu verir, çakışma olursa veritabanındaki unique constraint hata verir.

Hash'i kesmek çakışma üretir ve her yazmada kontrol gerektirir. Bunun yerine artan bir sayı alıp base62'ye çeviririm; 62^7 yaklaşık 3,5 trilyon, 10 yıllık 365 milyar link için yeter. Tek bir sequence darboğaz olmasın diye her sunucuya 10.000'lik bloklar veren bir ID servisi kullanırım; sunucular bloğu bellekte tüketir, çakışma imkansızdır.

Önce kod uzayını hesaplarım: 365 milyar link için 6 karakter (56 milyar) yetmez, 7 karakter (3,5 trilyon) yeter. Dört seçenek var: hash, rastgele, sayaç, Snowflake. Snowflake 64 bit olduğu için base62'de 11 karakter tutar, kısa link için uzun; iç ID'ler için kullanırım. Rastgele kod tahmin edilemez ama doluluk arttıkça çakışma artar, 365 milyarda her denemede yaklaşık yüzde 10, ve her yazma koşullu yazma olur. Seçimim blok tahsisli sayaç: ID servisi atomik bir UPDATE ... RETURNING ile 10.000'lik blok verir, tepe 3.500 yazma/s'de bu üç saniyede bir istek demek, pod'lar bir sonraki bloğu önceden alır, yani ID servisi kritik yolda değil. Sıralı sayaç linkleri taranabilir yapar; sayıyı 62^7 uzayında gizli anahtarlı bir permütasyondan geçiririm, böylece çakışmasızlık korunur ama ardışık kodlar ilişkisiz görünür. Custom alias ayrı bir biçimle sınırlanır ve koşullu yazmayla eklenir.

Junior çakışmayı veritabanına havale ediyor ama 7 karaktere kesilmiş hash'in ne sıklıkla çakışacağını ve bunun her yazmada okuma demek olduğunu düşünmüyor. Mid doğru mekanizmayı (blok tahsisi) buluyor. Senior seçenekleri sayılarla eliyor (Snowflake'in 11 karakteri, rastgelenin %10 çakışması), ID servisini kritik yoldan çıkarıyor ve sıralı sayacın güvenlik bedelini görüp kapatıyor. Fark, tek bir çözümü bilmek ile seçenekleri ölçülebilir kriterlerle karşılaştırmak arasında.

Takip sorusu: ID servisi bir blok verdi, pod bloğun yarısını kullanıp çöktü. Ne kaybedersin, bu bir problem mi?

Soru: "Yönlendirmede 301 mi döndürürsün, 302 mi?"

301 döndürürüm çünkü link kalıcı, SEO açısından da doğrusu bu.

301'i tarayıcı cache'ler; ikinci tıklama sunucuya gelmez ve istatistik kaybolur. İstatistik istendiği için 302 döndürürüm. 301 sunucu yükünü azaltır, analytics istenmiyorsa onu seçerim.

Bu bir ürün kararı, gereksinime bağlarım. 301 kalıcıdır ve açık cache başlığı olmasa bile cache'lenebilir; tekrar eden tıklamalar sunucuya gelmez, yani analytics eksik kalır, linki silmek veya hedefini değiştirmek o tarayıcılarda etkisiz olur, zararlı çıkan bir linki kapatamam. Gereksinimde analytics, süre sonu ve silme olduğu için 302 ve Cache-Control: private, max-age=0 döndürürüm. Yükü azaltmak gerekirse ara çözüm CDN'de 60 saniyelik TTL ile edge cache: origin yükü düşer, silme en fazla bir dakika gecikir, tıklamaları CDN loglarından sayarım. Metot korunması gereken bir senaryo olsaydı 307/308 kullanırdım, ama kısa link tıklaması zaten GET.

Junior ezber bir cevap veriyor ve analytics gereksinimini kaçırıyor. Mid temel trade-off'u biliyor. Senior kararı gereksinimlere (analytics, silme, güvenlik) bağlıyor, cache davranışının sonuçlarını sayıyor ve iki uç arasında edge cache gibi üçüncü bir seçenek öneriyor. Mülakatçı burada "doğru cevap"tan çok kararın gerekçesini dinler.

Takip sorusu: Pazarlama ekibi "301'e geçelim, trafik maliyeti yarıya iner" diyor. Neyi kaybedeceğimizi nasıl ölçer ve nasıl karar verirsin?

Redis cache'te olduğu için sorun olmaz, Redis çok hızlı.

Tek bir anahtar olduğu için Redis Cluster'da tek bir node'a gider, o node ısınır. Pod içinde Caffeine ile kısa ömürlü yerel cache koyarım; istekler Redis'e hiç gitmez. Analytics tarafında da Kafka key'i code olursa tek partition ısınır, key'siz gönderirim.

Üç yerde etkisi olur: cache, depo ve analytics. Cache'te tek anahtar tek slot'a, tek node'a düşer; pod içi cache bunu emer, viral linkte isabet oranı neredeyse yüzde yüz olur ve Redis'e pod başına saniyede bir istek bile gitmez. Gerekirse CDN'de kısa TTL'li edge cache ile istek origin'e hiç gelmez. Depo cache ıskalamasında sadece bir kez okunur; aynı anahtar için eşzamanlı ıskalamaları tek yükleyicide birleştiririm. Asıl sürpriz analytics'te: event'leri code ile key'lersem 100 bin event/s tek partition'a ve tek consumer'a gider, lag büyür. Sıra gerekmediği için key'siz gönderir, toplamayı consumer'da yaparım. Bu senaryoyu yük testinde tek anahtar profiliyle denerim; ortalama dağılımlı test bunu göstermez.

Junior "Redis hızlı" diyerek hot key kavramını kaçırıyor. Mid cache tarafını doğru çözüyor. Senior aynı problemin cache, depo ve analytics hattında farklı yüzlerini görüyor ve özellikle Kafka partition anahtarının yarattığı ikinci darboğazı yakalıyor. Ayrıca bunun ortalama dağılımlı yük testinde görünmeyeceğini söylemesi operasyonel olgunluk sinyali.

Takip sorusu: Pod içi cache'i olan bir linki sahibi sildi. Silme kaç saniyede tüm pod'lara yansır, bunu nasıl kısaltırsın?

Mülakatçı için

  • Kapasite hesabına geçmeden önce yazma hacmi, okuma/yazma oranı, saklama süresi ve analytics gereksinimini soruyor.
  • Kod uzayını hesaplayıp 7 karakteri gerekçelendiriyor (62^6 ve 62^7'yi 365 milyarla karşılaştırıyor).
  • En az üç ID üretim yöntemini karşılaştırıyor ve seçimini çakışma, uzunluk ve koordinasyon üzerinden savunuyor.
  • Tek bir global sayacın darboğaz/tek hata noktası olduğunu görüp blok tahsisi gibi bir çözüm öneriyor.
  • Sıralı kodların tahmin edilebilirlik riskinden kendiliğinden bahsediyor.
  • 301/302 kararını analytics, silme ve cache davranışına bağlıyor.
  • Okuma yolunda cache katmanlarını, negatif cache'i ve hot key'i ele alıyor.
  • Analytics'i yönlendirme yolundan ayırıyor (asenkron, best-effort) ve Kafka partition anahtarının etkisini düşünüyor.
  • Okuma ve yazma servislerini ayrı ölçeklenen bileşenler olarak çiziyor.
  • En az iki arıza senaryosu (Redis düşmesi, ID servisi düşmesi) için davranışı anlatıyor.
  • Kişisel veri (IP) ve kötüye kullanım (phishing, tarama) konularına değiniyor.

Takip soruları:

  • Custom alias ile üretilen kodlar nasıl çakışmaz? Koşullu yazmanın maliyeti nedir?
  • Bir link silindi; Redis'te, pod içi cache'te ve CDN'de ne kadar yaşar?
  • Analytics'te "son 5 dakikada en çok tıklanan 10 link" isteniyor. Hattı nasıl değiştirirsin?
  • 10 yılda 274 TB: süresi dolan linkleri nasıl temizlersin, tombstone ne getirir?

Seviye beklentisi: Mid seviyede blok sayaç + base62, cache-aside ve 302 kararı yeterlidir. Senior'dan seçeneklerin sayılarla karşılaştırılması, hot key'in analytics hattındaki etkisi, tahmin edilebilirlik ve arıza modları beklenir. Sadece "hash al, DB'ye yaz, Redis koy" diyen ve sayı kullanmayan aday zayıf sinyal verir.

Hızlı tekrar

S: 365 milyar link için base62 kaç karakter gerekir?

C: 7. 62^6 ≈ 56,8 milyar yetmez; 62^7 ≈ 3,52 trilyon, ihtiyacın yaklaşık 9,6 katı.

S: Global sayacın darboğazını blok tahsisi nasıl çözer?

C: ID servisi her pod'a 10.000'lik blok verir; pod bloğu bellekte tüketir ve sıradakini önceden alır. 3.500 yazma/s'de birkaç saniyede bir istek; ID servisi kritik yolda değildir.

S: Snowflake ID neden kısa link için uygun değil?

C: 64 bitliktir; base62'de 11 karakter tutar. Ayrıca saat geri giderse çakışma riski vardır. İç ID'ler için uygundur.

S: Rastgele 7 karakterlik kodda 365 milyar kod doluyken çakışma olasılığı?

C: Yaklaşık 365 milyar / 3,52 trilyon ≈ %10,4 (deneme başına). 8 karakterde yaklaşık %0,17.

S: 301 ile 302'nin analytics açısından farkı?

C: 301 kalıcıdır ve tarayıcıda cache'lenebilir; tekrarlayan tıklamalar sunucuya gelmez. 302'de her tıklama sunucuya gelir, sayılır; silme ve hedef değişikliği anında etkilidir.

S: Negatif cache neyi çözer?

C: Var olmayan kodlar için gelen isteklerin her seferinde depoya gitmesini (cache penetration). "Yok" sonucu kısa TTL ile cache'lenir.

S: Tıklama event'lerinde Kafka anahtarı neden code olmamalı?

C: Viral link tek partition'ı ve tek consumer'ı ezer. Sıra gerekmediği için anahtarsız gönderilir, toplama consumer'da yapılır.

S: Sıralı sayacın tahmin edilebilirliği nasıl kapatılır?

C: Sayı, 62^7 uzayında gizli anahtarlı bijektif bir permütasyondan (ör. Feistel tabanlı format koruyan şifreleme) geçirilir; çakışmasızlık korunur, ardışık kodlar ilişkisiz görünür.

Kaynaklar

  • Alex Xu, System Design Interview: An Insider's Guide (Cilt 1): "Design a URL Shortener" ve "Design a Unique ID Generator in Distributed Systems" bölümleri.
  • Martin Kleppmann, Designing Data-Intensive Applications: Partitioning ve replikasyon bölümleri.
  • Twitter Engineering: Announcing Snowflake: Zaman damgası + worker + sıra numarasından oluşan, kabaca sıralı 64 bit ID'nin gerekçesi.
  • MDN: 301 Moved Permanently: Kalıcı yönlendirme, SEO etkisi ve metodun korunması için 308.
  • MDN: 302 Found: Geçici yönlendirme, 307 ve 303 alternatifleri.
  • Redis: Key eviction: maxmemory-policy seçenekleri ve yaklaşık LRU/LFU.