Live Coding · Bölüm 20

Sistem tasarımı, code review ve mülakat davranışı

Bir sistem tasarımı sorusunu (flash sale checkout) çerçeveyle çözmeyi, İngilizce anlatmayı, bir PR'ı önceliklendirerek review etmeyi, iyi commit atmayı ve live coding ile davranışsal sorularda nasıl davranılacağını öğretir.

~40 dk okumaQuiz: q20, k6, z11, z13
Cevaplar:

Bu bölümde

Teknik bilgi mülakatın yarısıdır; diğer yarısı o bilgiyi yapılandırılmış ve iş birliğine açık biçimde ortaya koymaktır. Bu bölümde bir sistem tasarımı sorusunu (2 milyon kullanıcılı flash sale checkout) adım adım çözmeyi ve İngilizce anlatmayı, bir PR'ı ciddiyet sırasına göre review etmeyi, iyi commit'in ölçütlerini, live coding oturumunda zamanı ve iletişimi yönetmeyi ve "kendinden bahset", "en zor problemin", "bir incident anlat" gibi davranışsal-teknik soruları STAR çerçevesiyle cevaplamayı öğreneceksin. Bu konular mülakatta açık uçlu tasarım sorusu, ekran paylaşımlı PR inceleme ve oturumun tamamına yayılan davranış değerlendirmesi olarak karşına çıkar.

Konu anlatımı

Sistem tasarımı mülakatının çerçevesi

Sistem tasarımı sorusunun tek doğru cevabı yoktur; değerlendirilen şey belirsizliği nasıl yönettiğin, trade-off'ları görüp görmediğin ve en zor kısma zamanında inip inmediğindir. 45 dakikalık bir oturum için işe yarayan iskelet:

AdımSüreNe yaparsınÇıktı
1. Requirements5 dkFonksiyonel (ne yapacak) ve fonksiyonel olmayan (latency, availability, tutarlılık) gereksinimleri sor; kapsam dışını söyle3-5 maddelik liste
2. Estimates3-5 dkQPS, peak, okuma/yazma oranı, veri boyutuDarboğazın nerede olacağına dair ilk fikir
3. API3-5 dkAna endpoint'ler, idempotency, status kodları3-4 endpoint
4. Data model3-5 dkAna tablolar/anahtarlar, unique constraint'ler, partition anahtarıŞema taslağı
5. High-level design5-10 dkKutular ve oklar: istemci → CDN → gateway → servisler → cache/DB/kuyrukDiyagram
6. Deep dive10-15 dkEn zor 1-2 problem (burada: stok tutarlılığı ve trafik patlaması)Mekanizma düzeyinde çözüm
7. Bottlenecks & failures5 dkTek hata noktası, hot key, failover, backpressure, gözlemlenebilirlik"Ne bozulursa ne olur" listesi

En sık hata 6. adıma hiç gelememektir: 25 dakikayı kutu çizmeye harcayan aday, mülakatçının asıl merak ettiği soruyu ("overselling'i nasıl önlüyorsun?") cevaplayamadan süreyi bitirir. Her adımın sonunda "Bu kısmı burada bırakıp en kritik noktaya, stok tutarlılığına inmek istiyorum; uygun mu?" diye yönlendirme yap.

Kaba hesap (back-of-the-envelope) için akılda tutulacak sayılar:

  • 1 gün ≈ 86.400 saniye ≈ 10⁵ saniye. Günde 10 milyon istek ≈ ortalama 115 rps; peak genelde ortalamanın birkaç katıdır.
  • Aynı veri merkezi içinde bir ağ gidiş-dönüşü milisaniyenin altında; Redis gibi bellekte çalışan bir store'a çağrı tipik olarak ~1 ms civarı; disk ve veritabanı yazması milisaniyeler; kıtalar arası gidiş-dönüş on milisaniyeler ila 100+ ms.
  • Bu sayılar kesin değil, büyüklük mertebesidir. Mülakatta "kaba tahminle" deyip hesabı sesli yapmak, kesin sayıyı bilmekten değerlidir.

Flash sale checkout tasarımı (q20)

Soru: "20:00'de flash sale başlıyor: tek bir telefon modelinden 50.000 adet, büyük indirimle, müşteri başına en fazla 1. İlk dakikada yaklaşık 2 milyon kullanıcının uygulamayı açmasını bekliyoruz. Stok rezervasyonu ve checkout akışını tasarla."

1. Gereksinimler ve netleştirme

Sorulacak sorular ve neden önemli oldukları:

  • "Müşteri başına bir" hesap mı, adres mi, kart mı? Hesap bazlı kural kolay, ama bot sahibi bin hesap açabilir; kart/adres bazlı kural ödeme adımında ikinci bir kontrol ister.
  • Rezervasyon ödeme için ne kadar bekleyebilir? Bu süre (ör. 10 dakika), satılmayan stoğun ne zaman geri döneceğini belirler.
  • Ödeme sağlayıcısının saniyedeki işlem kapasitesi ne? Çoğu zaman gerçek darboğaz budur; sisteme kabul hızını bu belirler.
  • "Tükendi" bilgisinin birkaç saniye geç gösterilmesi kabul mü? Kabul ise stok sayısı cache'lenebilir ve okuma yükü dramatik düşer.
  • Overselling mutlak yasak mı? Evet (kampanya fiyatından fazla satış zarar ve hukuki risk). Underselling (birkaç adedin geç satılması) ise tolere edilebilir. Bu asimetri tasarımın kalbidir: şüphede reddet.

Fonksiyonel olmayan gereksinimler: overselling sıfır; checkout API'si yük altında da düşük latency ile cevap vermeli (kullanıcıya hızlı "sırada", "tükendi" ya da "rezerve edildi" demek); sistemin geri kalanı (normal alışveriş) etkilenmemeli; her siparişin kalıcı ve denetlenebilir kaydı.

2. Kapasite tahmini

  • 2 milyon kullanıcı / 60 saniye ≈ 33.000 kullanıcı/s ortalama. Gelişler düzgün değildir: 20:00:00'da bildirimle gelen dalga ilk saniyelerde ortalamanın birkaç katına çıkar.
  • Kullanıcı başına tek istek varsayımı iyimserdir: sayfa açma, stok sorgulama, sabırsız yenileme ve istemci retry'ları ile kullanıcı başına birkaç istek beklenir. Kenar katmanında (CDN + gateway) yüz binlerce rps mertebesi düşünülmeli.
  • Sadece 50.000 satış başarılı olabilir: kullanıcıların %97,5'i "tükendi" cevabı alacak. Tasarımın ana fikri: reddedilecek trafiği olabildiğince ucuz ve olabildiğince erken reddetmek, veritabanından uzakta.
  • Yazma tarafı küçüktür: 50.000 sipariş, birkaç dakikaya yayıldığında saniyede yüzler-binler mertebesinde; sipariş verisi megabaytlar düzeyinde. Problem veri hacmi değil, anlık eşzamanlılıktır.

3. API

http
GET  /api/v1/sales/{saleId}                      # statik detay + başlangıç zamanı (CDN'den)
POST /api/v1/sales/{saleId}/queue-tokens          # bekleme odasına gir -> 202 + sıra bilgisi
GET  /api/v1/sales/{saleId}/queue-tokens/{token}  # sıra durumu (polling)
POST /api/v1/sales/{saleId}/reservations          # header: Admission-Token, Idempotency-Key
     -> 201 {reservationId, expiresAt} | 409 already-reserved | 410/409 sold-out | 429
POST /api/v1/reservations/{id}/payment            # header: Idempotency-Key -> 202/200

"Tükendi" için status seçimi takım standardıdır (409 ya da 410 Gone; gövdede code: SOLD_OUT). Önemli olan istemcinin bu cevabı gördüğünde retry yapmaması; aksi halde tükenen bir kampanyaya istemciler istek yağdırmaya devam eder.

4. Veri modeli

  • Redis (sıcak yol): stock:{sale42} (kalan adet), buyers:{sale42} (rezerve etmiş/almış kullanıcılar kümesi), pending:{sale42} (sorted set: userId → rezervasyon bitiş zamanı).
  • PostgreSQL (kalıcı kayıt): flash_sale_orders(id, sale_id, user_id, reservation_id, status, amount, created_at) + UNIQUE (sale_id, user_id); kampanya stoğu için flash_sales(id, total_stock, sold) CHECK (sold <= total_stock). Bu constraint'ler son savunma hattıdır: Redis'te bir hata olsa bile veritabanı ikinci satışı ya da fazla satışı kabul etmez.
  • {sale42} süslü parantezi Redis Cluster hash tag'idir: sadece parantez içi hash'lenir, böylece üç anahtar aynı slot'a, aynı node'a düşer. Bir Lua script'inin birden fazla anahtara atomik dokunabilmesi için anahtarların aynı slot'ta olması gerekir.

