Kısım II · İnşaat · Bölüm 45

Uçtan Uca

Uygulamamızın tam haritası (Our app, end to end)

Neden burada · Neye dayanıyor

Neden burada: Kısım II boyunca parçaları tek tek öğrendin: dil, tarayıcı, Git, TypeScript, React Native, API, Go, Postgres, Redis, güvenlik, test, yayın, mimari. Her parça kendi bölümünde anlamlıydı ama bir şey eksik kaldı: hepsi aynı anda çalışırken ne olur? Bu bölüm tek bir kullanıcı eylemini, bir cevaba dokunmayı, baştan sona izleyerek parçaları birbirine bağlar. Bir önceki bölümde (Mimariler) parçaların nasıl bir bütün oluşturabileceğine dair büyük kararları gördün; burada bizim kararımızın canlı çalışan hâlini okuyorsun.

Neye dayanıyor: Neredeyse bütün Kısım II (Bölüm 21–Bölüm 44), ama özellikle şunlar: HTTP isteği ve JSON (Bölüm 35), Go handler (Bölüm 37), işlem ve ACID (Bölüm 39, 39.3), Redis sıralı küme (Bölüm 40, 40.2), HTTPS (Bölüm 20, 20.4) ve katmanlı mimari (Bölüm 44, 44.4). Bu bölüm yeni kavram getirmez; var olanları sıraya dizer. Bir adımda takılırsan, adımın yanındaki bölüm bağlantısı seni o kavramın asıl anlatımına götürür.

  1. Kat 10Bizim uygulamamızBu bölümün konusu: bütün alt katlar tek bir dokunuşta devreye giriyor.
  2. Kat 9Web ve tarayıcıİstemci tarafı: HTML, CSS, JS; mobilde aynı fikir, React Native üzerinden. Bölüm 20, 23–26, 31–33.
  3. Kat 8Ağ ve internetDNS, TCP, TLS, HTTP: istek ve yanıt buradan geçer. Bölüm 19–20, 35.
  4. Kat 7İşletim sistemiTelefonda ve sunucuda süreçleri, ağ bağlantısını, dosyaları yönetir. Bölüm 13–18.
  5. Kat 5–6Diller ve gömülü yazılımTypeScript/JS ve Go kodu çalışabilir hâle gelir. Bölüm 9–12, 21, 30, 36.
  6. Kat 0–4Donanım, makine diliHer adımın en altında hâlâ bitler, işlemci komutları ve bellek var. Bölüm 2–8.

Bu bölümde dört şey yapacağız. Önce bir cevabın yolculuğunu dokuz durakta izleyeceğiz; her durakta hangi katın çalıştığını ve hangi bölüme ait olduğunu söyleyeceğiz (45.1). Sonra aynı yolculuğu bir mimari çizim olarak tek sayfaya sığdıracağız (45.2). Üçüncü olarak, yolculuğun taşıdığı verinin ve oyunlaştırmanın (oyunlaştırma (gamification)) şeklini çizeceğiz (45.3). Son olarak, her parça için "takılırsam ne öğrenmeliyim" listesini çıkaracağız (45.4). Bu liste, Kerem'in Kısım II'den çıkarken eline alacağı pratik haritadır.

Bir uyarı: bu plan, uygulama değil

Kerem'in prototipinin kodunu görmedik; gerçek uygulamanın ekran adlarını, tablo adlarını, uç nokta adlarını bilmiyoruz. Bu bölümdeki her ad ve kod parçası, planlanan yığının (Expo + TypeScript, TanStack, Go, Postgres, Redis) tipik bir kurulumunu göstermek için örnek olarak yazıldı. Gerçek proje başka adlar kullanabilir; önemli olan adlar değil, sıra ve kat fikridir. Go parçasını gerçekten derleyip çalıştırdım (sahte veritabanı ve sahte Redis ile); TypeScript, SQL ve Redis parçaları taslaktır ve bu bölümde çalıştırılmadı.

45.1 Bir cevabın yolculuğu

Senaryo: Kerem'in kullanıcısı "Hukuka Giriş" bölümünde 17 numaralı soruyu görüyor, üç şıktan ikincisine (indeks 2) dokunuyor ve "Kontrol et" düğmesine basıyor. Yarım saniye sonra ekranda yeşil bir "Doğru! +10 puan" yazısı beliriyor, seri sayacı 5'ten 5'te kalıyor (bugün zaten ilk cevap değil), puan 340'a çıkıyor ve sıralamada 12. oldu. Bu yarım saniyede ne oldu? Dokuz durakta izleyelim.

Durak 1: Parmak, cam ve işletim sistemi (Kat 0–7)

Dokunmatik ekran, parmağının elektriksel etkisini bir koordinata (x, y) çevirir. Bu yalnızca sayıdır: Bölüm 2'deki gibi, bitler. Telefonun işletim sistemi bu koordinatı alır, hangi uygulamanın hangi penceresinde olduğunu bulur ve uygulamaya bir olay olarak iletir. Sürücü ve giriş/çıkış katmanı (Bölüm 17) bu işin donanım tarafıdır; "olay" fikri ise Bölüm 26'daki click olayının mobil karşılığıdır. Sen bu durakta hiçbir şey yazmadın; iOS ve Android yazdı. Ama dokunmanın kodunun çalışması için bir süreç (Bölüm 14) olarak uygulamanın telefonun belleğinde (Bölüm 15) yüklü olması gerekir.