5. Yüksek seviye mimari

diyagram yükleniyor…
flowchart LR
    U[Mobil / Web istemci] --> CDN[CDN: statik sayfa, geri sayım]
    U --> GW[API Gateway: auth, rate limit, bot kontrolü]
    GW --> WR[Bekleme odası servisi]
    WR -->|admission token| GW
    GW --> RS[Rezervasyon servisi]
    RS -->|Lua script| R[(Redis: stok, alıcılar, bekleyen rezervasyonlar)]
    RS -->|reservation event| K[(Kafka)]
    K --> OS[Sipariş servisi: idempotent consumer]
    OS --> DB[(PostgreSQL: siparişler + outbox)]
    U --> PS[Ödeme servisi]
    PS -->|idempotency key| PSP[Ödeme sağlayıcısı]
    PS -->|confirm / release| R
    PS --> DB
    SW[Süresi dolan rezervasyon süpürücüsü] -->|expire script| R
    DB -->|outbox relay| K
    REC[Uzlaştırma job'ı] --> R
    REC --> DB

6. Deep dive: trafiği karşılamak

Katmanlı savunma; her katman bir sonrakinin yükünü azaltır:

  1. CDN: Ürün sayfası, görseller ve geri sayım statiktir; origin'e hiç gelmez. Stok göstergesi istenirse "son birkaç saniyenin" değeri olarak CDN'de kısa TTL ile cache'lenir.
  2. Gateway'de rate limit ve bot koruması: Kullanıcı ve cihaz başına limit (bkz. Bölüm 19), kimliği doğrulanmamış checkout isteği yok, bot tespiti (davranış sinyalleri, cihaz parmak izi, gerektiğinde CAPTCHA). Satış başlamadan önceki istekler ucuzca reddedilir.
  3. Sanal bekleme odası (waiting room): Kullanıcılara sıra verilir ve sisteme checkout'un kaldırabileceği hızda alınırlar. Kabul edilen kullanıcı, kısa ömürlü ve imzalı bir admission token alır (ör. JWT: userId, saleId, geçerlilik süresi). Rezervasyon servisi token'ı doğrulamak için hiçbir yere sormaz (imza kontrolü), bu da kontrolü ucuzlatır. Kabul hızı, ödeme sağlayıcısının ve rezervasyon yolunun yük testinde ölçülen kapasitesine göre ayarlanır. Ek fayda: adalet ("ilk gelen önce") ve kullanıcıya dürüst bir sıra göstergesi. Hazır ürünler de var (CDN sağlayıcılarının waiting room servisleri).
  4. Önceden ölçekleme: HPA metrik toplayıp pod açana kadar dalga geçmiş olur. Pod'lar 20:00'den önce hedef sayıya çıkarılır, connection havuzları ve cache'ler ısıtılır, bir gün önce aynı profille yük testi yapılır.
  5. İzolasyon: Flash sale ayrı pod'larda (bulkhead) koşar; normal alışveriş trafiği aynı thread havuzlarını ve veritabanı connection'larını paylaşmaz.

7. Deep dive: overselling yok, müşteri başına bir

Stok sayacı Redis'tedir ve rezervasyon tek bir Lua script'tir. Redis script'i atomik çalıştırır, araya başka komut girmez; bu yüzden "kontrol et ve düş" adımları arasında yarış yoktur, kilit gerekmez:

lua
-- KEYS[1]=stock:{sale42}  KEYS[2]=buyers:{sale42}  KEYS[3]=pending:{sale42}
-- ARGV[1]=userId  ARGV[2]=reservation expiresAt (epoch ms)
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
  return -1                                   -- bu kullanıcı zaten aldı / rezerve etti
end
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then
  return 0                                    -- tükendi
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
redis.call('ZADD', KEYS[3], ARGV[2], ARGV[1])
return 1                                      -- rezervasyon başarılı

Bu script stok 100 iken 300 farklı kullanıcıdan gelen 600 eşzamanlı istekle denendi: tam 100 rezervasyon, aynı kullanıcının ikinci denemesine -1, stok tam 0.

Neden veritabanında UPDATE ... SET stock = stock - 1 WHERE stock > 0 değil? Bu da doğru ve atomiktir, ama tek bir satıra on binlerce eşzamanlı güncelleme o satırın kilidinde sıraya girer: her transaction bir öncekinin commit'ini bekler, connection havuzu dolar, latency saniyelere çıkar. Bekleme odası kabul hızını birkaç yüz/s'ye indirebiliyorsa veritabanı yaklaşımı yeterli ve daha basittir; Redis, kabul hızının veritabanı satır kilidinin kaldıramayacağı kadar yüksek olduğu durum içindir. Mülakatta bu eşiği söylemek, "Redis kullanırım" demekten değerlidir.

8. Rezervasyondan ödemeye

Başarılı rezervasyonun bir bitiş zamanı vardır (ör. 10 dakika). Kullanıcı bu sürede öder; ödemezse birim stoğa döner ve sıradaki kullanıcılar alabilir.

Süresi dolanları geri vermek. pending sorted set'i bitiş zamanına göre sıralı tutar; birkaç saniyede bir çalışan bir süpürücü, süresi geçenleri atomik bir script'le iade eder:

lua
-- KEYS: aynı üç anahtar  ARGV[1]=now (epoch ms)  ARGV[2]=batch size
local expired = redis.call('ZRANGEBYSCORE', KEYS[3], '-inf', ARGV[1], 'LIMIT', 0, ARGV[2])
for _, userId in ipairs(expired) do
  redis.call('ZREM', KEYS[3], userId)
  redis.call('SREM', KEYS[2], userId)        -- kullanıcı tekrar deneyebilir (iş kuralı)
  redis.call('INCR', KEYS[1])                -- birim stoğa geri döner
end
return #expired

Ödeme başarılı olunca onay aynı rezervasyonu pending'den atomik olarak çıkarır (ZREM, 1 dönerse satış kesinleşti). Onay ile süpürücü yarışırsa ikisi de atomik olduğu için sadece biri kazanır. Onay 0 dönerse rezervasyon süresi dolmuş ve stok iade edilmiştir: bu yüzden ödeme iki aşamalı yapılır: önce provizyon (authorize), sonra rezervasyonu onayla, onay başarılıysa tahsilat (capture), başarısızsa provizyonu iptal et (void). Böylece "para çekildi ama ürün başkasına gitti" durumu oluşmaz.

Ödeme çağrısı sağlayıcıya bir idempotency key ile gider; timeout "başarısız" değil "bilinmiyor" demektir, aynı anahtarla durum sorgulanır (bkz. Bölüm 18). Sağlayıcı çağrısı timeout, circuit breaker ve bulkhead ile korunur (bkz. Bölüm 14); sağlayıcı yavaşlarsa bekleme odasının kabul hızı düşürülür (backpressure).

9. Kalıcılık: siparişler veritabanına güvenilir şekilde nasıl ulaşır?

Rezervasyon başarılı olunca rezervasyon servisi Kafka'ya bir ReservationCreated event'i yazar (key = userId ya da reservationId). Sipariş servisi bu event'leri tüketip PostgreSQL'e yazar:

  • Idempotent consumer: Kafka en az bir kez teslim eder; aynı event iki kez gelebilir. UNIQUE (sale_id, user_id) ve reservation_id üzerinde unique constraint ile ikinci yazma etkisiz kalır.
  • Redis ile Kafka arasında dual-write: Lua script başarılı olup pod Kafka'ya yazamadan çökerse Redis'te rezervasyon var, siparişte yok. Rezervasyonun TTL'li olması bu durumu kendiliğinden iyileştirir: kullanıcı ödeme yapamaz (sipariş yok), rezervasyon süresi dolunca stok geri döner. Ödeme adımı siparişin veritabanında var olmasını şart koşar.
  • Sipariş servisinden çıkan event'ler (ödeme alındı, kargoya hazır) veritabanı transaction'ıyla aynı anda outbox tablosuna yazılır ve bir relay ile Kafka'ya taşınır (bkz. Bölüm 13).
  • Uzlaştırma (reconciliation) job'ı: Dakikada bir Redis'teki satılan adet, bekleyen rezervasyonlar ve veritabanındaki siparişleri karşılaştırır; farkları raporlar ve kurallı olanları düzeltir. Dağıtık bir sistemde "her şey tutarlı olacak" demek yerine "tutarsızlığı nasıl tespit edip düzelteceğim" demek senior bir cevaptır.

10. Cache, gözlemlenebilirlik ve başarısızlık senaryoları

Cache: Ürün detayı ve kampanya kuralları CDN ve lokal cache'te; kalan stok göstergesi kısa TTL ile (yaklaşık değer kabul). Stok kararının kendisi asla cache'ten verilmez; tek doğru kaynak Redis script'idir.

Gözlemlenebilirlik: Satış öncesi hazırlanmış bir dashboard: kalan stok, saniyelik rezervasyon / onay / iade sayısı, bekleme odası uzunluğu ve kabul hızı, 429 ve hata oranları (endpoint ve kod bazında), Redis latency ve CPU, Kafka consumer lag, ödeme sağlayıcısı latency ve circuit breaker durumu, uzlaştırma farkları. Alarmlar: "rezervasyon - sipariş farkı eşiği aştı", "consumer lag artıyor", "oversell kontrolü: satılan > toplam".

Başarısızlık senaryoları:

SenaryoEtkiÖnlem
Redis primary düşer, replica'ya failoverReplikasyon asenkron olduğu için onaylanmış son yazmalar kaybolabilir: stok sayacı geri gidebilir (overselling riski) ya da rezervasyon kaybolabilirDB'deki CHECK ve UNIQUE son savunma; uzlaştırma; kritik yazmalarda WAIT ile replica onayı beklemek riski azaltır ama Redis'i güçlü tutarlı yapmaz
Tek anahtar (stok) tek node'u boğar (hot key)Redis CPU'su tavan, latency artarÖnce bekleme odasıyla yükü sınırla ve ölç; gerekirse stoğu N kovaya böl (aşağıya bak)
Ödeme sağlayıcısı yavaşlarRezervasyonlar ödenemeden süresi dolar, thread'ler beklerTimeout, circuit breaker, bulkhead; kabul hızını düşür; rezervasyon süresini uzatmayı değerlendir
Kafka consumer geride kalırSiparişler gecikmeli görünürLag alarmı, partition/consumer sayısı; ödeme adımı siparişi bekler, kullanıcıya "işleniyor"
Pod rezervasyondan sonra çökerSiparişsiz rezervasyonTTL ile kendiliğinden iade; uzlaştırma
Botlar bekleme odasını doldururGerçek kullanıcılar sırada kalırHesap yaşı/geçmişi, cihaz parmak izi, CAPTCHA, kart/adres bazlı tekillik ödeme adımında

Hot key ve kovalar. Stok tek kovada tek node'dadır. Kabul hızı bir node'un kaldıramayacağı kadar yüksekse stok N kovaya bölünür (ör. 10 × 5.000), her kova kendi hash tag'iyle farklı slot'ta: stock:{sale42:3}, buyers:{sale42:3}. Ama "müşteri başına bir" kontrolü kovanın alıcı kümesindedir; kullanıcı rastgele kovaya giderse iki kovadan iki kez alabilir. Bu yüzden kullanıcı deterministik olarak kovasına yönlendirilir (hash(userId) % N). Bunun bedeli: kovası boşalan kullanıcı, başka kovalarda stok varken "tükendi" görür. Çözüm seçenekleri: kalan stoğu periyodik olarak kovalar arasında yeniden dağıtmak, ya da boş kovada alıcı kaydını kendi kovasında tutup başka kovadan stok almak (iki adım, telafi gerekir). Veritabanındaki UNIQUE (sale_id, user_id) her durumda son savunmadır. Bu karmaşıklık yüzünden kovalamaya ancak yük testi tek kovanın yetmediğini gösterirse geçilir.

11. Bilinçli olarak kapsam dışı bırakılanlar

Çok bölgeli (multi-region) aktif-aktif kurulum, dinamik fiyatlama, iade süreci, kargo entegrasyonu. Bunları söylemek "unuttum" değil "kapsamı yönetiyorum" mesajı verir.

Sistem tasarımında İngilizce: işe yarayan kalıplar

Mükemmel İngilizce gerekmez; yapı ve sinyal kelimeleri gerekir. Her adım için hazır cümleler:

AdımKalıp
Netleştirme"Before I start, I'd like to clarify a few requirements." / "Is it acceptable if the 'sold out' status shows up a few seconds late?" / "Should I focus on the reservation flow or also on payment?"
Varsayım"I'll assume one unit per account, and a ten-minute reservation window. Let me know if that's wrong."
Tahmin"As a back-of-the-envelope estimate, two million users in a minute is roughly thirty-three thousand requests per second on average, with much higher peaks."
Ana fikir"The key insight is that ninety-seven percent of the requests will fail, so we need to reject them as early and as cheaply as possible."
Seçenek ve seçim"There are two options here: a conditional update in the database, or an atomic Lua script in Redis. I'd go with Redis because..."
Trade-off"The trade-off is that we gain throughput but Redis replication is asynchronous, so we may lose the last writes on failover."
Son savunma"As a last line of defense, the database has a unique constraint on sale and user."
Başarısızlık"If the payment provider times out, the outcome is unknown, not failed, so we query it again with the same idempotency key."
Kapsam"To keep it simple, I'd start with a single bucket and only split the stock if load tests show a hot key." / "I'm deliberately leaving multi-region out of scope."
Yön değiştirme"I've covered the high-level design. Shall I deep dive into stock consistency?"
Takılma"Let me think about this for a second." / "I'm not sure about the exact number, but the order of magnitude is..."

Yaygın Türkçe kaynaklı hatalar: "I will make a reservation table" yerine "I'd create a reservations table"; "we can take the risk" yerine "we can accept that risk"; "it depends to" yerine "it depends on"; "more faster" yerine "faster". Cümleleri kısa tut; uzun cümlede dil hatası hem artar hem de fikrin kaybolur.

Code review: neye, hangi sırayla bakılır? (k6)

Review'da dikkat sınırlı bir kaynaktır; önce en pahalı hatalara harcanır:

  1. Güvenlik: Injection, yetkilendirme (IDOR), hassas verinin log'lanması, secret'ların koda gömülmesi.
  2. Doğruluk ve veri bütünlüğü: Kenar durumlar (boş liste, null, Optional.get()), eşzamanlılık (lost update), transaction sınırları, hata yönetimi.
  3. Performans: N+1, sayfalamasız sorgu, index kullanamayan sorgu, döngüde dış çağrı.
  4. Tasarım ve okunabilirlik: Katman sorumlulukları, isimlendirme, test edilebilirlik, API sözleşmesi.
  5. Stil: Biçim, import sırası. Bunlar insanın değil formatter ve linter'ın işidir; CI'da otomatikleştir.

Yorumun ciddiyetini açıkça işaretle. Yazar hangi yorumun merge'ü engellediğini tahmin etmek zorunda kalmamalı. Yaygın bir standart Conventional Comments'tir: issue (blocking):, suggestion (non-blocking):, nitpick:, question:, praise:. Google'ın review rehberi de küçük önerilere Nit: önekini önerir ve önemli bir ilke koyar: PR mükemmel olmasa da kod tabanının genel sağlığını kesin olarak iyileştiriyorsa onaylanmalıdır. Mükemmeliyetçilik review'ı tıkar.

Yorumun dili: Kişiyi değil kodu konuş; ne, neden, nasıl üçlüsünü ver; mümkünse kod önerisiyle gel.

text
Zayıf:  "Bu yanlış."  /  "Neden böyle yaptın?"
İyi:    issue (blocking): `name` doğrudan SQL'e ekleniyor; "' OR '1'='1" tüm tabloyu
        döker. Parametreli sorgu kullanalım:
        jdbcTemplate.queryForList("SELECT id, name FROM customers WHERE name ILIKE ?", "%" + name + "%")
Soru:   question: Bu endpoint'i sadece admin mi çağırıyor? Değilse müşterinin kendi id'sini
        SecurityContext'ten almamız gerekir.
Övgü:   praise: Testlerin isimlendirmesi çok net, hata durumunu hemen anlatıyor.

Soru biçimindeki yorumlar ("Burada boş liste gelebilir mi?") hem yazara düşünme alanı bırakır hem de senin bir şeyi kaçırmış olma ihtimaline karşı alçakgönüllüdür. Büyük tasarım itirazlarını uzun yorum zincirleri yerine kısa bir görüşmeyle çöz, kararı PR'a not düş.

Küçük PR. Yüzlerce satırlık PR'lar dikkatli incelenemez; review kalitesi PR boyutu arttıkça düşer. Büyük işler feature flag arkasında, her biri kendi başına derlenen ve test geçen küçük PR'lara bölünür. Refactor ile davranış değişikliği ayrı PR (ya da en azından ayrı commit) olur.

İnsan yerine CI'ın yapması gerekenler: Formatter (Spotless), statik analiz (SpotBugs, Error Prone, SonarQube), bağımlılık güvenlik taraması (OWASP Dependency-Check, Dependabot), SAST, mimari kurallar (ArchUnit: "controller repository'ye doğrudan erişemez"), test ve coverage eşiği.

İyi commit (z11)

İyi bir commit:

  • Atomiktir: Tek bir mantıksal değişiklik. "Refactor + yeni özellik + format düzeltmesi" üç commit'tir. Atomik commit tek başına geri alınabilir (git revert), cherry-pick edilebilir ve review'da anlaşılır.
  • Her commit derlenir ve testleri geçer: Geçmişin her noktası çalışır durumda olduğu için git bisect ile hatanın girdiği commit ikili aramayla bulunabilir.
  • Mesajı nedeni anlatır: "Ne" diff'te zaten görünür; mesaj "neden"i ve bağlamı anlatır.

Conventional Commits formatı: <type>[optional scope]: <description>, ardından boş satır, gövde ve footer'lar. Spesifikasyon feat (yeni özellik, semver minor) ve fix (hata düzeltme, semver patch) tiplerini tanımlar; uyumsuz değişiklik BREAKING CHANGE: footer'ı ya da tipten sonra ! ile belirtilir (semver major). refactor, test, docs, chore, build, ci, perf gibi diğer tipler spesifikasyonun izin verdiği, Angular geleneğinden gelen yaygın kullanımdır. Bu format otomatik changelog ve sürüm numarası üretimini mümkün kılar.

text
fix(shipping): reject unknown carrier instead of returning zero fee

Unknown carriers fell through the if/else chain and produced a fee of 0,
which meant free shipping for any typo in the carrier code.
Now an UnsupportedCarrierException is thrown and mapped to 400.

Refs: SHIP-142

Live coding'de commit atmak ilerlemeni görünür kılar: "characterization test eklendi", "strategy'ye refactor edildi, davranış aynı", "bilinmeyen kargo firması reddediliyor" gibi üç commit, mülakatçıya hem disiplinini hem düşünce sıranı anlatır.

Live coding oturumu: akış ve davranış

Spring tabanlı live coding mülakatlarında sık görülen bir format, yaklaşık iki saatlik, gerçek bir küçük kod tabanı üzerinde pair programming'dir (şirketten ve ekipten ekibe değişir): if/else ile yazılmış kargo hesaplamasını Strategy ile Open/Closed'a uygun hale getirmek, boş controller'ı doldurmak, endpoint isimlendirmesi, ortak RestTemplate kodunu toplamak, GlobalExceptionHandler eklemek, URL'leri konfigürasyona almak ve unit test yazmak. Bu tür görevlerde TDD, SOLID ve design pattern pratiklerinin uygulanması beklenir.

Örnek akış (2 saat):

ZamanNe yaparsın
0-5 dkTanışma. Ekran paylaşımı, IDE fontunu büyüt (Presentation Mode ya da 16pt+), bildirimleri kapat.
5-15 dkKod yazmadan önce: projeyi build et, mevcut testleri çalıştır, paket yapısını gez, görev listesini oku, belirsizlikleri sor. Planı sesli söyle: "Önce X, sonra Y; Z'ye zaman kalırsa geçerim. Uygun mu?"
15-90 dkKüçük adımlar: mevcut davranışı testle sabitle → refactor/özellik → testleri çalıştır → commit. Her 15-20 dakikada "şu an şuradayım, sırada bu var" özeti.
90-110 dkBitmeyen görevler için "zamanım olsa şöyle yapardım"; mülakatçının ölçek, hata senaryosu, test stratejisi soruları.
110-120 dkSenin soruların.

Neye bakılıyor? Resmi bir rubrik yayınlanmıyor; pair programming mülakatlarının tipik boyutları:

BoyutGüçlü sinyalZayıf sinyal
Problemi anlamaNetleştirme soruları, gereksinimi kendi cümlenle tekrar etmeHemen kodlamaya dalıp yanlış şeyi yapmak
İletişimSesli düşünme, trade-off'ları söyleme, öneriye açıklıkUzun sessizlik, savunmacı tavır
TasarımPattern'i doğru yerde ve gerekçesiyle (OCP, Strategy, DI)Pattern ezberi ya da her şeye soyutlama
TestÖnce ya da hemen sonra test; kenar durumlar; anlamlı test isimleri"Test yazmaya vakit kalmadı"
Kod kalitesiİsimlendirme, küçük metotlar, BigDecimal, immutability, constructor injectionMagic number, double para, field injection, yutulan exception
Hata yönetimiDomain exception, merkezi handler, doğru status koducatch (Exception e) → 500 "error"
Zaman yönetimiÖnceliklendirme, çalışan küçük adımlarBuild problemiyle 20 dakika kaybetmek

Sesli düşünme kalıpları:

  • "Önce gereksinimi doğru anladığımdan emin olayım: ... doğru mu?" (Let me make sure I understand the requirement correctly...)
  • "Bu metoda dokunmadan önce mevcut davranışını bir testle sabitleyeceğim." (Before touching this, I'll pin down the current behavior with a test.)
  • "Burada iki seçenek görüyorum: A ve B. A'yı seçiyorum çünkü..." (I see two options here. I'll go with A because...)
  • "Önce en basit çalışan hali yazıyorum; testler yeşil olunca refactor edeceğim." (I'll write the simplest version first and refactor once the tests are green.)
  • "Bilinmeyen kargo firmasında 0 TL dönüyor; bu bedava kargo demek. Hata mı dönelim, varsayılan ücret mi?" (There's an ambiguity: should an unknown carrier be an error or a default fee?)
  • "Daha fazla zamanım olsaydı timeout, idempotency ve integration test eklerdim." (If I had more time, I'd add timeouts, idempotency and an integration test.)

Takılırsan: (1) Sesli söyle: "Şu an X'in neden null geldiğini anlamaya çalışıyorum"; sessizlik takılmaktan daha kötü görünür. (2) Bildiğini topla: ne bekledin, ne oldu? (3) Küçült: küçük bir test ya da debugger ile problemi izole et; stack trace'i birlikte oku. (4) Yön iste: "Hangi tarafa bakmamı önerirsiniz?" Pair programming'de ipucu istemek iş birliğidir. (5) Bir görev kilitlendiyse not al ve diğerine geç: "Buna sonra döneceğim."

Ortam tuzakları: Proje eski bir Spring sürümüyle gelebilir: Boot 2.x'te javax.*, 3.x'te jakarta.*; RestClient 3.2+, ProblemDetail Boot 3+. Lombok ya da Feign eklemek için build'le 15 dakika uğraşma; durumu söyle ve elle yaz (record'lar Lombok ihtiyacının çoğunu karşılar). JUnit 4 ve 5 annotation'larının karışması testlerin hiç çalışmamasına ("0 test") yol açar. Projenin wrapper'ını kullan (./gradlew test, ./mvnw test).

Davranışsal-teknik sorular ve STAR

"Kendinden bahset", "en zorlandığın teknik problem", "bir production incident'ı anlat", "bir teknik anlaşmazlığı nasıl çözdün" gibi sorular, teknik derinliğini gerçek bir hikaye üzerinden ölçer. Yapı için STAR:

  • Situation (durum): Bağlam, 1-2 cümle. Sistem, ölçek, ekip.
  • Task (görev): Senin sorumluluğun neydi, risk neydi.
  • Action (eylem): Ne yaptın; "biz" değil "ben". Hikayenin ağırlığı burada olmalı: teşhis adımları, değerlendirdiğin seçenekler, neden birini seçtiğin.
  • Result (sonuç): Ölçülebilir sonuç (latency %X düştü, incident süresi, hata oranı) ve ne öğrendin, neyi kalıcı olarak değiştirdin.

İki-üç dakikada bitir; mülakatçı ilgilendiği yeri soracaktır. Önceden 4-5 hikaye hazırla ve her birinin hangi soruya uyduğunu eşle: zor teknik problem, production incident, anlaşmazlık, hata yaptığın bir an, sahiplendiğin bir iyileştirme. Teknik hikayede mekanizmaya in: "yavaştı, düzelttim" değil, "p99 1,8 saniyeydi, trace'te zamanın %80'inin N+1 sorgularda geçtiğini gördüm".

Hikaye uydurma. Mülakatçı takip sorularıyla iki dakikada derinleşir ("o sorgunun execution plan'ında ne vardı?"); uydurma hikaye orada çöker ve tüm görüşmeye güveni bozar. Aşağıdaki örnek cevaplar yapıyı göstermek içindir; kendi gerçek deneyiminle doldur.

Mülakatçıya sorulacak sorular

Soru sormak değerlendirmenin parçasıdır; ilgini ve neye önem verdiğini gösterir. Ezber "şirket kültürü nasıl?" yerine somut ve işle ilgili sorular:

  • "Ekibin sahip olduğu domain ve servisler neler; en büyük teknik zorluğunuz şu an ne?"
  • "Bir değişiklik merge'den production'a nasıl gidiyor, ne sıklıkla deploy ediyorsunuz?"
  • "Code review kültürünüz nasıl; bir PR ortalama ne kadar sürede review ediliyor?"
  • "On-call nasıl işliyor? Son ciddi incident'tan sonra ne değişti?"
  • "Teknik borcu nasıl önceliklendiriyorsunuz?"
  • "Bu pozisyonda ilk 6 ayda başarılı olmuş biri neyi başarmış olur?"
  • "Test stratejiniz nasıl; integration test'leri nasıl koşuyorsunuz?"

Quiz'deki sorular: çözüm ve nedenleri

q20: Flash sale checkout tasarımı (İngilizce cevap)

Senaryo: 20:00'de 50.000 adet, müşteri başına 1, ilk dakikada ~2 milyon kullanıcı. Netleştirme ve kaba sayılar, trafik patlamasından korunma, overselling ve müşteri başına bir garantisi, rezervasyon ile ödeme arası, siparişlerin veritabanına güvenilir ulaşması ve başarısızlık senaryoları istenir.

Güçlü cevabın unsurları: (1) Netleştirme ve tahmin: ~33k rps ortalama, çok daha yüksek peak; sadece 50k başarılı olabileceği için trafiğin çoğu kenarda ucuzca reddedilmeli. (2) Koruma: CDN, gateway'de rate limit, bekleme odası, önceden ölçekleme, yük testi. (3) Redis'te tek atomik Lua script ile stok ve alıcı kontrolü; hash tag ile aynı slot; DB'de UNIQUE ve CHECK son savunma. (4) TTL'li rezervasyon ve iade. (5) Kafka + idempotent consumer + outbox + uzlaştırma. (6) Failover, hot key, ödeme sağlayıcısı yavaşlığı, dashboard ve alarmlar.

Model cevaptaki eksikler ve düzeltmeler:

  • Keyspace expiry ile stok iadesi güvenilir değil. Model cevap, ödenmeyen rezervasyonun "delayed job or keyspace expiry" ile iade edilmesini öneriyor. Redis keyspace notification'ları Pub/Sub üzerinden gider ve fire-and-forget'tir: abone o an bağlı değilse event kaybolur. Ayrıca expired event'i TTL sıfıra ulaştığı an değil, anahtar fiilen silindiğinde (erişimde ya da arka plandaki temizlikte) üretilir; gecikebilir. Stok iadesi gibi para değerinde bir işlem buna bağlanmamalı. Yukarıdaki gibi bitiş zamanına göre sıralı bir sorted set ve periyodik, atomik bir süpürücü script'i (ya da veritabanı tabanlı bir zamanlanmış iş) güvenilirdir.
  • "2M kullanıcı ≈ 33k rps" kullanıcı başına tek istek varsayar. Yenileme, polling ve retry'larla kenardaki istek sayısı bunun birkaç katıdır; kapasite planı buna göre yapılmalı.
  • Hot key için "stoğu N kovaya böl" önerisi tek başına "müşteri başına bir" garantisini bozar. Alıcı kontrolü kovanın kümesindeyse kullanıcının deterministik olarak tek kovaya yönlendirilmesi gerekir; bunun da "kovası boşalan kullanıcı" problemi vardır (yukarıda anlatıldı).
  • WAIT güçlü tutarlılık sağlamaz. Redis dokümantasyonu da WAIT'in Redis'i güçlü tutarlı bir store yapmadığını belirtir; failover'da yazma kaybı riskini azaltır, ortadan kaldırmaz. Asıl güvence veritabanı constraint'leri ve uzlaştırmadır (model cevap bunlardan da bahsediyor, doğru).
  • Ödeme ile rezervasyon onayı arasındaki yarış model cevapta yok: rezervasyon süresi dolduktan hemen sonra gelen başarılı ödeme. Provizyon → atomik onay → tahsilat (ya da provizyon iptali) sırası bu durumu kapatır.

İngilizce için öneriler: "We will do a queue" yerine "We'll put users in a virtual waiting room"; "Redis is very fast so no problem" yerine "Redis executes the script atomically, so no locks are needed"; "maybe it can fail" yerine "One failure mode is...".

k6: Bu PR'ı inceleyip yorum yaz

Senaryo: CustomerController'da string birleştirmeyle kurulan bir LIKE sorgusu, siparişleri listeleyen ve log'a e-posta ile kart numarası yazan bir metot, private ve @Transactional bir updatePoints ile puan ekleyen bir POST. Yazar "küçük bir değişiklik, hızlıca onaylar mısın?" diyor.

Bulgular, ciddiyet sırasıyla:

CiddiyetProblemNeden önemliDüzeltme
BlockerSQL injection ('%" + name + "%')' OR '1'='1 tüm tabloyu döker; daha kötü payload'lar mümkünParametreli sorgu (?), kolonları seç, sayfala
BlockerKart numarası ve e-posta log'lanıyorPCI DSS kart numarasının okunabilir saklanmasını yasaklar; e-posta kişisel veri (KVKK); log'lar geniş erişimli ve uzun saklanırSadece id'ler; kart numarası zaten tokenize edilmeli
BlockerYetkilendirme yok (IDOR)Herkes başkasının siparişini okuyabilir, başkasına puan ekleyebilirMüşteri kimliği token'dan; puan ekleme sadece iç servis/admin rolü
Blocker@Transactional private metotta ve self-invocationProxy private metodu saramaz, this üzerinden çağrı proxy'den geçmez: transaction yokMantığı bir servisin public metoduna taşı
ÖnemliLost update (getPoints() + points)İki eşzamanlı istek aynı değeri okur, biri kaybolurUPDATE ... SET points = points + :p (atomik) ya da @Version + retry
Önemliorders.get(0) ve findById(id).get()Boş listede IndexOutOfBoundsException, olmayan müşteride NoSuchElementException: 500Boş kontrolü; orElseThrow → 404
ÖnemliN+1 ve entity dönmekDöngüde her sipariş için ayrı sorgu; entity JSON'a lazy alanları ve payment gibi iç verileri taşırDTO projection ya da @EntityGraph, Pageable, response DTO
ÖnemliLIKE '%x%'Baştaki joker karakter yüzünden B-tree index kullanılamaz, full scan; boş name tüm müşterileri dönerMinimum uzunluk, sayfalama; büyük tabloda pg_trgm GIN index ya da arama motoru
Önemlipoints validasyonu yokNegatif ya da devasa puan@Positive @Max(...)
ÖnericustomerRepository ve log tanımsızKod derlenmiyor; PR CI'dan geçmemişCI'ı zorunlu kıl
ÖneriField injection, iş mantığı controller'da, /api/v1 yok, void POSTTest edilebilirlik, katman sorumluluğu, sözleşmeConstructor injection, servis katmanı, /api/v1/customers/..., 204 ya da güncel puan
ÖneriTest yok, PR açıklaması yokDeğişikliğin doğruluğu kanıtlanmıyor"Bu değişiklik için hangi test eklendi?"

Neden bu sıra? Güvenlik ve veri bütünlüğü hataları geri alınamaz zarar verir (sızan veri geri toplanamaz); performans ve tasarım problemleri sonradan düzeltilebilir. "Küçük değişiklik, hızlıca onayla" baskısına rağmen blocker'lar varken onay verilmez; ama ton yapıcı kalır.

Takip sorularına kısa cevaplar: Junior ve hassas bir yazara: blocker'ları net ama kişisizleştirilmiş yaz, bir övgüyle başla, büyük konular için "10 dakika birlikte bakalım mı?" de; öğretici linkler ekle. Katılmadığın bir yorum gelirse: önce gerekçeyi anlamaya çalış, kendi gerekçeni veriyle söyle, ikinci turda hâlâ anlaşamıyorsanız kısa bir görüşme ya da üçüncü bir kişinin görüşü; ego değil kod tabanının sağlığı.

z11: Live coding sırasında iyi commit pratikleri

Doğru: a, c, d.

  • a) fix: reject unknown carrier instead of returning zero fee: Tip, ne değiştiği ve davranış farkı net; Conventional Commits formatında.
  • c) Refactor ile davranış değişikliğini ayrı commit'lerde yapmak: Refactor commit'i "davranış aynı" iddiasını taşır ve testlerle doğrulanır; davranış değişikliği ayrı olunca review kolaylaşır, gerekirse tek başına geri alınır.
  • d) Her commit'te testlerin geçmesi: Geçmişin her noktası çalışır; git bisect kullanılabilir.

Yanlış:

  • b) changes / WIP / fix: Hiçbir bilgi vermez; altı ay sonra git log ve git blame işe yaramaz hale gelir.
  • e) Sonda tek büyük commit: İlerleme görünmez, review zorlaşır, bir kısmı geri alınamaz; live coding'de süre biterse yarım kalan iş ile tamamlanan iş ayırt edilemez.

Production karşılığı: Conventional Commits otomatik changelog ve semantic versioning araçlarını besler; squash-merge kullanılan ekiplerde PR başlığı da bu formatta yazılır. Commit mesajında ticket referansı (Refs: SHIP-142) izlenebilirlik sağlar.

z13: Live coding'in ilk 10 dakikası

Doğru: b) Projeyi build edip testleri çalıştırmak, yapıyı hızlıca gezmek, belirsizlikleri sormak, görevleri önceliklendirip planı sesli söylemek. Bu adımlar üç şeyi birden gösterir: ortamın çalıştığından emin olma disiplini (kırık bir test varsa baştan söylemek, sonradan "ben mi bozdum?" belirsizliğini önler), problemi anlama ve önceliklendirme. "Önce şunları yapacağım, şu sırayla; uygun mu?" cümlesi mülakatçıyı sürecin ortağı yapar; mülakatçının önceliği farklıysa bunu dakika 10'da öğrenmek, dakika 60'ta öğrenmekten iyidir.

  • a) Hemen ilk maddeyi kodlamak: Projeyi anlamadan yazılan kod genelde geri alınır; mülakatçı soru sorup sormadığına da bakar.
  • c) Tüm projeyi kendi beğendiğin yapıya göre düzenlemek: Görev dışı, riskli ve zaman yiyici; kapsam mülakatçıyla birlikte belirlenir.
  • d) Sessizce okuyup tam plan çıkana kadar konuşmamak: Mülakatçı düşünce sürecini göremez; sesli düşünmek beklenir.