Durak 2: React Native bileşeni (Kat 9–10, istemci)

Uygulama React Native ile yazılmış bir bileşen (component)dir (Bölüm 31, Bölüm 32). Düğmenin onPress özelliğine bir fonksiyon bağlanmıştır; olay geldiğinde o fonksiyon çalışır. React'ın ana fikrini hatırla: ekran, durumun (state) bir fonksiyonudur. Fonksiyon durumu değiştirirse ("cevap gönderiliyor"), React ekranı yeniden çizer. TypeScript (Bölüm 30) burada sessiz bir bekçidir: cevap isteğinin şekli { questionId: number; choice: number } diye tanımlıysa, yanlışlıkla metin gönderen kod derlenmez bile.

// Taslak: ekran bileşeninin içinde (çalıştırılmadı)
const gonder = useCevapGonder();            // Durak 3'te anlatılacak
<Button title="Kontrol et" onPress={() =>
  gonder.mutate({ questionId: 17, choice: 2 })
} />

Durak 3: TanStack Query değişikliği (Kat 10, istemci)

Sunucuyla konuşan kodu doğrudan bileşene gömmek yerine, Bölüm 33'te gördüğümüz TanStack Query'nin mutasyon (mutation) mekanizması kullanılır. TanStack Query belgeleri, bir mutasyonun sunucudaki veriyi değiştiren bir işlem olduğunu ve onSuccess seçeneğiyle, sorgu istemcisinin invalidateQueries ve setQueryData yöntemleriyle birleşince güçlü bir araç olduğunu söyler.1 Bu bize iki şey verir: istek sürerken "yükleniyor" durumunu bedavaya alırız ve cevap gelince ekrandaki puan, seri gibi "sunucu verisini" tek hamlede tazeleyebiliriz.

// Taslak (çalıştırılmadı)
function useCevapGonder() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (g: CevapIstegi) => apiPost<CevapYaniti>("/api/answers", g),
    onSuccess: () => qc.invalidateQueries({ queryKey: ["profil"] }),
  });
}

Durak 4: fetch ve HTTP isteği (Kat 8–9)

apiPost sonunda fetch() çağırır. MDN'nin tarifine göre fetch() bir söz (Promise) döndürür; söz, sunucunun yanıtını temsil eden bir Response nesnesiyle yerine gelir ve gövdeyi JSON olarak okumak için response.json() çağrılır.2 Arka planda tarayıcının (mobilde React Native'in) ağ katmanı bir HTTP isteği kurar (Bölüm 35):

POST /api/answers HTTP/1.1
Host: api.ornek-hukuk-app.com
Authorization: Bearer <belirteç>
Content-Type: application/json

{"questionId":17,"choice":2}

Gövde 28 byte (bunu len() ile saydım; Bölüm 2'deki gibi, her ASCII karakter 1 byte). Authorization başlığı, kullanıcının kim olduğunu söyleyen belirteçtir (Bölüm 41, 41.3). Alan adı api.ornek-hukuk-app.com uydurmadır; gerçek adres Bölüm 43'te anlatılan DNS kaydıyla belirlenir.

Durak 5: Ağ ve HTTPS (Kat 8)

İstek, telefondan bir yere gitmeden önce üç şey olur. Önce alan adı, DNS ile bir IP adresine çevrilir (Bölüm 19). Sonra TCP bağlantısı kurulur. Sonra TLS el sıkışması ile bağlantı şifrelenir; sertifika, karşıdakinin gerçekten o alan adının sahibi olduğunu kanıtlar (Bölüm 20, 20.4). Yani istek, yoldaki yönlendiricilerin ve kafedeki Wi-Fi'ın okuyamayacağı bir zarfın içinde gider. Parçalar paketlere bölünür, paketler yönlendiricilerden atlayarak sunucunun işletim sistemine ulaşır, sunucunun ağ katmanı onları birleştirir ve dinleyen programa, yani Go sunucumuza verir. Bu durakta senin kodun yok; ama bu durak olmasa hiçbir şey çalışmaz.

Durak 6: Go handler (Kat 5, sunucu)

Go sunucusu, standart kütüphanedeki net/http paketiyle bir adreste dinler; http.HandleFunc belirli bir yola (ör. /api/answers) bir fonksiyon bağlar.3 İstek gelince o fonksiyon, yani handler (handler), çalışır (Bölüm 37). Görevi sırayla: kimlik doğrula, gövdeyi çöz, veriyi doğrula, işi yap, yanıt yaz. Aşağıdaki program bu iskeleti sahte parçalarla gösterir; derleyip go vet ile denetledim ve çalıştırdım:

func (s *server) postAnswer(w http.ResponseWriter, r *http.Request) {
    userID := 42 // gerçekte kimlik doğrulama katmanından gelir (Bölüm 41)
    var req answerReq
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "bad json", http.StatusBadRequest)   // 400
        return
    }
    res, err := s.store.SaveAnswer(r.Context(), userID, req) // Postgres (Durak 7)
    if err != nil {
        http.Error(w, "server error", http.StatusInternalServerError) // 500
        return
    }
    if res.PointsEarned > 0 {
        if rank, err := s.board.AddScore(r.Context(), userID, res.PointsEarned); err == nil {
            res.Rank = rank                                  // Redis (Durak 8)
        }
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(res)                           // JSON yanıt (Durak 9)
}