Ana kavram: Çoğu zaman listedeki her şeyi bitirmen beklenmez; doğru önceliklendirme, çalışan küçük adımlar ve iletişim değerlendirilir. İki görevi testleriyle bitirmek, altı görevi yarım bırakmaktan iyidir.

Üç seviyede cevap

Soru: "Bir PR'ı nasıl review edersin? İyi bir commit'in kriterleri neler?"

Kodu satır satır okurum, isimlendirmeye ve formatlamaya bakarım, hata görürsem yorum yazarım. Testler geçiyorsa onaylarım. İyi commit açıklayıcı mesajı olan commit'tir.

Önce PR açıklamasını ve neyi çözdüğünü okurum, sonra güvenlik, doğruluk, performans ve okunabilirlik sırasıyla bakarım: SQL injection, null ve boş liste durumları, transaction, N+1. Yorumlarımda blocker olanlarla öneri olanları ayırırım. Stil konularını linter'a bırakırım. İyi commit atomiktir, testleri geçer, mesajı neden'i anlatır; Conventional Commits formatını kullanırım.

Önce bağlam: PR'ın amacı, ilgili ticket, ve PR'ın boyutu; birkaç yüz satırı aşıyorsa bölünmesini isterim çünkü büyük PR'da review kalitesi düşer. Sonra riske göre okurum: güvenlik (injection, yetki, log'a giden veri), veri bütünlüğü (transaction sınırı, eşzamanlılık, idempotency), sonra performans ve tasarım. Testlere koddan önce bakarım: testler davranışı anlatıyorsa kodu anlamak kolaylaşır, anlatmıyorsa bu başlı başına bir yorumdur. Her yorumu ciddiyetiyle etiketlerim (blocking, suggestion, nit), ne-neden-nasıl yazarım, mümkünse kod önerisiyle gelirim ve iyi olanı da söylerim. Ölçütüm mükemmellik değil: PR kod tabanını kesin olarak iyileştiriyorsa ve blocker yoksa onaylarım, nit'ler için merge'ü bekletmem. Tekrar eden yorumları sürece çeviririm: aynı şeyi üçüncü kez yazıyorsam o bir ArchUnit kuralı, linter ayarı ya da PR şablonu maddesidir. Commit'te atomiklik, her commit'in yeşil olması (bisect için), refactor ile davranış değişikliğinin ayrılması ve mesajda nedenin yazılması benim için kriter.