İki ince karar var. Birincisi, Store ve Board birer arayüzdür (Bölüm 27, Bölüm 44): handler Postgres'in ya da Redis'in ne olduğunu bilmez, yalnızca "cevabı kaydet" ve "puan ekle" der. Bu, test yazmayı (Bölüm 42) kolaylaştırır: gerçek veritabanı yerine sahte bir Store verirsin. İkincisi, sıralama güncellemesi başarısız olursa (err != nil) cevap yine de kaydedilmiş kalır; çünkü Redis'teki sıralama, Postgres'teki gerçeğin önbellekteki bir görüntüsüdür; kaybolursa yeniden kurulabilir. Bu düşünce biçimini Bölüm 40'da "önbellek ne zaman kaybedilebilir" sorusunda görmüştün.

Durak 7: Postgres işlemi (Kat 5, veri katmanı)

Doğru mu yanlış mı bilmek ve kaydetmek için veritabanına gidilir. Bir cevap birden çok değişiklik demektir: answers tablosuna satır eklemek, kullanıcının toplam puanını artırmak, günlük hedef ve seri alanlarını güncellemek, yanlışsa canı düşürmek. PostgreSQL belgelerinin tarifiyle, işlem (transaction) birden çok adımı tek bir hep-ya-da-hiç işlemde birleştirir; ara durumlar eşzamanlı diğer işlemlere görünmez ve bir hata olursa adımların hiçbiri veritabanını etkilemez.4 Banka örneğindeki "Alice'ten düş, Bob'a ekle" ikilisi bizde "cevabı yaz, puanı artır" ikilisidir: puan artıp cevap yazılmadıysa, kullanıcı hem puanı alır hem de aynı soruyu tekrar çözerek bir daha alabilir. (Bölüm 39, 39.3)

-- Taslak (çalıştırılmadı): Go tarafında tek bir işlem içinde
BEGIN;
  INSERT INTO answers (user_id, question_id, choice, correct)
       VALUES ($1, $2, $3, $4);
  UPDATE users
     SET total_points = total_points + $5,
         streak       = $6,
         last_active  = CURRENT_DATE
   WHERE id = $1
  RETURNING total_points, streak, hearts;
COMMIT;

$1, $2 gibi yer tutucular parametreli sorgudur (Bölüm 39, 39.5): kullanıcıdan gelen değerler SQL metnine yapıştırılmaz, ayrı gönderilir; böylece SQL enjeksiyonu olmaz (Bölüm 41, 41.4). Doğru cevap bilgisinin questions tablosunda sunucuda durduğuna dikkat et: istemci "doğru" demez, "ikinci şıkkı seçtim" der; doğruluğu sunucu hesaplar. İstemciye güvenirsen herkes puanını kendisi yazar.

Durak 8: Redis leaderboard (Kat 5, önbellek)

Postgres puanı doğru ve kalıcı olarak saklar. Ama "şu an kaçıncıyım?" sorusunu her istekte tüm kullanıcıları sıralayarak cevaplamak, kullanıcı sayısı arttıkça yavaşlar. Redis'in sıralı kümesi (sıralı küme (sorted set)) bunun için vardır (Bölüm 40, 40.2). Redis belgelerine göre ZINCRBY key increment member, sıralı kümedeki bir üyenin puanını artırır ve karmaşıklığı O(log N)'dir; ZRANGE komutu REV seçeneğiyle en yüksekten alçağa sıralı okur.56

ZINCRBY leaderboard:haftalik 10 kullanici:42     # puanı 10 artır
ZRANGE  leaderboard:haftalik 0 9 REV WITHSCORES  # ilk 10 (taslak, çalıştırılmadı)