Junior stil odaklı ve "testler geçiyorsa onayla" diyor; testlerin neyi test ettiğini sorgulamıyor, güvenlikten hiç bahsetmiyor. Mid doğru öncelik sırasını ve ciddiyet ayrımını biliyor. Senior review'ı bir risk yönetimi ve ekip süreci olarak ele alıyor: PR boyutu, testleri önce okumak, onay ölçütü (mükemmeliyetçilik değil iyileştirme), tekrar eden yorumları otomasyona çevirmek ve iletişim tonu.

Takip sorusu: PR'da üç blocker buldun ama yazar "yarın release var, sonra düzeltiriz" diyor. Ne yaparsın?

Soru: "Kendinden bahseder misin?"

Beş yıldır Java ile backend geliştiriyorum. Spring Boot, Kafka, PostgreSQL, Docker kullandım. Bankacılık projelerinde çalıştım. Yeni teknolojiler öğrenmeyi seviyorum ve kendimi geliştirmek istiyorum.

Beş yıldır Java ve Spring Boot ile backend geliştiriyorum, son dönemde bir bankanın teknoloji merkezinde müşteri ve hesap verileriyle ilgili servisler üzerinde çalışıyorum. REST API'ler, Kafka ile event tabanlı entegrasyonlar ve PostgreSQL üzerinde performans iyileştirmeleri yaptım. Bir API'nin feature owner'ıydım; tasarımından production'a kadar sorumluydum. Şimdi daha yüksek trafikli, e-ticaret ölçeğinde problemlerle çalışmak istiyorum.

Beş yıldır backend geliştiriyorum, son yılım bir bankanın teknoloji merkezinde, müşteri ve hesap domain'inde geçti. Orada bir hesaplama API'sinin feature owner'ıydım: gereksinimleri iş birimiyle netleştirdim, API sözleşmesini tasarladım, birkaç tüketici ekiple entegrasyonu yönettim ve production'a çıktıktan sonra performansını izledim. Bankacılık bana iki şey öğretti: veri doğruluğunda ve denetlenebilirlikte taviz verilmiyor, yani idempotency, transaction sınırları ve uzlaştırma benim için refleks; ve değişiklik maliyeti yüksek olduğu için test ve gözlemlenebilirlik olmadan bir şeyi "bitti" saymıyorum. Bunun dışında yan projelerde uçtan uca ürün geliştiriyorum, mobil tarafı da dahil; bu bana ürün düşüncesi kazandırdı. Bu role başvurma nedenim ölçek: bankacılıktaki tutarlılık disiplinini, çok daha yüksek trafik ve daha hızlı teslimat döngüsüyle birleştirmek istiyorum. Bu pozisyonda özellikle sipariş ve ödeme gibi tutarlılığın kritik olduğu alanlarda katkı verebileceğimi düşünüyorum.

Junior teknoloji listesi sayıyor; liste özgeçmişte zaten var, mülakatçı yeni bir şey öğrenmiyor. "Kendimi geliştirmek istiyorum" her adayın cümlesi. Mid sorumluluk ve somut iş anlatıyor. Senior 90 saniyede bir hikaye kuruyor: sahiplik (feature owner, uçtan uca), deneyimden çıkan ilkeler (tutarlılık, test, gözlemlenebilirlik), neden bu şirket ve bu rol, ve kendi geçmişini hedef rolün ihtiyacına bağlıyor. Kalıp: şimdiki rol ve sahiplik → öğrendiğin ve taşıdığın ilkeler → neden burası.

Takip sorusu: Feature owner olarak aldığın ve sonradan yanlış olduğunu düşündüğün bir teknik karar var mı?

Soru: "Karşılaştığın en zor teknik problemi anlatır mısın?"

Bir projede API çok yavaştı. Cache ekledik ve sorun çözüldü. Ekipçe çok uğraştık ama sonunda başardık.

Bir hesap listeleme API'sinin p99 latency'si yük altında 2 saniyeye çıkıyordu. Log'lara ve trace'lere baktım, her hesap için ilişkili tabloya ayrı sorgu atıldığını, yani N+1 olduğunu gördüm. Sorguyu tek bir join'li projection'a çevirdim ve eksik bir composite index ekledim. p99 200 milisaniyenin altına indi. Sonrasında benzer endpoint'lerde de aynı kalıbı aradık.