Bu iki komutun adlarını örnek olarak yazdım; hangi anahtar adının kullanılacağı bizim kararımızdır. Önemli olan: sıralama, hesaplanan bir şey değil, sürekli sıralı tutulan bir veri yapısıdır; ekleme sırasında yerini bulur (Bölüm 11'deki ağaç ve sıralı yapılar fikrinin bellek içi sürümü).

Durak 9: JSON yanıt ve ekranın değişmesi (Kat 8–10)

Go, sonucu JSON'a çevirip yazar (json.NewEncoder(w).Encode). Çalıştırdığım örnekte yanıt şuydu ve 85 byte (satır sonu dahil) tuttu:

HTTP 200
{"correct":true,"pointsEarned":10,"totalPoints":340,"streak":5,"hearts":3,"rank":12}

Yanıt aynı şifreli bağlantıdan geri döner, telefonun ağ katmanı onu birleştirir, fetch'in sözü yerine gelir, TanStack Query onSuccess'i çağırır, "profil" sorgusunu geçersiz kılar (yenilenir), React durumun değiştiğini görür ve ekranı yeniden çizer: yeşil "Doğru! +10" ve yeni puan. Dokunuştan bu ana kadar geçen süre, iyi bir ağda genellikle bir saniyenin altındadır; bunun büyük kısmı ağın ışık hızıyla ilgili gecikmesidir (Bölüm 19), Go ve veritabanı kısmı çoğunlukla milisaniyelerdir.

DurakNe oluyorHangi parçaHangi bölüm
1Dokunuş, olayDokunmatik ekran, telefon işletim sistemiBölüm 17, Bölüm 26
2onPress çalışırReact Native bileşeni, TypeScript tipiBölüm 30, Bölüm 31, Bölüm 32
3Mutasyon başlarTanStack QueryBölüm 33
4HTTP isteği kurulurfetch, JSON gövde, belirteçBölüm 35, Bölüm 41
5Şifreli taşımaDNS, TCP, TLS (HTTPS)Bölüm 19, Bölüm 20
6Handler çalışırGo, net/httpBölüm 36, Bölüm 37
7Cevap ve puan yazılırPostgres işlemiBölüm 38, Bölüm 39
8Sıralama güncellenirRedis sıralı kümeBölüm 40
9JSON döner, ekran tazelenirHTTP yanıtı, TanStack, ReactBölüm 33, Bölüm 35

Dokuz durağa baktığında bir örüntü fark etmelisin: her durak bir alt katın sunduğu imkânı kullanır ve bir üst kata daha basit bir şey sunar. Go handler'ı yazan kişi paketlerin nasıl yönlendirildiğini düşünmez (Durak 5 onu gizler); React bileşenini yazan kişi TCP'yi düşünmez. Kat planının işe yaradığı yer burasıdır: bir hata olduğunda, hatanın hangi durakta olduğunu bulmak, çözümün yarısıdır.

Uygulamamızda

Bir agent'a "cevap gönderince puan güncellenmiyor" dediğinde, haritayı bilen biri hemen şunu sorar: istek hiç çıktı mı (Durak 4)? Sunucuya ulaştı mı (5–6)? Veritabanına yazıldı mı (7)? Yanıt döndü mü ve ekran bu yanıtı kullandı mı (9)? Her soru, tek bir durağı yoklar. Agent'a körü körüne "düzelt" demek yerine "önce Durak 6'daki handler'ın log'unu göster" diyebilirsin. Bu, Bölüm 46'daki agent yönetiminin temelidir.

45.2 Mimari çizim: hangi parça nerede çalışır

Yolculuğu dokuz durak olarak anlattık. Şimdi aynı şeyi bir harita olarak çizelim: hangi parça hangi bilgisayarda çalışır? Bu soru Bölüm 44'ün "mimari" kelimesinin somut hâli: değiştirmesi pahalı kararlar, yani parçaların yerleri ve aralarındaki sınırlar.

  KULLANICININ TELEFONU                      İNTERNET                    SUNUCU (bulutta) 
 ┌───────────────────────────┐                                 ┌────────────────────────────────────┐
 │ Expo / React Native (TS)  │                                 │ Go programı (tek süreç, monolit)   │
 │  ├ ekranlar (bileşenler)  │      HTTPS (DNS+TCP+TLS)        │  ├ HTTP katmanı (handler'lar)      │
 │  ├ TanStack Query         │ ───── POST /api/answers ──────► │  ├ iş mantığı (puan, seri, can)    │
 │  │   (sunucu verisi       │       JSON, belirteç            │  └ veri katmanı ──┐   ┌─────────┐   │
 │  │    önbelleği)          │ ◄──── 200 + JSON ────────────── │        │          │   │         │   │
 │  └ fetch                  │                                 └────────┼──────────┼───┼─────────┼───┘
 └───────────────────────────┘                                          ▼          ▼   ▼         ▼
                                                                  ┌────────────┐  ┌────────────┐
   Geliştirici tarafı:  Git (worktree) ─ agent'lar ─ AGENTS.md    │ PostgreSQL │  │   Redis    │
   test, CI, konteyner → yayın (Bölüm 42–43)                      │ gerçek veri│  │ sıralama   │
                                                                  │ (kalıcı)   │  │ (önbellek) │
                                                                  └────────────┘  └────────────┘

Çizimden okunacak beş şey:

  1. Üç ayrı bilgisayar türü var: kullanıcının telefonu (senin kontrolünde değil, yüz binlerce farklı cihaz), sunucu (senin kontrolünde, kiralanan makine ya da konteyner) ve ikisinin arasındaki internet (kimsenin kontrolünde değil). Güvenlik düşüncesi bu üçlüden doğar: telefona asla güvenme (Bölüm 41), internete "şifreli zarf" olmadan bir şey emanet etme (Bölüm 20).
  2. Sunucuda tek bir Go programı var: Bölüm 44'ün diliyle monolit ya da daha doğrusu modüler monolit. Mikroservis yok. İki kişilik, agent'larla çalışan bir ekip için bu akıllıca bir başlangıçtır; çünkü her ek servis, ağ hatası, ayrı yayın ve ayrı izleme demektir.
  3. Katmanlı iç yapı: HTTP katmanı (istek–yanıt ve JSON), iş mantığı (puan nasıl hesaplanır, seri ne zaman sıfırlanır) ve veri katmanı (SQL ve Redis komutları). Bir katman yalnızca altındakini çağırır. Seri kuralını değiştirmek istersen, yalnızca iş mantığı katmanına dokunursun; ekran ve SQL aynı kalır.
  4. Postgres gerçeğin kaynağıdır, Redis ise hız içindir: Redis'i kaybedersen leaderboard'u Postgres'teki puanlardan yeniden kurarsın; Postgres'i kaybedersen kullanıcıların her şeyini kaybedersin. Bu yüzden Postgres için yedekleme (Bölüm 39, 39.6) hayati, Redis için nispeten gevşektir.
  5. Geliştirici tarafı çizimin altında: Git, test, CI ve yayın hattı kullanıcıya görünmez ama kodun telefona ve sunucuya nasıl ulaştığını belirler (Bölüm 28, Bölüm 42, Bölüm 43). Bölüm 46'da agent'lar bu hattın başına otururlar.
Haritayı yanlış okuma

Bu çizim planlanan yığının resmidir, bugünkü durumun değil. Prototip henüz vanilla HTML/CSS/JS ve muhtemelen tarayıcıda çalışan, sunucusuz bir şey. Plan, o prototipin ekranlarını Expo + TypeScript'e taşımak ve sunucu kısmını sıfırdan Go + Postgres + Redis ile kurmaktır. Çizimdeki her kutu bir hedef; hepsini aynı anda kurmak gerekmez. Mesela Redis'i en sona bırakıp ilk sürümde sıralamayı doğrudan Postgres'ten hesaplamak da meşru bir yoldur ({B(40)}, 40.5: ne zaman önbellek gerekmez).

45.3 Bölüm tasarımı, oyunlaştırma ve verinin şekli

Yolculuğun taşıdığı veri nereden geliyor ve neye benziyor? Bir öğrenme uygulamasının kalbi, içerik modelidir: kullanıcıya ne gösterdiğimiz ve onun etkileşimini nasıl kaydettiğimiz. Bölüm 38'de şema tasarımını (kullanıcılar, bölümler, sorular, cevaplar) kavramsal olarak çizdin; burada aynı şemayı uygulamanın dilinde okuyalım.

İçerik: bölüm, soru, şık

Bir hukuk dersi uygulamasında içerik hiyerarşik olur: Ünite (ör. "Hukuka Giriş") altında bölümler (ör. "Hukuk kuralı nedir?"), her bölümün altında kısa sorular, her sorunun altında şıklar. Hiyerarşi bir ağaçtır (Bölüm 11) ve ilişkisel veritabanında yabancı anahtarlarla ifade edilir (Bölüm 38, 38.2): bir soru section_id ile bir bölüme, bir şık question_id ile bir soruya bağlıdır. Soru metinleri ve şıklar kullanıcıdan bağımsız, herkes için aynıdır; kullanıcıya özel olan yalnızca cevaplar ve onlardan türeyen puan, seri ve can durumudur.

-- Taslak şema (Postgres sözdizimi; çalıştırılmadı)
CREATE TABLE users (
  id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  display_name  text    NOT NULL,
  total_points  integer NOT NULL DEFAULT 0,
  streak        integer NOT NULL DEFAULT 0,
  last_active   date,
  hearts        integer NOT NULL DEFAULT 5
);
CREATE TABLE sections  (id bigint PRIMARY KEY, title text NOT NULL, position integer NOT NULL);
CREATE TABLE questions (id bigint PRIMARY KEY, section_id bigint REFERENCES sections(id),
                        body text NOT NULL, correct_choice integer NOT NULL);
CREATE TABLE answers   (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
                        user_id bigint REFERENCES users(id),
                        question_id bigint REFERENCES questions(id),
                        choice integer NOT NULL, correct boolean NOT NULL,
                        answered_at timestamptz NOT NULL DEFAULT now());

Şemada iki karar öğretici. correct_choice sorular tablosunda durur; istemciye gönderilmez (yoksa kullanıcı ağ isteğini inceleyip doğru şıkkı okur; Bölüm 41). Ve answers her cevabı saklar, yalnızca "sonuç"u değil: geçmişi saklamak, sonradan "hangi soru en çok yanlış yapılıyor?" gibi soruları cevaplamayı mümkün kılar. Verinin şekline karar verirken genel kural: bugün sormak istemediğin ama yarın sormak isteyebileceğin şeyi atma; ama ihtiyacın olmayan yığınlarca şeyi de saklama.

Oyunlaştırma: puan, seri, can, leaderboard

Oyunlaştırma (gamification), oyun mekaniklerini (puan, rozet, sıralama, seri) oyun olmayan bir bağlama, burada öğrenmeye, taşımaktır. Duolingo benzeri bir uygulamanın dört temel mekaniği ve her birinin teknik karşılığı şöyle:

MekanikNe yaparVeri neredeHesap nerede
PuanDoğru cevaba ödülusers.total_points (Postgres)Go iş mantığı, işlem içinde
Seri (streak)Art arda gün sayısıusers.streak, last_activeGo: tarih karşılaştırması
Can (hearts)Yanlış cevap bedeliusers.heartsGo: yanlışta düş, sıfırsa engelle
LeaderboardKullanıcıları puana göre sıralarRedis sıralı küme + PostgresRedis: ZINCRBY, ZRANGE

Seri hesabı güzel bir küçük örnektir; çünkü basit görünür ama tarih mantığı gerektirir. Kural: bugün zaten cevap verildiyse seri aynı kalır; son cevap dündüyse seri bir artar; daha eskiyse seri 1'e döner. Bu kuralı Python ile yazıp 13 Kasım 2026 "bugün" kabul edilerek denedim:

son cevap      önceki seri   yeni seri
2026-11-13         5            5     (bugün zaten cevaplamış)
2026-11-12         5            6     (dün: seri sürüyor)
2026-11-10         5            1     (iki gün boşluk: seri koptu)
hiç yok            0            1     (ilk cevap)

Bu kuralın nerede yaşadığı bir mimari sorudur. Eğer kural telefonda hesaplanırsa, kullanıcı saatini ileri alarak seriyi şişirebilir; bu yüzden hesap sunucuda yapılır ve "hangi gün"ün hangi saat dilimine göre sayıldığı açıkça belirlenir. Hukuk okumuş biri için tanıdık bir ilke: tarih ve süre hesabını tarafların beyanına değil, tek bir yetkili kayda göre yaparsın. Burada yetkili kayıt sunucunun saatidir.

Verinin yolculuktaki şekli

Aynı bilgi yolculuk boyunca beş farklı biçimde yaşar. Telefonda TypeScript nesnesi (Bölüm 25, Bölüm 30); ağda JSON metni (UTF-8 byte'ları, Bölüm 2); Go'da struct (Bölüm 36); Postgres'te tablo satırı; Redis'te sıralı kümede (üye, puan) çifti. Her sınırı geçerken bir dönüşüm olur (nesneyi JSON'a çevir, JSON'u struct'a çöz, struct'ı SQL parametrelerine ver). Hataların büyük kısmı bu dönüşüm sınırlarında doğar: alan adı büyük–küçük harf farkı (questionId ile question_id), sayının metin olarak gelmesi, tarihin saat dilimi. Bölüm 2'deki ilke burada tekrar çalışır: anlam bağlamdadır; aynı değeri her kat kendi kuralıyla okur.

Uygulamamızda

Prototipinde puan ve seri muhtemelen tarayıcının localStorage'ında ya da bir JavaScript değişkeninde duruyor (görmedik, o yüzden "muhtemelen"). Bu, tek kullanıcılı bir uygulama için yeterli; ama leaderboard'un tanımı gereği birden çok kullanıcının verisini bir araya getirmesi gerekir. Yani leaderboard, uygulamayı "tek bilgisayarda çalışan program"dan "paylaşılan veriye sahip sunucu uygulaması"na çeviren özelliktir. Go + Postgres + Redis'in neden gerektiği tam da buradan çıkar: veri artık bir telefonda değil, herkesin ortak gerçeğinde yaşamak zorunda.

45.4 Neyi bilmeden sürüklenmezsin

Bu kitabın amacı seni her parçada uzman yapmak değil; anlamadığın bir şeyin seni sürüklemesini engellemektir. Aşağıdaki liste, her parça için "bir şey ters giderse en az neyi bilmem lazım" sorusunun cevabıdır. Hepsini ezberleme; takıldığın zaman bu tabloya dön, doğru bölüme git.

ParçaBozulunca ilk görülen belirtiTakılırsam öğrenmem gerekenBölüm
Ekran / React NativeEkran boş, düğme çalışmıyor, "kırmızı ekran"Bileşen, durum (state), olay; hata mesajının hangi dosyayı gösterdiğiBölüm 31, Bölüm 32
TypeScriptDerleme hatası: "tip uyuşmuyor"Tip nedir, hangi nesnenin şekli yanlışBölüm 30
TanStack QueryEski veri görünüyor, güncelleme yansımıyorSorgu anahtarı, geçersiz kılma, önbellekBölüm 33
Ağ / HTTPS"Network request failed", sertifika hatasıİstek–yanıt, durum kodları, DNS, TLSBölüm 19, Bölüm 20, Bölüm 35
Go sunucusu500 hatası, sunucu çöküyorHandler, hata dönüşü, log okumakBölüm 36, Bölüm 37
PostgresVeri kayıp/çift, yavaş sorgu, "deadlock"İşlem, dizin, EXPLAIN, migrationBölüm 38, Bölüm 39
RedisSıralama Postgres ile uyuşmuyorÖnbellek geçersiz kılma, yeniden kurmaBölüm 40
Kimlik, güvenlik401/403, başkasının verisini görmeBelirteç, yetkilendirme, SQL enjeksiyonuBölüm 41
Test, CIBir değişiklik başka yeri bozduBirim/entegrasyon testi, CI neden kırmızıBölüm 42
YayınYerelde çalışıyor, canlıda çalışmıyorOrtamlar, ortam değişkenleri, konteyner, logBölüm 43
Git, worktreeDosyalar karıştı, "merge conflict"Commit, dal, birleştirme, worktreeBölüm 28, Bölüm 46

Tabloyu ikinci bir şekilde de okuyabilirsin: "belirti" sütunu hangi katta aramaya başlayacağını söyler. Bir agent "500 hatası alıyorum" dediğinde, 500 sunucu hatasıdır; yani sorun telefonda değildir, Durak 6–8 arasındadır. "Eski puan görünüyor" derse, sunucu muhtemelen doğru çalışmıştır, TanStack'in önbelleği tazelenmemiştir (Durak 9). Haritayı bilmek, soruyu doğru yere yönlendirmeyi sağlar.

Bir hata ayıklama ritmi

Katlı yapılarda hata ayıklamanın sağlam bir ritmi vardır: ikiye böl. Dokuz durağı tek tek yoklamak yerine ortadan başla: "sunucuya istek ulaşıyor mu?" (sunucu log'u). Ulaşıyorsa sorun Durak 6 ve sonrasındadır; ulaşmıyorsa Durak 1–5. Bu, Bölüm 1'deki telefon rehberinde "ortadan böl" yönteminin, ve Bölüm 11'de ikili aramanın, bire bir aynısıdır. Dokuz durak yaklaşık dört soruda tek bir durağa iner (2⁴ = 16 ≥ 9).

Bölüm 44 ile bağlantı

Bölüm 44 mimariyi "değiştirmesi pahalı kararlar" diye tanımladı. Bu bölümdeki çizimde pahalı kararlar şunlardır: Go'yu seçmek (dil), Postgres'i gerçeğin kaynağı yapmak (veri), istemcinin sunucudan ayrı bir uygulama olması (sınır), monolit ile başlamak (bölünme). Kolay değiştirilebilir olanlar: bir ekranın rengi, bir ucun adı, Redis'in kullanılıp kullanılmaması. Bir kararın pahalı mı ucuz mu olduğunu ayırt etmek, agent'a hangi konuda özgür bırakıp hangisinde kendin karar vereceğini bilmenin yarısıdır.

Bu bölümde neler kuruldu

Bir cevabın yolculuğunu dokuz durakta izledik: dokunuş ve işletim sistemi olayı, React Native bileşeni, TanStack Query mutasyonu, fetch ile HTTP isteği, DNS+TCP+TLS ile şifreli taşıma, Go handler, Postgres işlemi, Redis sıralı küme güncellemesi ve JSON yanıtla ekranın yenilenmesi. Mimariyi üç bilgisayar türüyle (telefon, internet, sunucu) ve sunucu içinde katmanlarla (HTTP, iş mantığı, veri) çizdik; Postgres'i gerçeğin kaynağı, Redis'i hız önbelleği olarak ayırdık. Verinin şeklini (ünite–bölüm–soru–şık hiyerarşisi, kullanıcı ve cevap kayıtları) ve oyunlaştırma mekaniklerinin (puan, seri, can, leaderboard) teknik karşılıklarını gördük; seri kuralını çalıştırarak doğruladık. Son olarak her parça için "takılırsam ne öğrenmeliyim" tablosunu ve ikiye bölerek hata ayıklama ritmini çıkardık.

Terimler sözlükte, Bölüm 45 başlığı altında.

Birincil okuma

Bu bölümün tek bir kaynağı yok; çünkü Expo + Go + Postgres yığınını uçtan uca anlatan tek bir yazılı kaynak bulunmuyor (bkz. RESOURCES.md). Bunun yerine, yolculuğun üç ayağını birincil belgelerinden oku: MDN, Using the Fetch API (istek ve yanıtın istemci tarafı); PostgreSQL belgeleri, Tutorial, Transactions (Durak 7'nin neden hep-ya-da-hiç olduğu); Redis belgeleri, ZINCRBY (Durak 8).

Derinleşmek için: TanStack Query, Mutations ve Go net/http paketi belgesi.

Takıldığın her yeri yapay zekâ öğretmenine sor. Bir öneri: ondan, "Bir kullanıcı 'Kontrol et' düğmesine basınca ne olur?" sorusunun cevabını kendi cümlelerinle dokuz durakta anlatmanı istesin; her durakta "bu hangi katta, hangi bölüme ait?" diye seni yoklasın. Sonra ondan bir hata senaryosu ("puan güncellenmiyor") uydurmasını iste ve hangi durakta arayacağını söyle.

Bölüm sonu soruları

Kitaba bakmadan cevapla. Kâğıt kalem serbest.

1. Kullanıcı cevabı gönderdi ama puan ekranda eski kalıyor; sunucu log'unda 200 ve doğru puan görünüyor. Sorun en olası nerede?
Sunucu doğru yanıt verdiyse Durak 6–8 çalışmıştır. Eski puan, yanıtın ekrana ulaştığı Durak 9'da, yani TanStack Query önbelleğinin tazelenmemesinde aranır. Bkz. 45.1 ve 45.4.
2. Neden doğru şıkkın bilgisi istemciye gönderilmez ve doğruluk sunucuda hesaplanır?
İstemci kullanıcının elindedir; ağ isteği incelenebilir ve kod değiştirilebilir. Doğruluk ve puan hesabı kullanıcının kontrol edemediği sunucuda yapılmalıdır. Bkz. 45.1 (Durak 7) ve 45.3.
3. Cevabı yazmak ve puanı artırmak neden aynı Postgres işleminin içindedir?
İşlem hep-ya-da-hiç çalışır: araya bir hata girse, ya ikisi de yazılır ya da hiçbiri. Aksi hâlde cevap kaydı olmadan puan artabilir. Bkz. 45.1 (Durak 7).
4. Leaderboard için neden sadece Postgres'e güvenmek yerine Redis sıralı kümesi tercih edilir?
Sıralı küme, her puan eklendiğinde üyeyi doğru yerine koyar (ZINCRBY, O(log N)); her istekte tüm kullanıcıları sıralamak gerekmez. Kalıcı gerçek hâlâ Postgres'tedir. Bkz. 45.1 (Durak 8) ve 45.2.
5. Mimari çizimde Redis yeniden kurulabilirken Postgres neden çok daha kritiktir?
Redis'teki sıralama, Postgres'teki puanlardan yeniden üretilebilir; Postgres kaybolursa kullanıcıların gerçek verisi gider. Bu yüzden yedekleme Postgres için hayatidir. Bkz. 45.2.
6. Seri hesabı neden telefonda değil sunucuda yapılır?
Telefonun saati kullanıcı tarafından değiştirilebilir. Yetkili kayıt sunucunun saatidir; seri kuralı orada uygulanır. Bkz. 45.3.
7. "Dokuz durak yaklaşık dört soruda tek durağa iner" cümlesi hangi fikre dayanır?
Her soru adayları yarıya indirir; 2⁴ = 16 ≥ 9. Bu, telefon rehberinde ortadan bölme ve ikili arama fikridir. Bkz. 45.4.
8. Go handler'ında Postgres ve Redis arayüz olarak (Store, Board) tanımlanırsa ne kazanılır?
Handler yalnızca "cevabı kaydet" ve "puan ekle" der; arkada sahte bir uygulama varsa gerçek veritabanı olmadan test edilir. Bkz. 45.1 (Durak 6).
9. Kendi cümlelerinle: "Kontrol et" düğmesine dokunduktan sonra ekranda yeni puanı görene kadar olan yolculuğu, en az altı durak sayarak anlat. Her durak için hangi katta olduğunu söyle.
Örnek cevabı göster

Dokunuş telefonun işletim sistemine olay olarak gelir; React Native bileşeninin onPress fonksiyonu çalışır; TanStack Query mutasyonu fetch ile bir HTTP isteği kurar; istek DNS, TCP ve TLS ile şifreli olarak sunucuya gider; Go handler gövdeyi çözer ve Postgres işleminde cevabı kaydedip puanı artırır; Redis sıralı kümesi güncellenir; sunucu JSON yanıt yazar; telefon yanıtı alır, TanStack Query önbelleği tazeler ve React ekranı yeniden çizer.

10. "Redis'te leaderboard yoksa uygulama yine çalışır ama yavaşlar" cümlesini savun ya da çürüt. Hangi durumda Redis'i hiç kurmamak mantıklı olur?
Örnek cevabı göster

Savunma: Postgres gerçeğin kaynağıdır; sıralama her istekte puanlara göre hesaplanabilir, yalnızca yavaşlar. Kullanıcı sayısı küçükken (yüzler, binler) bu hesap saniyenin çok altında biter ve Redis fazladan bir parça, bir hata kaynağı olur. Kullanıcı sayısı ve sıralama sorgusu sıklığı arttıkça Redis'in sürekli sıralı tutan veri yapısı değer kazanır. Bu yüzden Redis'i ilk sürümde kurmamak ve gerektiğinde eklemek meşru bir tercihtir.

Kaynaklar

  1. TanStack Query belgeleri, Mutations: mutasyon kavramı, onSuccess, invalidateQueries, setQueryData.
  2. MDN, Using the Fetch API: fetch() bir Promise döndürür, Response nesnesi, JSON okuma.
  3. Go, net/http paket belgesi: HandleFunc, ListenAndServe. Bölümdeki Go taslağı bu paketle derlenip çalıştırıldı (go vet ve go run, Go 1.27.2).
  4. PostgreSQL belgeleri, Tutorial: Transactions: işlemin hep-ya-da-hiç olması, ara durumların diğer işlemlere görünmemesi.
  5. Redis belgeleri, ZINCRBY: sıralı kümede üyenin puanını artırır, O(log N).
  6. Redis belgeleri, ZRANGE: REV seçeneği, WITHSCORES.