Durum: Birkaç tüketici ekibin çağırdığı bir API'miz, kampanya dönemlerinde p99'u 2 saniyeyi aşıp çağıran servislerde timeout'a yol açıyordu; ortalama latency normal görünüyordu, bu yüzden uzun süre fark edilmemişti. Görev: Feature owner olarak SLA'yı (p99 300 ms) geri getirmek, ama tüketicilerin sözleşmesini bozmadan. Eylem: Önce ölçtüm: trace'lerde yavaş isteklerin hepsinin çok sayıda alt kayıt içeren müşterilere ait olduğunu, zamanın büyük kısmının döngüde atılan sorgularda geçtiğini gördüm; yani problem ortalamada görünmeyen bir uzun kuyruk ve N+1'di. Üç seçenek değerlendirdim: cache, sorgu düzeltmesi ve sonucu asenkron önceden hesaplamak. Cache'i reddettim çünkü veri sık değişiyordu ve bayat veri bu domain'de kabul edilemezdi. Sorguyu tek bir projection'a çevirdim, execution plan'da sequential scan gördüğüm yer için composite index ekledim ve index'i CONCURRENTLY ile, tablodaki yazmaları bloklamadan production'a aldım. Değişikliği feature flag arkasında yüzde onluk trafikle açıp iki versiyonu karşılaştırdım. Sonuç: p99 2,1 saniyeden 180 ms'ye indi, tüketici tarafındaki timeout'lar sıfırlandı. Kalıcı olarak iki şey değiştirdim: dashboard'a ortalama yerine p99 ve p99,9'u koydum, ve integration testlere sorgu sayısını doğrulayan bir kontrol ekledim ki N+1 tekrar girmesin.

Junior'ın hikayesinde "ben" yok, teşhis yok, sonuç ölçülmemiş; "cache ekledik" de çoğu zaman problemi gizler. Mid somut ve ölçülmüş bir hikaye anlatıyor. Senior STAR yapısını kullanıyor ve şunları ekliyor: problemin neden fark edilmediği (ortalama vs. p99), değerlendirilen ve gerekçeyle reddedilen alternatifler (domain bilgisine dayalı), production'a güvenli çıkış (CONCURRENTLY, feature flag, kademeli açma) ve tekrarı önleyen kalıcı değişiklikler. Bu hikaye mülakatçıya beş farklı takip sorusu kapısı açar ve aday hepsine hazırdır.

Takip sorusu: CREATE INDEX CONCURRENTLY yarıda kalırsa ne olur, nasıl tespit edip düzeltirsin?

Soru: "Yaşadığın bir production incident'ını anlatır mısın?"

Bir gün production'da servis çöktü. Hemen restart attık ve düzeldi. Sonra log'lara baktık ama sebebini tam bulamadık.

Bir deploy sonrası bir consumer'ın sürekli aynı mesajda hata alıp partition'ı tıkadığını fark ettik, consumer lag artıyordu. Önce deploy'u geri aldık, lag eridi. Sonra sebebin yeni eklenen bir alanın eski formatlı mesajlarda null gelmesi olduğunu bulduk. Null kontrolünü ekledik, hatalı mesajlar için dead letter topic tanımladık ve tekrar deploy ettik.

Durum: Gece bir sürümden sonra hesap event'lerini işleyen consumer'da lag dakikalar içinde yükselmeye başladı; alarm lag eşiğinden geldi. Aşağı akıştaki bir ekip güncel olmayan bakiye bilgisi görüyordu. Görev: On-call olarak etkiyi durdurmak, sonra kök nedeni bulmak. Eylem: Önce etkiyi sınırladım: hatanın sürümle başladığı zaman çizelgesinden belliydi, kök nedeni araştırmadan önce rollback yaptım ve ilgili ekiplere durum mesajı geçtim; lag 15 dakikada eridi. Sonra kök nedene indim: eski şemayla üretilmiş mesajlarda yeni alan yoktu, deserialization sonrası null geliyordu, consumer exception fırlatıyor ve retry politikası sonsuz olduğu için aynı offset'te takılıp partition'ı bloke ediyordu (poison pill). Yani iki hata vardı: geriye uyumsuz bir şema değişikliği ve sınırsız retry. Sonuç ve öğrenme: Kodu düzelttik, ama asıl kalıcı değişiklikler süreçteydi: retry'ı sınırlı yapıp başarısız mesajları dead letter topic'e yönlendirdik ve DLT için alarm koyduk; şema değişikliklerine geriye uyumluluk kontrolü ekledik; deploy'dan sonra ilk 15 dakika lag ve hata oranını izleyen otomatik bir kontrol koyduk. Blameless bir postmortem yazdım; en değerli bulgu, problemi yazan kişi değil, sınırsız retry'ın bir poison pill'i tüm partition'ı durdurabilmesine izin veren tasarımdı.

Junior'ın hikayesi "restart attık, sebebini bulamadık"; bu bir incident değil, yarım kalmış bir incident ve en zayıf sinyaldir. Mid etkiyi durdurmayı ve kök nedeni bulmayı anlatıyor. Senior incident yönetiminin tam döngüsünü gösteriyor: tespit (alarm), önce etkiyi azaltma (rollback, iletişim), sonra kök neden, birden fazla katkıda bulunan faktörü ayırma (şema + retry politikası), kalıcı süreç iyileştirmeleri ve blameless kültür. Ayrıca teknik derinlik (poison pill, DLT, şema uyumluluğu) hikayenin içinde doğal olarak geçiyor.

Takip sorusu: Rollback mümkün olmasaydı, örneğin şema değişikliği veritabanına da yansımış olsaydı, ne yapardın?

Sorulmadı ama sorulabilir

Bildirim sistemi tasarımı (kısa)

"Sipariş durumu değişince kullanıcıya push, SMS ve e-posta gönder." Ana fikirler: sipariş servisi event yayınlar (outbox ile), bildirim servisi tüketir; kanal başına ayrı kuyruk/topic ki yavaş SMS sağlayıcısı push'u bekletmesin; kullanıcı tercihleri ve sessiz saatler; şablonlar; aynı event'in tekrar gelmesine karşı event_id ile tekilleştirme (aksi halde aynı SMS iki kez gider ve para tutar); sağlayıcı hatasında backoff ile retry, sınır aşılırsa DLT; sağlayıcı rate limit'lerine uymak için çıkış tarafında throttling; gönderim durumu takibi ve teslim raporu (webhook). Ölçek: kampanya bildirimlerinde milyonlarca mesajın sağlayıcı limitlerine göre yayılarak gönderilmesi.

Tek cümle: "Event tabanlı, kanal başına izole, idempotent ve sağlayıcı limitine göre throttle eden bir bildirim hattı kurarım."

Cache stratejileri ve cache stampede

Cache-aside (uygulama önce cache'e bakar, yoksa DB'den okuyup yazar; en yaygın), read-through/write-through (cache katmanı DB'yi kendisi okur/yazar), write-back (yazma önce cache'e, DB'ye sonra; hızlı ama veri kaybı riski). Stampede: Popüler bir anahtarın süresi dolunca binlerce istek aynı anda DB'ye gider. Önlemler: aynı anahtar için tek yükleme (request coalescing; Caffeine get(key, loader), dağıtıkta kısa kilit), süresi dolmadan arka planda yenileme (refreshAfterWrite), TTL'lere rastgele sapma (jitter) ekleyip toplu süre dolmasını önlemek.

Tek cümle: "Varsayılanım cache-aside; popüler anahtarlarda stampede'e karşı tek yükleme, erken yenileme ve TTL jitter kullanırım."

Partition/shard anahtarı seçimi

Kafka partition key'i, veritabanı shard anahtarı ve Redis hash tag'i aynı soruyu sorar: hangi veriler birlikte olmalı (sıralama, atomiklik, join) ve yük eşit dağılıyor mu? Sipariş event'lerinde orderId key'i sipariş içi sıralamayı korur ve iyi dağılır; sellerId büyük satıcılarda hot partition yaratır. Kural: erişim desenine göre seç, kardinalite ve çarpıklığı (skew) ölç, sıcak anahtarlar için anahtarı tuzla (salt) ya da ayrı ele al.

Tek cümle: "Partition anahtarını sıralama ve birlikte erişim gereksinimine göre seçer, çarpıklığı ölçer, sıcak anahtarları ayrıca ele alırım."

"Bu kod tabanında nereyi iyileştirirsin?" (case study review)

Senior varyasyonunda yarım kalmış bir case study'yi inceleyip iyileştirme önermen istenebilir. Yapı: önce çalıştır ve testleri koş; sonra riske göre sırala (doğruluk ve veri bütünlüğü, güvenlik, hata yönetimi, test eksikliği, sonra tasarım ve okunabilirlik); her öneri için "şu an ne riski var, ne kadar sürer" söyle; hepsini yapmaya kalkma, en yüksek etki/maliyet oranlı ikisini canlı yap. "Neden CQRS kullandın?" gibi sorulara hazır ol: her mimari kararın bir gerekçesi ve bir bedeli olmalı; "kullanmasaydım ne olurdu?" sorusunu kendine önceden sor.

Tek cümle: "İyileştirmeleri risk ve maliyete göre sıralar, en yüksek getirili iki tanesini testleriyle canlı yaparım, geri kalanını gerekçesiyle listelerim."

Trunk-based development ve feature flag'ler

Uzun yaşayan branch'ler merge conflict ve entegrasyon riskini biriktirir. Trunk-based development'ta herkes küçük değişikliklerle sık sık ana branch'e merge eder; bitmemiş işler feature flag arkasında kapalı durur. Flag'ler kademeli açma (yüzde 1, 10, 100), anında geri alma ve A/B testi sağlar. Bedeli: flag borcu; kullanılmayan flag'ler düzenli temizlenmelidir, aksi halde kod test edilmesi zor kombinasyonlarla dolar.

Tek cümle: "Küçük ve sık merge, bitmemiş işi flag arkasında tutmak; flag'leri de teknik borç olarak takip edip temizlemek."

Teknik anlaşmazlık

"Takım arkadaşınla teknik bir konuda anlaşamadığın bir durum?" STAR ile, ama odak süreçte: varsayımları ayırmak, karşı tarafın gerekçesini kendi cümlenle tekrar etmek, kararı veriye dayandırmak (küçük bir prototip, benchmark, yük testi), karar verilince "disagree and commit", ve kararı yazılı bir ADR'ye (Architecture Decision Record) dökmek. Kaybettiğin bir tartışmayı anlatmak, kazandığını anlatmaktan çoğu zaman daha güçlü bir sinyaldir.

Tek cümle: "Anlaşmazlığı kişiden ayırıp veriyle çözerim; karar çıkınca katılmasam da arkasında dururum ve kararı yazılı hale getiririm."

Sık yapılan hatalar

  • Sistem tasarımında netleştirmeden çizime başlamak → Yanlış problemi tasarlarsın, en zor kısma süre kalmaz → İlk 5 dakika gereksinim ve tahmin, sonra yönlendirmeli ilerle.
  • Her kutuya bir teknoloji adı koyup mekanizmayı anlatmamak → "Kafka koyarım" bir cevap değildir → Neden, nasıl (key, idempotent consumer), ne bozulursa ne olur.
  • Tek doğru kaynağı belirsiz bırakmak → Stok kararı hem cache'ten hem DB'den verilir, tutarsızlık → Karar tek bir atomik noktada, diğerleri türetilmiş ve uzlaştırılmış.
  • Güvenilmez mekanizmaya para değerinde iş bağlamak → Keyspace notification kaybolur, stok iade edilmez → Sıralı küme + periyodik atomik süpürücü ya da kalıcı zamanlanmış iş.
  • Başarısızlık senaryolarını atlamak → Tasarım sadece mutlu yolda çalışır → Her bileşen için "düşerse ne olur" ve gözlemlenebilirlik.
  • PR'da stil yorumlarıyla blocker'ları karıştırmak → Yazar neyin önemli olduğunu ayırt edemez → Ciddiyet etiketi; stil linter'a.
  • Kişiyi eleştiren yorum ("Bunu neden böyle yaptın?") → Savunmacılık, review kültürü bozulur → Kodu konuş, ne-neden-nasıl, öneriyle gel.
  • WIP/fix commit mesajları, sonda tek dev commit → Geçmiş okunamaz, geri alma ve bisect imkansız → Atomik, yeşil, nedeni anlatan commit'ler.
  • Live coding'de sessiz kalmak → Mülakatçı düşünceni göremez → Sesli düşün, her 15-20 dakikada durum özeti.
  • Build/bağımlılık problemiyle uzun süre boğuşmak → Görev zamanı erir → Söyle, alternatif yol (elle yaz), devam et.
  • Mülakatçının önerisine direnmek → İş birliği sinyali düşer → Gerekçeni söyle, sonra birlikte karar ver.
  • Davranışsal soruda "biz" ile anlatmak, sonucu ölçmemek → Katkın görünmez → "Ben", sayılarla sonuç, öğrenme.
  • Hikaye uydurmak ya da süslemek → Takip sorularında çöker → Gerçek hikayeleri önceden yapılandır.
  • Soru sormadan bitirmek → İlgisizlik sinyali → İşle ilgili 2-3 somut soru hazırla.

Hızlı tekrar

S: Sistem tasarımı mülakatının adımları?

C: Requirements → estimates → API → data model → high-level design → deep dive (en zor 1-2 problem) → bottlenecks ve failure'lar; trade-off'ları ve kapsam dışını söyle.

S: Flash sale tasarımının ana fikri nedir?

C: Trafiğin büyük çoğunluğu başarısız olacak; onu kenarda (CDN, rate limit, bekleme odası) ucuzca reddet, stok kararını tek bir atomik noktada (Redis Lua) ver, veritabanı constraint'lerini son savunma yap.

S: Redis Lua script'i neden kilitsiz overselling önler?

C: Redis script'i atomik çalıştırır; "alıcı mı, stok var mı, düş, kaydet" adımları arasına başka komut giremez. Anahtarlar hash tag ile aynı slot'ta olmalıdır.

S: Neden stok iadesini Redis keyspace notification'a bağlamamalısın?

C: Pub/Sub fire-and-forget'tir (bağlı olmayan abone event'i kaçırır) ve expired event'i TTL sıfırlandığında değil anahtar fiilen silindiğinde üretilir. Sıralı küme + periyodik atomik süpürücü güvenilirdir.

S: Rezervasyon süresi dolduktan hemen sonra gelen başarılı ödeme nasıl ele alınır?

C: Ödeme iki aşamalı: provizyon → rezervasyonu atomik onayla → onay başarılıysa tahsilat, değilse provizyon iptali. Böylece para alınıp ürün verilmemesi durumu oluşmaz.

S: Stoğu kovalara bölmek "müşteri başına bir" kuralını neden bozabilir, nasıl korunur?

C: Alıcı kontrolü kovanın kümesindedir; kullanıcı farklı kovalara giderse iki kez alabilir. Kullanıcı hash(userId) % N ile deterministik kovaya yönlendirilir; DB'deki UNIQUE (sale_id, user_id) son savunmadır.

S: Redis'te WAIT failover'daki yazma kaybını tamamen önler mi?

C: Hayır. Replica onayını bekleyerek riski azaltır ama Redis'i güçlü tutarlı yapmaz. Asıl güvence DB constraint'leri ve uzlaştırmadır.

S: PR review'da öncelik sırası?

C: Güvenlik → doğruluk ve veri bütünlüğü → performans → tasarım ve okunabilirlik → stil (linter'a bırak).

S: İyi bir review yorumu nasıl yazılır?

C: Ciddiyet etiketiyle (blocking/suggestion/nit), kişiyi değil kodu konuşarak, ne-neden-nasıl ile ve mümkünse kod önerisiyle; iyi olanı da söyleyerek.

S: İyi commit'in kriterleri?

C: Atomik (tek mantıksal değişiklik), her commit derlenir ve testleri geçer (bisect), mesaj nedeni anlatır, refactor ile davranış değişikliği ayrı; Conventional Commits formatı.

S: Conventional Commits'te hangi tipler spesifikasyonda tanımlı, uyumsuz değişiklik nasıl belirtilir?

C: feat (minor) ve fix (patch); diğer tipler yaygın gelenektir. Uyumsuz değişiklik BREAKING CHANGE: footer'ı ya da tipten sonra ! ile (major).

S: Live coding'in ilk 10 dakikasında ne yapılır?

C: Build et ve testleri çalıştır, yapıyı gez, belirsizlikleri sor, görevleri önceliklendir ve planı sesli söyleyip onay al.

S: STAR nedir, hikayenin ağırlığı nerede olmalı?

C: Situation, Task, Action, Result. Ağırlık "ben" ile anlatılan Action'da (teşhis, seçenekler, gerekçe); Result ölçülebilir olmalı ve öğrenilen/kalıcı değiştirilen şeyi içermeli.

Kaynaklar