{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "sade.dev",
  "home_page_url": "https://sade.dev/tr/",
  "feed_url": "https://sade.dev/tr/feed.json",
  "description": "Backend, sistem tasarımı, PostgreSQL, Redis, Laravel ve AI-destekli mühendislik üzerine Türkçe notlar ve geliştirici araçları.",
  "language": "tr-TR",
  "authors": [
    {
      "name": "Muhammet Şafak",
      "url": "https://www.muhammetsafak.com.tr"
    }
  ],
  "items": [
    {
      "id": "https://sade.dev/tr/notes/indeks-yonetimi/",
      "url": "https://sade.dev/tr/notes/indeks-yonetimi/",
      "title": "İndeksler: Eksik Yavaşlatır, Fazlası Yazmayı Öldürür",
      "summary": "İndeks bedava okuma hızı değil, yazmayla ödenmiş okuma hızıdır. Her INSERT tablonun her indeksine bir girdi yazar; her UPDATE de indeksli bir kolona dokunduğu anda aynısını yapar. HOT güncellemesi ve kısmi indeks istisnadır, varsayılan değil. Bu yüzden indeksi gerçek bir sorgu planı istediği için ekleyin, pg_stat_user_indexes idx_scan = 0 gösterdiği için silin.",
      "content_html": "<p>İki karşıt ekip gördüm. Biri tabloya hiç indeks koymamıştı — her sorgu bir sequential scan, her liste sayfası saniyeler sürüyordu. Diğeri tam tersini yapmıştı: “ne olur ne olmaz” diye her kolona bir indeks açmıştı ve şimdi her <code>INSERT</code> ağır aksak ilerliyordu.</p>\n<p>İkisi de aynı yanılgının iki ucu: indeksi ücretsiz bir hız kaynağı sanmak.</p>\n<h2 id=\"i̇ndeks-ücretsiz-hız-değildir\">İndeks ücretsiz hız değildir</h2>\n<p>Bir indeks okumayı hızlandırır, çünkü veritabanı tüm tabloyu taramak yerine sıralı bir yapıya bakar. Ama o sıralı yapı kendi kendine güncel kalmaz: tablodaki bir <code>INSERT</code>, o tablonun <strong>her indeksine</strong> bir girdi yazar; bir <code>UPDATE</code> de indeksli bir kolona dokunduğu anda aynısını yapar. İstisnalar var — indeksli hiçbir kolonu değiştirmeyen HOT güncellemesi indekslere dokunmaz, kısmi indeks yüklemi dışında kalan satırlar için güncellenmez, <code>DELETE</code> ise indeks girdilerini temizlemeyi <code>VACUUM</code>’a bırakır — ama bunlar istisna, varsayılan değil.</p>\n<p>Yani her indeks bir takastır: okuma hızını, yazma maliyetiyle satın alırsınız. Beş indeksli bir tabloya tek satır eklemek, altı yapıyı birden güncellemektir. İndeks “bedava okuma hızı” değil, “yazmayla ödenmiş okuma hızı”dır.</p>\n<h2 id=\"eksik-indeks-tahmin-değil-ölçüm\">Eksik indeks: tahmin değil, ölçüm</h2>\n<p>Eksik indeksi sorgu planından bulun, tahminden değil:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">EXPLAIN ANALYZE</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">SELECT</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> *</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> FROM</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">WHERE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> status</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pending</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<p>Çıktıda büyük bir tabloda <code>Seq Scan</code> görüyorsanız ve sorgu satırların küçük bir kısmını döndürüyorsa, muhtemelen bir indeks eksiktir. <code>pg_stat_user_tables</code> tablosunda <code>seq_scan</code> sayısı yüksek olan büyük tabloları tarayın — bunlar adaylarınızdır.</p>\n<p>İndeksi gerçek, yavaş bir sorgu için ekleyin. “İleride lazım olabilir” diye eklenen indeks, <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> veritabanı katmanındaki hâlidir.</p>\n<h2 id=\"fazla-indeks-kullanılmayanı-bul\">Fazla indeks: kullanılmayanı bul</h2>\n<p>Ters yönde, hiç kullanılmayan indeksler her yazmayı boş yere pahalılaştırır. Onları da ölçümle bulun:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">SELECT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> indexrelname, idx_scan</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">FROM</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> pg_stat_user_indexes</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">WHERE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> idx_scan </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">=</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<p><code>idx_scan = 0</code> olan bir indeks hiç okuma hızlandırmıyor ama her yazmaya maliyet ekliyor demektir. (Primary key ve unique kısıtların indekslerini elbette koruyun.) Bu, <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerin</a> yazma kırılma noktasında sayılan ilk temizliktir.</p>\n<h2 id=\"doğru-indeksi-nasıl-seçersiniz\">Doğru indeksi nasıl seçersiniz?</h2>\n<p>İndeks “var/yok”tan ibaret değildir; doğru türü ve şekli seçmek önemlidir:</p>\n<ul>\n<li><strong>Bileşik indekste kolon sırası.</strong> <code>(a, b)</code> indeksi, <code>a</code>’da eşitlik + <code>b</code>’de aralık sorgusuna hizmet eder; <code>b</code>’de tek başına yapılan bir arama da bu indeksi kullanabilir ama taranan indeks bölümünü daraltmaz — PostgreSQL 18 öncesinde indeksin tamamı taranır, 18’den itibaren skip scan optimizasyonu bunu daraltır. Eşitlikle filtrelenen kolonu başa, aralıkla filtreleneni sona koyun.</li>\n<li><strong>Kısmi indeks (partial).</strong> Sorgu hep aynı alt kümeye bakıyorsa indeksi de o alt kümeyle sınırlayın: <code>CREATE INDEX ... WHERE status = &#39;active&#39;</code>. Daha küçük indeks, daha ucuz bakım.</li>\n<li><strong>Covering indeks.</strong> <code>INCLUDE</code> ile sık okunan kolonları indekse ekleyip veritabanının tabloya hiç gitmemesini sağlayabilirsiniz.</li>\n<li><strong>B-tree dışı türler.</strong> Eşitlik/sıralama için B-tree; <code>jsonb</code> ve tam metin için GIN; saf sıralı, append ağırlıklı veri için küçük ve ucuz BRIN.</li>\n</ul>\n<h2 id=\"duplikat-ve-örtüşen-indeksler\">Duplikat ve örtüşen indeksler</h2>\n<p><code>(a)</code> üzerindeki bir indeks, zaten <code>(a, b)</code> indeksiniz varsa gereksizdir — bileşik indeks <code>a</code> ile başlayan sorgulara da hizmet eder. Bu tür örtüşmeleri periyodik olarak temizleyin; her biri sessiz bir yazma vergisidir.</p>\n<h2 id=\"denge-sorgu-planıyla-kurulur\">Denge sorgu planıyla kurulur</h2>\n<p>Tek kural: indeksi ne tahminle ekleyin ne tahminle silin. Bir indeks, gerçek bir sorgu planı onu istediği için eklenir; bir indeks, istatistikler onu kimsenin kullanmadığını gösterdiği için silinir. Eksik ile fazla arasındaki denge, hislerle değil, <code>EXPLAIN ANALYZE</code> ve <code>pg_stat_user_indexes</code> ile kurulur.</p>\n<hr/>\n<p>İndeks yönetimi, “daha çok indeks daha iyi” sanısından kurtulmakla başlar. Her indeks bir okuma kazancı ve bir yazma maliyetidir; iyi yönetim, bu ikisini ölçerek dengede tutmaktır.</p>\n<p>Eksik indeks sorguyu yavaşlatır; fazlası ise tüm tabloyu. İkisinden de ölçüm korur.</p>",
      "content_text": "İki karşıt ekip gördüm. Biri tabloya hiç indeks koymamıştı — her sorgu bir sequential scan, her liste sayfası saniyeler sürüyordu. Diğeri tam tersini yapmıştı: \"ne olur ne olmaz\" diye her kolona bir indeks açmıştı ve şimdi her `INSERT` ağır aksak ilerliyordu.\n\nİkisi de aynı yanılgının iki ucu: indeksi ücretsiz bir hız kaynağı sanmak.\n\n## İndeks ücretsiz hız değildir\n\nBir indeks okumayı hızlandırır, çünkü veritabanı tüm tabloyu taramak yerine sıralı bir yapıya bakar. Ama o sıralı yapı kendi kendine güncel kalmaz: tablodaki bir `INSERT`, o tablonun **her indeksine** bir girdi yazar; bir `UPDATE` de indeksli bir kolona dokunduğu anda aynısını yapar. İstisnalar var — indeksli hiçbir kolonu değiştirmeyen HOT güncellemesi indekslere dokunmaz, kısmi indeks yüklemi dışında kalan satırlar için güncellenmez, `DELETE` ise indeks girdilerini temizlemeyi `VACUUM`'a bırakır — ama bunlar istisna, varsayılan değil.\n\nYani her indeks bir takastır: okuma hızını, yazma maliyetiyle satın alırsınız. Beş indeksli bir tabloya tek satır eklemek, altı yapıyı birden güncellemektir. İndeks \"bedava okuma hızı\" değil, \"yazmayla ödenmiş okuma hızı\"dır.\n\n## Eksik indeks: tahmin değil, ölçüm\n\nEksik indeksi sorgu planından bulun, tahminden değil:\n\n```sql\nEXPLAIN ANALYZE\nSELECT * FROM orders WHERE status = 'pending';\n```\n\nÇıktıda büyük bir tabloda `Seq Scan` görüyorsanız ve sorgu satırların küçük bir kısmını döndürüyorsa, muhtemelen bir indeks eksiktir. `pg_stat_user_tables` tablosunda `seq_scan` sayısı yüksek olan büyük tabloları tarayın — bunlar adaylarınızdır.\n\nİndeksi gerçek, yavaş bir sorgu için ekleyin. \"İleride lazım olabilir\" diye eklenen indeks, [spekülatif genelliğin](/tr/journal/ileride-lazim-olur-kodunun-faturasi) veritabanı katmanındaki hâlidir.\n\n## Fazla indeks: kullanılmayanı bul\n\nTers yönde, hiç kullanılmayan indeksler her yazmayı boş yere pahalılaştırır. Onları da ölçümle bulun:\n\n```sql\nSELECT indexrelname, idx_scan\nFROM pg_stat_user_indexes\nWHERE idx_scan = 0;\n```\n\n`idx_scan = 0` olan bir indeks hiç okuma hızlandırmıyor ama her yazmaya maliyet ekliyor demektir. (Primary key ve unique kısıtların indekslerini elbette koruyun.) Bu, [veri yoğunluklu sistemlerin](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) yazma kırılma noktasında sayılan ilk temizliktir.\n\n## Doğru indeksi nasıl seçersiniz?\n\nİndeks \"var/yok\"tan ibaret değildir; doğru türü ve şekli seçmek önemlidir:\n\n- **Bileşik indekste kolon sırası.** `(a, b)` indeksi, `a`'da eşitlik + `b`'de aralık sorgusuna hizmet eder; `b`'de tek başına yapılan bir arama da bu indeksi kullanabilir ama taranan indeks bölümünü daraltmaz — PostgreSQL 18 öncesinde indeksin tamamı taranır, 18'den itibaren skip scan optimizasyonu bunu daraltır. Eşitlikle filtrelenen kolonu başa, aralıkla filtreleneni sona koyun.\n- **Kısmi indeks (partial).** Sorgu hep aynı alt kümeye bakıyorsa indeksi de o alt kümeyle sınırlayın: `CREATE INDEX ... WHERE status = 'active'`. Daha küçük indeks, daha ucuz bakım.\n- **Covering indeks.** `INCLUDE` ile sık okunan kolonları indekse ekleyip veritabanının tabloya hiç gitmemesini sağlayabilirsiniz.\n- **B-tree dışı türler.** Eşitlik/sıralama için B-tree; `jsonb` ve tam metin için GIN; saf sıralı, append ağırlıklı veri için küçük ve ucuz BRIN.\n\n## Duplikat ve örtüşen indeksler\n\n`(a)` üzerindeki bir indeks, zaten `(a, b)` indeksiniz varsa gereksizdir — bileşik indeks `a` ile başlayan sorgulara da hizmet eder. Bu tür örtüşmeleri periyodik olarak temizleyin; her biri sessiz bir yazma vergisidir.\n\n## Denge sorgu planıyla kurulur\n\nTek kural: indeksi ne tahminle ekleyin ne tahminle silin. Bir indeks, gerçek bir sorgu planı onu istediği için eklenir; bir indeks, istatistikler onu kimsenin kullanmadığını gösterdiği için silinir. Eksik ile fazla arasındaki denge, hislerle değil, `EXPLAIN ANALYZE` ve `pg_stat_user_indexes` ile kurulur.\n\n---\n\nİndeks yönetimi, \"daha çok indeks daha iyi\" sanısından kurtulmakla başlar. Her indeks bir okuma kazancı ve bir yazma maliyetidir; iyi yönetim, bu ikisini ölçerek dengede tutmaktır.\n\nEksik indeks sorguyu yavaşlatır; fazlası ise tüm tabloyu. İkisinden de ölçüm korur.",
      "date_published": "2026-09-05T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "performance",
        "indexing",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/zero-downtime-veritabani-gocleri/",
      "url": "https://sade.dev/tr/notes/zero-downtime-veritabani-gocleri/",
      "title": "Canlıda Sıfır Kesintiyle Şema Değiştirmek",
      "summary": "Tehlikeli olan şema değişikliği değil, onu tek adımda yapmak. Okumaları da kuyruğa sokan yalnızca ACCESS EXCLUSIVE lock alan işlemlerdir ve 50 milyon satırlık bir tabloda o kuyruğun adı kesintidir. Değişikliği expand-contract adımlarına bölün: kısıtı NOT VALID ile ekleyip VALIDATE'i ayrı adımda çalıştırın, indeksleri CONCURRENTLY kurun ve migration oturumuna kısa bir lock_timeout verin.",
      "content_html": "<p>Bir deploy, 50 milyon satırlık bir tabloda tek bir <code>ALTER TABLE</code> çalıştırdı ve tabloyu dört dakika kilitledi. Dört dakika boyunca o tabloya değen her istek bekledi; site fiilen çöktü. Migration’ın kendisi doğruydu — sorun, onun tek adımda yapılmasıydı.</p>\n<p>Tehlikeli olan şema değişikliği değildir. Tehlikeli olan, şema değişikliğini tek seferde, geri uyumluluğu düşünmeden yapmaktır.</p>\n<h2 id=\"locku-kim-alır\">Lock’u kim alır?</h2>\n<p>Her şema değişikliği aynı maliyette değildir. Modern PostgreSQL’de sabit <code>DEFAULT</code> ile kolon eklemek bir metadata işlemidir — hızlıdır. Asıl tehlike, tabloyu uzun süre kilitleyen işlemlerdedir:</p>\n<ul>\n<li><code>CREATE INDEX</code> — <code>CONCURRENTLY</code> olmadan tabloyu yazmaya kapatır.</li>\n<li>Tabloyu yeniden yazan <code>ALTER COLUMN ... TYPE</code> değişiklikleri.</li>\n<li>Tüm tabloyu tarayan <code>NOT NULL</code>, <code>CHECK</code> veya foreign key eklemeleri.</li>\n</ul>\n<p>Bu işlemler güçlü bir lock alır; <code>ACCESS EXCLUSIVE</code> alanlar — tablo yeniden yazımı ya da <code>NOT NULL</code>/<code>CHECK</code> eklemesi — tabloya değen her sorguyu, okuma dahil, kuyruğa sokar. Geri kalanı yalnızca yazmaları bloklar. Tablo büyükse kuyruk uzar.</p>\n<h2 id=\"tehlikeli-olan-tek-adımdır\">Tehlikeli olan tek adımdır</h2>\n<p>Çözüm migration yapmamak değil; her tehlikeli migration’ı, her biri tek başına güvenli ve geri uyumlu olan küçük adımlara bölmektir. Bunun adı <strong>expand-contract</strong> (genişlet–daralt) kalıbıdır.</p>\n<p>Bir kolonu yeniden adlandırmak istediğinizi düşünün. Tek adımda <code>RENAME COLUMN</code>, eski kolonu okuyan çalışan kodu anında kırar. Bunun yerine üç deploy:</p>\n<ol>\n<li><strong>Genişlet.</strong> Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, hâlâ eskiden okusun.</li>\n<li><strong>Geçiş.</strong> Eski veriyi yeni kolona batch’ler hâlinde taşı. Kod artık yeniden okusun.</li>\n<li><strong>Daralt.</strong> Eski kolonu sil.</li>\n</ol>\n<p>Her adım, hem bir önceki sürümdeki kod hem de yeni kod ile çalışır. Hiçbir an, çalışan kod ile şema uyumsuz değildir.</p>\n<h2 id=\"güvenli-reçeteler\">Güvenli reçeteler</h2>\n<p>Sık yapılan değişikliklerin kesintisiz hâlleri:</p>\n<p><strong>Yeni <code>NOT NULL</code> kolon.</strong> Tek adımda <code>NOT NULL</code> eklemek tabloyu tarar. Bölün:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- 1. Önce nullable ekle</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ALTER</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> TABLE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ADD</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> COLUMN </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">status</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> text</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- 2. Mevcut satırları batch&#39;ler hâlinde doldur (uygulama tarafında)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- 3. Kısıtı önce doğrulamadan ekle, sonra ayrı adımda doğrula</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ALTER</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> TABLE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ADD</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> CONSTRAINT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders_status_not_null</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    CHECK</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> (</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">status</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> IS NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">) </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NOT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> VALID;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ALTER</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> TABLE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders VALIDATE </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">CONSTRAINT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders_status_not_null;</span></span></code></pre>\n<p><code>NOT VALID</code> ile eklenen kısıt yeni satırlar için hemen geçerlidir ama mevcut satırları taramaz; <code>VALIDATE</code> ise tabloyu yalnızca <code>SHARE UPDATE EXCLUSIVE</code> lock ile tarar — yazmaları engellemez.</p>\n<p><strong>İndeks.</strong> Her zaman <code>CONCURRENTLY</code>:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">CREATE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> INDEX</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> CONCURRENTLY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> idx_orders_status </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ON</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> orders (</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">status</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">);</span></span></code></pre>\n<p><strong>Foreign key.</strong> Aynı iki adımlı kalıp: önce <code>ADD CONSTRAINT ... NOT VALID</code>, sonra <code>VALIDATE CONSTRAINT</code>.</p>\n<p><strong><code>lock_timeout</code>.</strong> Bir migration, uzun süren bir sorgunun arkasında lock için beklerken kendisi de arkasındaki her şeyi bloklar. Bunu önlemek için migration oturumuna kısa bir <code>lock_timeout</code> verin — lock hemen alınamıyorsa migration beklemek yerine başarısız olsun, siz tekrar deneyin.</p>\n<h2 id=\"kod-ve-şema-birlikte-uyumlu-olmalı\">Kod ve şema birlikte uyumlu olmalı</h2>\n<p>Expand-contract’ın özü tek bir kuralda: her ara adımda, hem <strong>çalışmakta olan eski kod</strong> hem de <strong>yeni kod</strong> mevcut şema ile çalışabilmeli.</p>\n<p>Veritabanı şeması, <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">ileride lazım olur</a> yazısında “tek yönlü kapı” dediğim şeydir — geri almak pahalıdır. Bu yüzden değişikliği büyük ve geri dönülmez tek bir adım olarak değil, her biri ayrı ayrı geri alınabilir küçük adımlar olarak tasarlayın.</p>\n<hr/>\n<p>Sıfır kesintili migration bir araç değil, bir disiplindir: her şema değişikliğini, çalışan kodun hiç fark etmeyeceği kadar küçük adımlara bölmek.</p>\n<p>Tehlikeli olan değişikliğin kendisi değil, onu tek nefeste yapmaktır.</p>",
      "content_text": "Bir deploy, 50 milyon satırlık bir tabloda tek bir `ALTER TABLE` çalıştırdı ve tabloyu dört dakika kilitledi. Dört dakika boyunca o tabloya değen her istek bekledi; site fiilen çöktü. Migration'ın kendisi doğruydu — sorun, onun tek adımda yapılmasıydı.\n\nTehlikeli olan şema değişikliği değildir. Tehlikeli olan, şema değişikliğini tek seferde, geri uyumluluğu düşünmeden yapmaktır.\n\n## Lock'u kim alır?\n\nHer şema değişikliği aynı maliyette değildir. Modern PostgreSQL'de sabit `DEFAULT` ile kolon eklemek bir metadata işlemidir — hızlıdır. Asıl tehlike, tabloyu uzun süre kilitleyen işlemlerdedir:\n\n- `CREATE INDEX` — `CONCURRENTLY` olmadan tabloyu yazmaya kapatır.\n- Tabloyu yeniden yazan `ALTER COLUMN ... TYPE` değişiklikleri.\n- Tüm tabloyu tarayan `NOT NULL`, `CHECK` veya foreign key eklemeleri.\n\nBu işlemler güçlü bir lock alır; `ACCESS EXCLUSIVE` alanlar — tablo yeniden yazımı ya da `NOT NULL`/`CHECK` eklemesi — tabloya değen her sorguyu, okuma dahil, kuyruğa sokar. Geri kalanı yalnızca yazmaları bloklar. Tablo büyükse kuyruk uzar.\n\n## Tehlikeli olan tek adımdır\n\nÇözüm migration yapmamak değil; her tehlikeli migration'ı, her biri tek başına güvenli ve geri uyumlu olan küçük adımlara bölmektir. Bunun adı **expand-contract** (genişlet–daralt) kalıbıdır.\n\nBir kolonu yeniden adlandırmak istediğinizi düşünün. Tek adımda `RENAME COLUMN`, eski kolonu okuyan çalışan kodu anında kırar. Bunun yerine üç deploy:\n\n1. **Genişlet.** Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, hâlâ eskiden okusun.\n2. **Geçiş.** Eski veriyi yeni kolona batch'ler hâlinde taşı. Kod artık yeniden okusun.\n3. **Daralt.** Eski kolonu sil.\n\nHer adım, hem bir önceki sürümdeki kod hem de yeni kod ile çalışır. Hiçbir an, çalışan kod ile şema uyumsuz değildir.\n\n## Güvenli reçeteler\n\nSık yapılan değişikliklerin kesintisiz hâlleri:\n\n**Yeni `NOT NULL` kolon.** Tek adımda `NOT NULL` eklemek tabloyu tarar. Bölün:\n\n```sql\n-- 1. Önce nullable ekle\nALTER TABLE orders ADD COLUMN status text;\n\n-- 2. Mevcut satırları batch'ler hâlinde doldur (uygulama tarafında)\n\n-- 3. Kısıtı önce doğrulamadan ekle, sonra ayrı adımda doğrula\nALTER TABLE orders ADD CONSTRAINT orders_status_not_null\n    CHECK (status IS NOT NULL) NOT VALID;\nALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;\n```\n\n`NOT VALID` ile eklenen kısıt yeni satırlar için hemen geçerlidir ama mevcut satırları taramaz; `VALIDATE` ise tabloyu yalnızca `SHARE UPDATE EXCLUSIVE` lock ile tarar — yazmaları engellemez.\n\n**İndeks.** Her zaman `CONCURRENTLY`:\n\n```sql\nCREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);\n```\n\n**Foreign key.** Aynı iki adımlı kalıp: önce `ADD CONSTRAINT ... NOT VALID`, sonra `VALIDATE CONSTRAINT`.\n\n**`lock_timeout`.** Bir migration, uzun süren bir sorgunun arkasında lock için beklerken kendisi de arkasındaki her şeyi bloklar. Bunu önlemek için migration oturumuna kısa bir `lock_timeout` verin — lock hemen alınamıyorsa migration beklemek yerine başarısız olsun, siz tekrar deneyin.\n\n## Kod ve şema birlikte uyumlu olmalı\n\nExpand-contract'ın özü tek bir kuralda: her ara adımda, hem **çalışmakta olan eski kod** hem de **yeni kod** mevcut şema ile çalışabilmeli.\n\nVeritabanı şeması, [ileride lazım olur](/tr/journal/ileride-lazim-olur-kodunun-faturasi) yazısında \"tek yönlü kapı\" dediğim şeydir — geri almak pahalıdır. Bu yüzden değişikliği büyük ve geri dönülmez tek bir adım olarak değil, her biri ayrı ayrı geri alınabilir küçük adımlar olarak tasarlayın.\n\n---\n\nSıfır kesintili migration bir araç değil, bir disiplindir: her şema değişikliğini, çalışan kodun hiç fark etmeyeceği kadar küçük adımlara bölmek.\n\nTehlikeli olan değişikliğin kendisi değil, onu tek nefeste yapmaktır.",
      "date_published": "2026-08-29T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "migrations",
        "production",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/read-write-splitting/",
      "url": "https://sade.dev/tr/notes/read-write-splitting/",
      "title": "Read/Write Splitting: Okuma ve Yazma Yükünü Ayırmak",
      "summary": "Read replica okumayı ölçekler, yazma kapasitesine hiçbir şey katmaz; bu yüzden takas bedava değildir, bedeli replication lag'dir. Laravel'in sticky seçeneği read-after-write'ı yalnızca tek bir istek içinde onarır; iki istek arasında kullanıcı az önce güncellediği profili hâlâ bayat görür. Önce indeks disiplini: replica çoğu zaman tek bir CREATE INDEX'in çözeceği şeyi pahalıya satın almaktır.",
      "content_html": "<p>Primary veritabanının CPU’su sürekli tavandaydı. İlginç olan şuydu: yazma oranı düşüktü. Yükün neredeyse tamamı okumaydı — rapor sayfaları, listeleme endpoint’leri, arama. Tek bir primary, kendisine hiç ihtiyaç duymayan bir sürü okumayı taşımaya çalışıyordu.</p>\n<p>Okuma ve yazma yükünü ayırmak — read/write splitting — bu tablonun bilinen çözümü. Ama doğru zamanda ve doğru tuzakların farkında uygulanmazsa, çözdüğünden çok problem getirir.</p>\n<h2 id=\"çoğu-yük-okuma-ağırlıklıdır\">Çoğu yük okuma ağırlıklıdır</h2>\n<p>Trafiğin ne kadarının okuma olduğu iş yüküne bağlıdır: standart OLTP benchmark’larının ölçümünde TPC-E %90,69 okumadır, TPC-C ise %65,71’de kalır — yani kendi oranınızı varsaymak yerine ölçün. Her sipariş bir kez yazılır, ama onlarca kez okunur: listede, detayda, raporda, panelde. Bu asimetri, read/write splitting’i cazip kılan şeydir — çünkü ölçeklemeniz gereken taraf bellidir.</p>\n<h2 id=\"önce-bu-gerçekten-bir-kapasite-sorunu-mu\">Önce: bu gerçekten bir kapasite sorunu mu?</h2>\n<p>Replica eklemeden önce durun. Primary’nin dolu olması her zaman “kapasite bitti” demek değildir. Çoğu zaman tek bir eksik indeks, primary’yi olduğundan kat kat meşgul gösterir.</p>\n<p>Bir replica eklemek — yeni bir sunucu, replication kurulumu, lag izleme — aslında bir <code>CREATE INDEX</code>’in çözeceği bir problemi pahalıya satın almak olabilir. <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">Veri yoğunluklu sistemlerin kırılma noktaları</a> yazısındaki sıra nettir: önce indeks disiplini, sonra replica. Bu sırayı atlamayın.</p>\n<p>Sorgularınızı <code>EXPLAIN ANALYZE</code> ile ölçün. Primary, doğru indekslenmiş sorgular altında gerçekten doyuyorsa — işte o zaman replica.</p>\n<h2 id=\"replica-okumayı-ölçekler-yazmayı-değil\">Replica okumayı ölçekler, yazmayı değil</h2>\n<p>Net olalım: bir read replica, yazma kapasitenize <strong>hiçbir şey</strong> katmaz. Aynı yazmalar her replica’da yeniden oynanır. Replica okuma yükü problemini çözer; yazma yükü probleminiz varsa replica yanlış araçtır.</p>\n<h2 id=\"laravelde-kurulum\">Laravel’de kurulum</h2>\n<p>Laravel, okuma/yazma bağlantı ayrımını doğal destekler:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">// config/database.php</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pgsql</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">driver</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pgsql</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">read</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">   =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">host</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">10.0.0.2</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">]],</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">   // replica</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">write</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">  =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">host</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">10.0.0.1</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">]],</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">   // primary</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">sticky</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> true</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">    // ...ortak ayarlar</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span></code></pre>\n<p><code>SELECT</code>’ler replica’ya, <code>INSERT/UPDATE/DELETE</code>’ler primary’ye gider. Replica’nın kendi bağlantı havuzu olur — primary’ninkinden ayrı; <a href=\"/tr/notes/pgbouncer-auth-query/\">pgBouncer</a> kullanıyorsanız ikisi ayrı havuzdur.</p>\n<h2 id=\"replication-lag-asıl-fatura\">Replication lag: asıl fatura</h2>\n<p>Replica, primary’nin birkaç milisaniye — yük altında birkaç saniye — gerisindedir. Bu gecikme, read/write splitting’in gerçek bedelidir ve adı <strong>read-after-write</strong> problemidir.</p>\n<p><code>sticky =&gt; true</code> bunu kısmen çözer: bir istek içinde yazma yaptıysanız, aynı istekteki sonraki okumalar primary’ye gider. Ama <code>sticky</code> yalnızca <strong>tek bir istek</strong> sınırında çalışır.</p>\n<p>Sınırın dışı hâlâ açıktır: kullanıcı profilini günceller (istek 1, primary’ye yazıldı), sonraki sayfaya geçer (istek 2, replica’dan okur) ve henüz replica’da oynanmamış eski profilini görür. Kullanıcı kendi yazdığı veriyi bayat görür. Bu bir bug gibi görünür ama aslında mimarinin kabul ettiği bir ödündür — bilerek kabul edilmesi gereken bir ödün.</p>\n<h2 id=\"sorgu-sınıflandırma-disiplini\">Sorgu sınıflandırma disiplini</h2>\n<p>Read/write splitting’i kurmak, her okumanın şu soruya cevap vermesini gerektirir: <strong>bu sorgu bayat veri okuyabilir mi?</strong></p>\n<ul>\n<li><strong>Replica’dan okunabilir:</strong> listeler, raporlar, arama sonuçları, paneller. Birkaç saniye gecikme önemsiz.</li>\n<li><strong>Primary’den okunmalı:</strong> hesap bakiyesi, stok adedi, yetki kontrolü, bir yazma kararının dayandığı her okuma.</li>\n</ul>\n<p>Bu sınıflandırma artık mimarinin bir parçasıdır ve dokümante edilmesi gerekir. Yeni gelen bir geliştiricinin “bu sorgu nereden okumalı” sorusuna bakacak bir yeri olmalı — yoksa sınıflandırma sessizce çürür.</p>\n<h2 id=\"ne-zaman-gerçekten-gerekir\">Ne zaman gerçekten gerekir?</h2>\n<p>Read/write splitting şu üç koşul birlikte sağlandığında doğru hamledir:</p>\n<ol>\n<li>İndeks disiplini tamamlanmış; sorgular doğru indeksli ve hâlâ primary doyuyor.</li>\n<li>Yük ölçülmüş biçimde okuma ağırlıklı.</li>\n<li>Bayat-okuma sınıflandırmasını yapıp dokümante edecek disiplin var.</li>\n</ol>\n<p>Bunlardan biri eksikse, replica’yı ertelemek daha ucuzdur.</p>\n<hr/>\n<p>Read/write splitting, dikey büyümeden ucuz bir ölçek hamlesidir — ama bedava değildir. Bedeli replication lag’dir ve onu görmezden gelmek, kullanıcıya kendi verisini eski göstermekle ödenir.</p>\n<p>Okumayı ayırmadan önce, hangi okumanın bayatlığa dayanabileceğini bilin.</p>",
      "content_text": "Primary veritabanının CPU'su sürekli tavandaydı. İlginç olan şuydu: yazma oranı düşüktü. Yükün neredeyse tamamı okumaydı — rapor sayfaları, listeleme endpoint'leri, arama. Tek bir primary, kendisine hiç ihtiyaç duymayan bir sürü okumayı taşımaya çalışıyordu.\n\nOkuma ve yazma yükünü ayırmak — read/write splitting — bu tablonun bilinen çözümü. Ama doğru zamanda ve doğru tuzakların farkında uygulanmazsa, çözdüğünden çok problem getirir.\n\n## Çoğu yük okuma ağırlıklıdır\n\nTrafiğin ne kadarının okuma olduğu iş yüküne bağlıdır: standart OLTP benchmark'larının ölçümünde TPC-E %90,69 okumadır, TPC-C ise %65,71'de kalır — yani kendi oranınızı varsaymak yerine ölçün. Her sipariş bir kez yazılır, ama onlarca kez okunur: listede, detayda, raporda, panelde. Bu asimetri, read/write splitting'i cazip kılan şeydir — çünkü ölçeklemeniz gereken taraf bellidir.\n\n## Önce: bu gerçekten bir kapasite sorunu mu?\n\nReplica eklemeden önce durun. Primary'nin dolu olması her zaman \"kapasite bitti\" demek değildir. Çoğu zaman tek bir eksik indeks, primary'yi olduğundan kat kat meşgul gösterir.\n\nBir replica eklemek — yeni bir sunucu, replication kurulumu, lag izleme — aslında bir `CREATE INDEX`'in çözeceği bir problemi pahalıya satın almak olabilir. [Veri yoğunluklu sistemlerin kırılma noktaları](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) yazısındaki sıra nettir: önce indeks disiplini, sonra replica. Bu sırayı atlamayın.\n\nSorgularınızı `EXPLAIN ANALYZE` ile ölçün. Primary, doğru indekslenmiş sorgular altında gerçekten doyuyorsa — işte o zaman replica.\n\n## Replica okumayı ölçekler, yazmayı değil\n\nNet olalım: bir read replica, yazma kapasitenize **hiçbir şey** katmaz. Aynı yazmalar her replica'da yeniden oynanır. Replica okuma yükü problemini çözer; yazma yükü probleminiz varsa replica yanlış araçtır.\n\n## Laravel'de kurulum\n\nLaravel, okuma/yazma bağlantı ayrımını doğal destekler:\n\n```php\n// config/database.php\n'pgsql' => [\n    'driver' => 'pgsql',\n    'read'   => ['host' => ['10.0.0.2']],   // replica\n    'write'  => ['host' => ['10.0.0.1']],   // primary\n    'sticky' => true,\n    // ...ortak ayarlar\n],\n```\n\n`SELECT`'ler replica'ya, `INSERT/UPDATE/DELETE`'ler primary'ye gider. Replica'nın kendi bağlantı havuzu olur — primary'ninkinden ayrı; [pgBouncer](/tr/notes/pgbouncer-auth-query) kullanıyorsanız ikisi ayrı havuzdur.\n\n## Replication lag: asıl fatura\n\nReplica, primary'nin birkaç milisaniye — yük altında birkaç saniye — gerisindedir. Bu gecikme, read/write splitting'in gerçek bedelidir ve adı **read-after-write** problemidir.\n\n`sticky => true` bunu kısmen çözer: bir istek içinde yazma yaptıysanız, aynı istekteki sonraki okumalar primary'ye gider. Ama `sticky` yalnızca **tek bir istek** sınırında çalışır.\n\nSınırın dışı hâlâ açıktır: kullanıcı profilini günceller (istek 1, primary'ye yazıldı), sonraki sayfaya geçer (istek 2, replica'dan okur) ve henüz replica'da oynanmamış eski profilini görür. Kullanıcı kendi yazdığı veriyi bayat görür. Bu bir bug gibi görünür ama aslında mimarinin kabul ettiği bir ödündür — bilerek kabul edilmesi gereken bir ödün.\n\n## Sorgu sınıflandırma disiplini\n\nRead/write splitting'i kurmak, her okumanın şu soruya cevap vermesini gerektirir: **bu sorgu bayat veri okuyabilir mi?**\n\n- **Replica'dan okunabilir:** listeler, raporlar, arama sonuçları, paneller. Birkaç saniye gecikme önemsiz.\n- **Primary'den okunmalı:** hesap bakiyesi, stok adedi, yetki kontrolü, bir yazma kararının dayandığı her okuma.\n\nBu sınıflandırma artık mimarinin bir parçasıdır ve dokümante edilmesi gerekir. Yeni gelen bir geliştiricinin \"bu sorgu nereden okumalı\" sorusuna bakacak bir yeri olmalı — yoksa sınıflandırma sessizce çürür.\n\n## Ne zaman gerçekten gerekir?\n\nRead/write splitting şu üç koşul birlikte sağlandığında doğru hamledir:\n\n1. İndeks disiplini tamamlanmış; sorgular doğru indeksli ve hâlâ primary doyuyor.\n2. Yük ölçülmüş biçimde okuma ağırlıklı.\n3. Bayat-okuma sınıflandırmasını yapıp dokümante edecek disiplin var.\n\nBunlardan biri eksikse, replica'yı ertelemek daha ucuzdur.\n\n---\n\nRead/write splitting, dikey büyümeden ucuz bir ölçek hamlesidir — ama bedava değildir. Bedeli replication lag'dir ve onu görmezden gelmek, kullanıcıya kendi verisini eski göstermekle ödenir.\n\nOkumayı ayırmadan önce, hangi okumanın bayatlığa dayanabileceğini bilin.",
      "date_published": "2026-08-22T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "scaling",
        "performance",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/dogru-cache-stratejisi/",
      "url": "https://sade.dev/tr/notes/dogru-cache-stratejisi/",
      "title": "Redis Koymadan Önce: Doğru Cache Stratejisi",
      "summary": "Cache hiçbir zaman daha hızlı bir veritabanı değildir; kabul edilmiş bayatlık karşılığında satın alınmış hızdır. Bu yüzden ilk soru teknik değil: bu veri ne kadar bayat olabilir? TTL sıkıcı varsayılandır, çünkü her yazma yolunu bilmeyi gerektirmez ve biri unutulduğunda sessizce bozulmaz. Açık invalidation ancak her yolu eksiksiz yakalarsanız kesindir. Ve cache okuma yükünü azaltır, yazma yükünü asla.",
      "content_html": "<p>Bir ekip “site yavaş” diye Redis cache ekledi. Bir hafta sonra destek kuyruğu doldu: “Fiyatı güncelledim ama eski fiyat görünüyor.” Cache yavaşlığı çözmüştü — ve sessizce yeni bir problem yaratmıştı.</p>\n<p>Cache eklemek bedava bir performans kazancı değildir. Tazeliği hıza takas etmektir; bu takası bilerek yapmadıysanız, fatura bir bug olarak geri gelir.</p>\n<h2 id=\"cache-bir-tutarlılık-ödünüdür\">Cache bir tutarlılık ödünüdür</h2>\n<p>Bir değeri cache’lediğiniz an şunu kabul etmiş olursunuz: birileri, bir süreliğine, eski veriyi görebilir. Cache hiçbir zaman “daha hızlı veritabanı” değildir; “kabul edilmiş bayatlık karşılığında hız”dır.</p>\n<p>Bu yüzden ilk soru teknik değil: <strong>bu veri ne kadar bayat olabilir?</strong> Cevabı “hiç” olan bir veri — bir hesap bakiyesi, bir stok adedi — cache’lenmeden önce iki kez düşünülmelidir.</p>\n<h2 id=\"önce-sorulacak-sorular\">Önce sorulacak sorular</h2>\n<p>Redis’i açmadan önce:</p>\n<ul>\n<li><strong>Bu gerçekten bir okuma darboğazı mı?</strong> “Site yavaş” bir hipotezdir, ölçüm değil. Yavaşlığın kaynağını görmeden cache eklemek, bilinmeyen bir şeyin üstünü örtmektir.</li>\n<li><strong>Yavaşlık aslında eksik bir indeks mi?</strong> Cache, kötü bir sorgunun üstünü örter ama onu çözmez. Bir <code>CREATE INDEX</code>’in çözeceği şeyi cache’le gizlemek, problemi taşımaktır — <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> yanlış kırılma noktası tam olarak budur.</li>\n<li><strong>Bu veri ne kadar bayat kalabilir?</strong> Cevap, hangi stratejiyi seçeceğinizi belirler.</li>\n</ul>\n<h2 id=\"invalidation-neden-zor\">Invalidation neden zor?</h2>\n<p>Yazılımın iki zor probleminden biri olarak anılır cache invalidation — ve abartı değil. Cache’i koymak kolaydır; onu doğru anda <strong>temizlemek</strong> zordur.</p>\n<p>Çünkü bir cache değeri, onu etkileyen her yazma yolunda temizlenmelidir. Ürün fiyatı yalnızca “ürünü düzenle” ekranından değil; toplu fiyat güncellemesinden, bir indirim işinden, bir admin script’inden de değişir. Bu yolların <strong>hepsini</strong> bulup cache’i temizlemek — işte zor olan bu. Bir tanesini kaçırırsanız, yazının başındaki destek talebini alırsınız.</p>\n<h2 id=\"i̇ki-strateji\">İki strateji</h2>\n<p>Pratikte iki yol var:</p>\n<ul>\n<li><strong>TTL tabanlı.</strong> Değere bir ömür biçersiniz — 60 saniye, 5 dakika. Süre dolunca cache kendini tazeler. Basit, sağlam, her yazma yolunu bilmeyi gerektirmez. Karşılığında: TTL süresi kadar bayatlık kabul edersiniz.</li>\n<li><strong>Açık invalidation.</strong> Veri değiştiğinde cache’i elle silersiniz. Kesin ve tazedir — ama yalnızca her yazma yolunu eksiksiz yakalarsanız.</li>\n</ul>\n<p>Varsayılanım TTL’dir. Sıkıcı, öngörülebilir ve unutulan bir yazma yolu yüzünden sessizce bozulmaz. Açık invalidation’a yalnızca bayatlık gerçekten kabul edilemezken ve yazma yollarının sayısı sınırlı ve bilinirken geçerim. Çoğu zaman ikisi birlikte kullanılır: kısa bir TTL bir güvenlik ağı olarak, açık invalidation hız için.</p>\n<h2 id=\"cache-yazmayı-azaltmaz\">Cache yazmayı azaltmaz</h2>\n<p>Sık yapılan bir hata: yazma yükü altındaki bir sistemi cache ekleyerek kurtarmaya çalışmak. Cache <strong>okuma</strong> yükünü azaltır; yazma yüküne hiçbir etkisi yoktur — aksine, invalidation’ın kendisi ek yazma işidir. Probleminiz yazma tarafındaysa, cache yanlış araçtır.</p>\n<h2 id=\"pratik-kalıplar\">Pratik kalıplar</h2>\n<ul>\n<li><strong>Cache-aside.</strong> Oku: önce cache’e bak, yoksa veritabanından al ve cache’e yaz. En yaygın ve en anlaşılır kalıp.</li>\n<li><strong>Stampede koruması.</strong> Popüler bir anahtarın TTL’i dolduğu an yüzlerce istek aynı anda veritabanına gider. Cache miss’te kısa bir lock ile yalnızca bir isteğin veritabanına gitmesini sağlayın.</li>\n<li><strong>Anahtar disiplini.</strong> Cache anahtarlarını namespace’leyin — <a href=\"/tr/notes/shared-redis-namespace-izolasyonu/\">shared Redis namespace izolasyonu</a> notundaki kalıp, cache anahtarları için de geçerlidir.</li>\n</ul>\n<hr/>\n<p>Cache, yavaş bir sistemin çözümü değil; ölçülmüş bir okuma darboğazının, bilinçli bir tutarlılık ödünüyle hafifletilmesidir. Invalidation planı olmadan eklenen cache, hızlandırdığından fazlasını borç olarak geri alır.</p>\n<p>Cache koymadan önce sorun: bu veri ne kadar eski olabilir — ve onu kim temizleyecek?</p>",
      "content_text": "Bir ekip \"site yavaş\" diye Redis cache ekledi. Bir hafta sonra destek kuyruğu doldu: \"Fiyatı güncelledim ama eski fiyat görünüyor.\" Cache yavaşlığı çözmüştü — ve sessizce yeni bir problem yaratmıştı.\n\nCache eklemek bedava bir performans kazancı değildir. Tazeliği hıza takas etmektir; bu takası bilerek yapmadıysanız, fatura bir bug olarak geri gelir.\n\n## Cache bir tutarlılık ödünüdür\n\nBir değeri cache'lediğiniz an şunu kabul etmiş olursunuz: birileri, bir süreliğine, eski veriyi görebilir. Cache hiçbir zaman \"daha hızlı veritabanı\" değildir; \"kabul edilmiş bayatlık karşılığında hız\"dır.\n\nBu yüzden ilk soru teknik değil: **bu veri ne kadar bayat olabilir?** Cevabı \"hiç\" olan bir veri — bir hesap bakiyesi, bir stok adedi — cache'lenmeden önce iki kez düşünülmelidir.\n\n## Önce sorulacak sorular\n\nRedis'i açmadan önce:\n\n- **Bu gerçekten bir okuma darboğazı mı?** \"Site yavaş\" bir hipotezdir, ölçüm değil. Yavaşlığın kaynağını görmeden cache eklemek, bilinmeyen bir şeyin üstünü örtmektir.\n- **Yavaşlık aslında eksik bir indeks mi?** Cache, kötü bir sorgunun üstünü örter ama onu çözmez. Bir `CREATE INDEX`'in çözeceği şeyi cache'le gizlemek, problemi taşımaktır — [veri yoğunluklu sistemlerdeki](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) yanlış kırılma noktası tam olarak budur.\n- **Bu veri ne kadar bayat kalabilir?** Cevap, hangi stratejiyi seçeceğinizi belirler.\n\n## Invalidation neden zor?\n\nYazılımın iki zor probleminden biri olarak anılır cache invalidation — ve abartı değil. Cache'i koymak kolaydır; onu doğru anda **temizlemek** zordur.\n\nÇünkü bir cache değeri, onu etkileyen her yazma yolunda temizlenmelidir. Ürün fiyatı yalnızca \"ürünü düzenle\" ekranından değil; toplu fiyat güncellemesinden, bir indirim işinden, bir admin script'inden de değişir. Bu yolların **hepsini** bulup cache'i temizlemek — işte zor olan bu. Bir tanesini kaçırırsanız, yazının başındaki destek talebini alırsınız.\n\n## İki strateji\n\nPratikte iki yol var:\n\n- **TTL tabanlı.** Değere bir ömür biçersiniz — 60 saniye, 5 dakika. Süre dolunca cache kendini tazeler. Basit, sağlam, her yazma yolunu bilmeyi gerektirmez. Karşılığında: TTL süresi kadar bayatlık kabul edersiniz.\n- **Açık invalidation.** Veri değiştiğinde cache'i elle silersiniz. Kesin ve tazedir — ama yalnızca her yazma yolunu eksiksiz yakalarsanız.\n\nVarsayılanım TTL'dir. Sıkıcı, öngörülebilir ve unutulan bir yazma yolu yüzünden sessizce bozulmaz. Açık invalidation'a yalnızca bayatlık gerçekten kabul edilemezken ve yazma yollarının sayısı sınırlı ve bilinirken geçerim. Çoğu zaman ikisi birlikte kullanılır: kısa bir TTL bir güvenlik ağı olarak, açık invalidation hız için.\n\n## Cache yazmayı azaltmaz\n\nSık yapılan bir hata: yazma yükü altındaki bir sistemi cache ekleyerek kurtarmaya çalışmak. Cache **okuma** yükünü azaltır; yazma yüküne hiçbir etkisi yoktur — aksine, invalidation'ın kendisi ek yazma işidir. Probleminiz yazma tarafındaysa, cache yanlış araçtır.\n\n## Pratik kalıplar\n\n- **Cache-aside.** Oku: önce cache'e bak, yoksa veritabanından al ve cache'e yaz. En yaygın ve en anlaşılır kalıp.\n- **Stampede koruması.** Popüler bir anahtarın TTL'i dolduğu an yüzlerce istek aynı anda veritabanına gider. Cache miss'te kısa bir lock ile yalnızca bir isteğin veritabanına gitmesini sağlayın.\n- **Anahtar disiplini.** Cache anahtarlarını namespace'leyin — [shared Redis namespace izolasyonu](/tr/notes/shared-redis-namespace-izolasyonu) notundaki kalıp, cache anahtarları için de geçerlidir.\n\n---\n\nCache, yavaş bir sistemin çözümü değil; ölçülmüş bir okuma darboğazının, bilinçli bir tutarlılık ödünüyle hafifletilmesidir. Invalidation planı olmadan eklenen cache, hızlandırdığından fazlasını borç olarak geri alır.\n\nCache koymadan önce sorun: bu veri ne kadar eski olabilir — ve onu kim temizleyecek?",
      "date_published": "2026-08-15T00:00:00.000Z",
      "tags": [
        "caching",
        "redis",
        "performance",
        "production",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/nosql-tuzagi/",
      "url": "https://sade.dev/tr/journal/nosql-tuzagi/",
      "title": "NoSQL Tuzağı: \"Şeması Yok\" Diye Başlamak",
      "summary": "Şemasız veri diye bir şey yoktur; yalnızca şeklin nerede tanımlandığı ve onu kimin zorladığı sorusu vardır. Bir doküman veritabanını şema istemediğiniz için seçmek şemayı silmez; onu her okumaya, her yazmaya ve ekibin kafasına, hiçbir şeyin zorlamadığı bir yere taşır. NoSQL yerini yatay yazma ölçeğiyle ya da gerçekten heterojen belgelerle hak eder, şemadan kaçmakla değil.",
      "content_html": "<p>“Şeması yok, hızlı ilerleriz” diyerek MongoDB seçen bir projeyi iki yıl sonra gördüm. Her okuma savunmacıydı: <code>data?.user?.address?.city ?? null</code>. Sebep basitti — hiçbir belge bir diğeriyle tam olarak aynı şekle sahip değildi. Şema kaybolmamıştı; sadece veritabanından çıkıp her okuma kodu satırına dağılmıştı.</p>\n<h2 id=\"şemasız-bir-yanılgı\">”Şemasız” bir yanılgı</h2>\n<p>Şemasız veri diye bir şey yoktur. Verinin her zaman bir şekli vardır: bir kullanıcının e-postası, bir siparişin kalemleri, bir faturanın tarihi. Soru, şekil olup olmadığı değil; o şeklin <strong>nerede tanımlandığı</strong>.</p>\n<p>İki seçenek var. Şema ya veritabanında durur ve veritabanı tarafından zorlanır; ya da uygulama kodunda durur ve herkesin ona uyduğu varsayılır. “Şemasız” demek, ikincisini seçmektir — şemayı silmek değil, onu zorlanmayan bir yere taşımak.</p>\n<h2 id=\"şema-nereye-taşınır\">Şema nereye taşınır?</h2>\n<p>Veritabanı şemayı zorlamayı bıraktığında, o iş ortadan kalkmaz; şu yerlere dağılır:</p>\n<ul>\n<li><strong>Her okumaya.</strong> Kod, gelen belgenin beklenen alanlara sahip olduğunu artık varsayamaz; her alanı savunmacı biçimde kontrol eder.</li>\n<li><strong>Her yazmaya.</strong> Doğru şekli yazdığınızın garantisi yoktur; yalnızca umut vardır.</li>\n<li><strong>Dağınık doğrulama koduna.</strong> Veritabanının tek satırda yaptığı <code>NOT NULL</code>, tip kontrolü, foreign key — hepsi elle yazılmış kontrollere dönüşür.</li>\n<li><strong>Ekibin kafasına.</strong> “O alan bazen string, bazen array” bilgisi, dokümante edilmemiş bir kabile bilgisi olur.</li>\n</ul>\n<p>Şema bürokrasi değildir; veritabanının sizin yerinize ücretsiz yaptığı doğrulamadır. Onu reddetmek faturayı silmek değil, başka birine kesmektir.</p>\n<h2 id=\"çarpılan-krizler\">Çarpılan krizler</h2>\n<p>“Şemasız” rahatlığıyla başlayan projeler er ya da geç şunlara çarpar:</p>\n<ul>\n<li><strong>Veri drift’i.</strong> Zamanla belgenin on farklı sürümü aynı koleksiyonda yaşar. 2023’te yazılan kayıt, 2026’da yazılanla aynı şekilde değildir ve hiçbir migration bunu zorlamamıştır.</li>\n<li><strong>Referans bütünlüğü yok.</strong> Var olmayan bir kullanıcıya işaret eden bir sipariş, veritabanı tarafından engellenmez. Yetim kayıtlar sessizce birikir.</li>\n<li><strong>Migration bir uygulama işine dönüşür.</strong> İlişkisel dünyada tek bir <code>ALTER TABLE</code> ile yapılan şey, burada milyonlarca belgeyi tek tek gezen, batch’lenmesi gereken bir script olur.</li>\n</ul>\n<p>Bunlar küçük projede görünmez; tam da projenin büyüdüğü, ekibin değiştiği, kimsenin “o alan neden bazen yok” sorusunu cevaplayamadığı anda patlar.</p>\n<h2 id=\"nosqlin-gerçekten-doğru-olduğu-yerler\">NoSQL’in gerçekten doğru olduğu yerler</h2>\n<p>Bu, NoSQL’e karşı bir yazı değil. NoSQL’in gerçek ve haklı kullanımları var:</p>\n<ul>\n<li>Tek bir primary’nin yazma kapasitesini aşan, gerçekten yatay ölçek gerektiren yükler — <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> en zor kırılma noktası.</li>\n<li>Doğası gereği gerçekten heterojen belgeler.</li>\n<li>Belirli erişim desenleri — saf key-value, wide-column, graph.</li>\n</ul>\n<p>Tuzak NoSQL değil; NoSQL’i bu gerçek güçlerinden biri için değil, sadece “şema istemiyorum” diye seçmektir. Doğru araç, yanlış gerekçe.</p>\n<h2 id=\"esneklik-istiyorsanız-postgresql-zaten-veriyor\">Esneklik istiyorsanız PostgreSQL zaten veriyor</h2>\n<p>“Bazı alanlar esnek olsun” ihtiyacı gerçekse, bunun için tüm bütünlüğü feda etmeniz gerekmez. PostgreSQL’in <code>jsonb</code> sütunu, ilişkisel tablonun bütünlüğü içinde esnek bir belge alanı verir — <a href=\"/tr/journal/postgresql-her-seye-yeter-mi/\">PostgreSQL her şeye yeter mi</a> yazısında değindiğim gibi. Sabit olan alanlar şema olarak kalır, gerçekten değişken kısım <code>jsonb</code>’de durur. İkisinin en iyisi, hem de tek sistemde.</p>\n<hr/>\n<p>Şema bir yük değil, bir kalkandır. Onu veritabanından kaldırdığınızda kaybolmaz — yalnızca sizi koruduğu yerden çıkıp, sizi koruyamayacağı yere taşınır.</p>\n<p>“Şemasız” diye bir şey yoktur; yalnızca “şemayı kim zorluyor” sorusu vardır.</p>",
      "content_text": "\"Şeması yok, hızlı ilerleriz\" diyerek MongoDB seçen bir projeyi iki yıl sonra gördüm. Her okuma savunmacıydı: `data?.user?.address?.city ?? null`. Sebep basitti — hiçbir belge bir diğeriyle tam olarak aynı şekle sahip değildi. Şema kaybolmamıştı; sadece veritabanından çıkıp her okuma kodu satırına dağılmıştı.\n\n## \"Şemasız\" bir yanılgı\n\nŞemasız veri diye bir şey yoktur. Verinin her zaman bir şekli vardır: bir kullanıcının e-postası, bir siparişin kalemleri, bir faturanın tarihi. Soru, şekil olup olmadığı değil; o şeklin **nerede tanımlandığı**.\n\nİki seçenek var. Şema ya veritabanında durur ve veritabanı tarafından zorlanır; ya da uygulama kodunda durur ve herkesin ona uyduğu varsayılır. \"Şemasız\" demek, ikincisini seçmektir — şemayı silmek değil, onu zorlanmayan bir yere taşımak.\n\n## Şema nereye taşınır?\n\nVeritabanı şemayı zorlamayı bıraktığında, o iş ortadan kalkmaz; şu yerlere dağılır:\n\n- **Her okumaya.** Kod, gelen belgenin beklenen alanlara sahip olduğunu artık varsayamaz; her alanı savunmacı biçimde kontrol eder.\n- **Her yazmaya.** Doğru şekli yazdığınızın garantisi yoktur; yalnızca umut vardır.\n- **Dağınık doğrulama koduna.** Veritabanının tek satırda yaptığı `NOT NULL`, tip kontrolü, foreign key — hepsi elle yazılmış kontrollere dönüşür.\n- **Ekibin kafasına.** \"O alan bazen string, bazen array\" bilgisi, dokümante edilmemiş bir kabile bilgisi olur.\n\nŞema bürokrasi değildir; veritabanının sizin yerinize ücretsiz yaptığı doğrulamadır. Onu reddetmek faturayı silmek değil, başka birine kesmektir.\n\n## Çarpılan krizler\n\n\"Şemasız\" rahatlığıyla başlayan projeler er ya da geç şunlara çarpar:\n\n- **Veri drift'i.** Zamanla belgenin on farklı sürümü aynı koleksiyonda yaşar. 2023'te yazılan kayıt, 2026'da yazılanla aynı şekilde değildir ve hiçbir migration bunu zorlamamıştır.\n- **Referans bütünlüğü yok.** Var olmayan bir kullanıcıya işaret eden bir sipariş, veritabanı tarafından engellenmez. Yetim kayıtlar sessizce birikir.\n- **Migration bir uygulama işine dönüşür.** İlişkisel dünyada tek bir `ALTER TABLE` ile yapılan şey, burada milyonlarca belgeyi tek tek gezen, batch'lenmesi gereken bir script olur.\n\nBunlar küçük projede görünmez; tam da projenin büyüdüğü, ekibin değiştiği, kimsenin \"o alan neden bazen yok\" sorusunu cevaplayamadığı anda patlar.\n\n## NoSQL'in gerçekten doğru olduğu yerler\n\nBu, NoSQL'e karşı bir yazı değil. NoSQL'in gerçek ve haklı kullanımları var:\n\n- Tek bir primary'nin yazma kapasitesini aşan, gerçekten yatay ölçek gerektiren yükler — [veri yoğunluklu sistemlerdeki](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) en zor kırılma noktası.\n- Doğası gereği gerçekten heterojen belgeler.\n- Belirli erişim desenleri — saf key-value, wide-column, graph.\n\nTuzak NoSQL değil; NoSQL'i bu gerçek güçlerinden biri için değil, sadece \"şema istemiyorum\" diye seçmektir. Doğru araç, yanlış gerekçe.\n\n## Esneklik istiyorsanız PostgreSQL zaten veriyor\n\n\"Bazı alanlar esnek olsun\" ihtiyacı gerçekse, bunun için tüm bütünlüğü feda etmeniz gerekmez. PostgreSQL'in `jsonb` sütunu, ilişkisel tablonun bütünlüğü içinde esnek bir belge alanı verir — [PostgreSQL her şeye yeter mi](/tr/journal/postgresql-her-seye-yeter-mi) yazısında değindiğim gibi. Sabit olan alanlar şema olarak kalır, gerçekten değişken kısım `jsonb`'de durur. İkisinin en iyisi, hem de tek sistemde.\n\n---\n\nŞema bir yük değil, bir kalkandır. Onu veritabanından kaldırdığınızda kaybolmaz — yalnızca sizi koruduğu yerden çıkıp, sizi koruyamayacağı yere taşınır.\n\n\"Şemasız\" diye bir şey yoktur; yalnızca \"şemayı kim zorluyor\" sorusu vardır.",
      "date_published": "2026-08-08T00:00:00.000Z",
      "tags": [
        "nosql",
        "database",
        "architecture",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/postgresql-her-seye-yeter-mi/",
      "url": "https://sade.dev/tr/journal/postgresql-her-seye-yeter-mi/",
      "title": "PostgreSQL Her Şeye Yeter mi?",
      "summary": "PostgreSQL'in yanına Elasticsearch, MongoDB ve RabbitMQ dizen listenin büyük kısmını uzun süre tek bir PostgreSQL taşır: arama için GIN index'li tsvector, doküman için jsonb, orta hacimli kuyruk için SELECT FOR UPDATE SKIP LOCKED. Sınırlar dürüst ve gerçek, ama ikinci veri sistemi bir histen ya da bir blog yazısından değil, ölçülmüş bir sınırdan gelmeli.",
      "content_html": "<p>Bir mimari planında, daha tek kullanıcı yokken şu liste vardı: kalıcı veri için PostgreSQL, arama için Elasticsearch, cache için Redis, esnek belgeler için MongoDB, kuyruk için RabbitMQ. Beş ayrı veri sistemi — beşinin de yedeği, monitoring’i, sürüm yükseltmesi ve kendine özgü hata modu.</p>\n<p>O listenin büyük kısmını, uzun bir süre boyunca, tek başına PostgreSQL taşır. Soru “PostgreSQL bunu yapabilir mi” değil; “bu beş sistemin operasyon yükünü gerçekten satın aldınız mı”.</p>\n<h2 id=\"postgresqlin-az-bilinen-genişliği\">PostgreSQL’in az bilinen genişliği</h2>\n<p>PostgreSQL’i sadece bir tablo-satır deposu sanmak, yeteneklerinin küçük bir kısmını kullanmaktır. Sık sık ayrı bir sistem getirten ihtiyaçların çoğu zaten içinde:</p>\n<ul>\n<li><strong>Tam metin arama.</strong> <code>tsvector</code>, <code>tsquery</code> ve GIN index ile gövdeli bir arama. Çoğu uygulamanın “ürün ara”, “yazı ara” ihtiyacını dilsel sapma, ağırlıklandırma ve sıralama dahil karşılar.</li>\n<li><strong>Doküman saklama.</strong> <code>jsonb</code> sütunu, GIN index’iyle birlikte sorgulanabilir bir belge deposudur. Şema esnekliği isteyen alanları, ilişkisel tablonun içinde tutabilirsiniz.</li>\n<li><strong>Kuyruk.</strong> <code>SELECT ... FOR UPDATE SKIP LOCKED</code> ve <code>LISTEN/NOTIFY</code>, orta hacimli bir iş kuyruğunu ayrı bir broker olmadan çalıştırır. Laravel’in <code>database</code> queue driver’ı bunun <code>SKIP LOCKED</code> tarafını kullanır — worker’ları <code>LISTEN/NOTIFY</code> sinyali beklemek yerine kuyruk tablosunu belirli aralıklarla yoklar.</li>\n<li><strong>Analitik sorgular.</strong> Window function’lar, CTE’ler, materialized view’lar — ayrı bir analitik veritabanına gitmeden ciddi raporlama.</li>\n<li><strong>Coğrafi veri.</strong> PostGIS eklentisiyle konum sorguları.</li>\n</ul>\n<p>Bu özellikler “var ama kullanılmıyor” değil; production’da güvenilir biçimde çalışan, olgun yeteneklerdir.</p>\n<h2 id=\"tek-sistemin-sessiz-getirisi\">Tek sistemin sessiz getirisi</h2>\n<p>Her yeni veri sistemi yalnızca bir kutu değil; bir bakım yükümlülüğüdür. Tek sistemde kalmanın getirisi:</p>\n<ul>\n<li><strong>Tek yedek hattı.</strong> Bir tane backup/restore prosedürü, bir tane kurtarma tatbikatı.</li>\n<li><strong>Tek izleme hedefi.</strong> Öğrenilecek bir metrik seti, kurulacak bir alarm düzeni.</li>\n<li><strong>Çapraz tutarlılık.</strong> En önemlisi bu. Arama indeksiniz ile veriniz aynı veritabanındaysa aralarında <strong>drift olamaz</strong> — ikisi aynı transaction’da güncellenir. Ayrı bir Elasticsearch ise eninde sonunda veriyle senkronizasyonu bozulur ve onu düzeltmek ayrı bir iş kalemine dönüşür.</li>\n<li><strong>Tek zihinsel model.</strong> Ekip tek bir sistemin tuhaflıklarını öğrenir, beş sistemin değil.</li>\n</ul>\n<p>Bu, <a href=\"/tr/journal/neden-boring-architecture/\">boring architecture</a> yazısındaki innovation token mantığının veri katmanındaki hâlidir: her yeni sistem bir token ve token sınırlı.</p>\n<h2 id=\"sınır-nerede-başlar\">Sınır nerede başlar?</h2>\n<p>PostgreSQL her şeye yetmez — dürüst sınırları var ve onları görmezden gelmek de bir hata olur:</p>\n<ul>\n<li><strong>Arama.</strong> Yazım hatası toleransı, gelişmiş alaka düzeyi ayarı, faceted search ve çok dilli analiz ciddi ölçekte gerektiğinde, adanmış bir arama motoru gerçekten kazandırır.</li>\n<li><strong>Kuyruk.</strong> Çok yüksek throughput, karmaşık yönlendirme ya da çok tüketicili fan-out gerektiğinde gerçek bir message broker doğru araçtır. PostgreSQL kuyruğu orta hacimde rahat, uç hacimde değil.</li>\n<li><strong>Yazma throughput’u.</strong> Tek primary’nin fsync kapasitesini aşan sürekli yazma — bu, <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> en zor kırılma noktası ve gerçek bir sınır.</li>\n</ul>\n<p>Bu sınırlardan birine çarptığınızda ek sistem getirmek doğru karardır. Çarpmadan getirmek ise yalnızca bir tahmin.</p>\n<h2 id=\"karar-önce-postgresql-sonra-ölçüm\">Karar: önce PostgreSQL, sonra ölçüm</h2>\n<p>Kural sade: yeni bir projede, PostgreSQL’in yapabildiği her şeyi PostgreSQL’e yaptırın. Arama, kuyruk, cache benzeri ihtiyaçların ilk durağı o olsun. Ayrı bir sistem, ancak <strong>ölçülmüş</strong> bir sınır aşıldığında gelsin — bir histen, bir blog yazısından ya da CV kaygısından değil.</p>\n<p>Çoğu ekip PostgreSQL’in belki %20’sini kullanır, sonra “yetmiyor” der. Yetmeyen çoğu zaman PostgreSQL değil, onu tanıma çabasıdır.</p>\n<hr/>\n<p>PostgreSQL her şeye yetmez — ama sandığınızdan çok daha fazlasına yeter. İkinci bir veri sistemi getirmeden önce, birincisinin gerçekten sonuna gelip gelmediğinizi sorun.</p>\n<p>Önce PostgreSQL; gerisi ölçümle gelsin.</p>",
      "content_text": "Bir mimari planında, daha tek kullanıcı yokken şu liste vardı: kalıcı veri için PostgreSQL, arama için Elasticsearch, cache için Redis, esnek belgeler için MongoDB, kuyruk için RabbitMQ. Beş ayrı veri sistemi — beşinin de yedeği, monitoring'i, sürüm yükseltmesi ve kendine özgü hata modu.\n\nO listenin büyük kısmını, uzun bir süre boyunca, tek başına PostgreSQL taşır. Soru \"PostgreSQL bunu yapabilir mi\" değil; \"bu beş sistemin operasyon yükünü gerçekten satın aldınız mı\".\n\n## PostgreSQL'in az bilinen genişliği\n\nPostgreSQL'i sadece bir tablo-satır deposu sanmak, yeteneklerinin küçük bir kısmını kullanmaktır. Sık sık ayrı bir sistem getirten ihtiyaçların çoğu zaten içinde:\n\n- **Tam metin arama.** `tsvector`, `tsquery` ve GIN index ile gövdeli bir arama. Çoğu uygulamanın \"ürün ara\", \"yazı ara\" ihtiyacını dilsel sapma, ağırlıklandırma ve sıralama dahil karşılar.\n- **Doküman saklama.** `jsonb` sütunu, GIN index'iyle birlikte sorgulanabilir bir belge deposudur. Şema esnekliği isteyen alanları, ilişkisel tablonun içinde tutabilirsiniz.\n- **Kuyruk.** `SELECT ... FOR UPDATE SKIP LOCKED` ve `LISTEN/NOTIFY`, orta hacimli bir iş kuyruğunu ayrı bir broker olmadan çalıştırır. Laravel'in `database` queue driver'ı bunun `SKIP LOCKED` tarafını kullanır — worker'ları `LISTEN/NOTIFY` sinyali beklemek yerine kuyruk tablosunu belirli aralıklarla yoklar.\n- **Analitik sorgular.** Window function'lar, CTE'ler, materialized view'lar — ayrı bir analitik veritabanına gitmeden ciddi raporlama.\n- **Coğrafi veri.** PostGIS eklentisiyle konum sorguları.\n\nBu özellikler \"var ama kullanılmıyor\" değil; production'da güvenilir biçimde çalışan, olgun yeteneklerdir.\n\n## Tek sistemin sessiz getirisi\n\nHer yeni veri sistemi yalnızca bir kutu değil; bir bakım yükümlülüğüdür. Tek sistemde kalmanın getirisi:\n\n- **Tek yedek hattı.** Bir tane backup/restore prosedürü, bir tane kurtarma tatbikatı.\n- **Tek izleme hedefi.** Öğrenilecek bir metrik seti, kurulacak bir alarm düzeni.\n- **Çapraz tutarlılık.** En önemlisi bu. Arama indeksiniz ile veriniz aynı veritabanındaysa aralarında **drift olamaz** — ikisi aynı transaction'da güncellenir. Ayrı bir Elasticsearch ise eninde sonunda veriyle senkronizasyonu bozulur ve onu düzeltmek ayrı bir iş kalemine dönüşür.\n- **Tek zihinsel model.** Ekip tek bir sistemin tuhaflıklarını öğrenir, beş sistemin değil.\n\nBu, [boring architecture](/tr/journal/neden-boring-architecture) yazısındaki innovation token mantığının veri katmanındaki hâlidir: her yeni sistem bir token ve token sınırlı.\n\n## Sınır nerede başlar?\n\nPostgreSQL her şeye yetmez — dürüst sınırları var ve onları görmezden gelmek de bir hata olur:\n\n- **Arama.** Yazım hatası toleransı, gelişmiş alaka düzeyi ayarı, faceted search ve çok dilli analiz ciddi ölçekte gerektiğinde, adanmış bir arama motoru gerçekten kazandırır.\n- **Kuyruk.** Çok yüksek throughput, karmaşık yönlendirme ya da çok tüketicili fan-out gerektiğinde gerçek bir message broker doğru araçtır. PostgreSQL kuyruğu orta hacimde rahat, uç hacimde değil.\n- **Yazma throughput'u.** Tek primary'nin fsync kapasitesini aşan sürekli yazma — bu, [veri yoğunluklu sistemlerdeki](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) en zor kırılma noktası ve gerçek bir sınır.\n\nBu sınırlardan birine çarptığınızda ek sistem getirmek doğru karardır. Çarpmadan getirmek ise yalnızca bir tahmin.\n\n## Karar: önce PostgreSQL, sonra ölçüm\n\nKural sade: yeni bir projede, PostgreSQL'in yapabildiği her şeyi PostgreSQL'e yaptırın. Arama, kuyruk, cache benzeri ihtiyaçların ilk durağı o olsun. Ayrı bir sistem, ancak **ölçülmüş** bir sınır aşıldığında gelsin — bir histen, bir blog yazısından ya da CV kaygısından değil.\n\nÇoğu ekip PostgreSQL'in belki %20'sini kullanır, sonra \"yetmiyor\" der. Yetmeyen çoğu zaman PostgreSQL değil, onu tanıma çabasıdır.\n\n---\n\nPostgreSQL her şeye yetmez — ama sandığınızdan çok daha fazlasına yeter. İkinci bir veri sistemi getirmeden önce, birincisinin gerçekten sonuna gelip gelmediğinizi sorun.\n\nÖnce PostgreSQL; gerisi ölçümle gelsin.",
      "date_published": "2026-08-01T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "architecture",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/gereginden-karmasik-sistem-sinyalleri/",
      "url": "https://sade.dev/tr/journal/gereginden-karmasik-sistem-sinyalleri/",
      "title": "Gereğinden Karmaşık Bir Sistemin Verdiği Sinyaller",
      "summary": "Karmaşıklık tek bir kararla gelmez; tek tek savunulabilir adımlarla birikir, bu yüzden sadeleştirme bir hisle değil adı konmuş sinyallerle başlar. Sekiz somut sinyal, ardından dört adımlık bir çıkarma: ölü kod, tek implementasyonlu interface, erken dolaylılık ve en son net sınırlar. Hedef en az kod değil, en az gereksiz kod — özsel karmaşıklık silinmez, yalnızca daha kötü bir yere taşınır.",
      "content_html": "<p>Ekibe yeni katılan bir mühendis ilk haftasının sonunda sordu: “Tek bir alan eklemek neden iki gün sürüyor?” Soru masumdu, teşhis netti. Kimse o sistemi bir gün oturup “karmaşıklaştıralım” diye kararlaştırmamıştı. Karmaşıklık öylece birikmişti.</p>\n<p>Sorun da burada: karmaşıklık tek bir kararla gelmez, fark edilmeden birikir. Onu geri almak için önce onu <strong>görebilmek</strong> gerekir.</p>\n<h2 id=\"karmaşıklık-nasıl-birikir\">Karmaşıklık nasıl birikir?</h2>\n<p>Hiçbir karmaşık sistem, karmaşık olmaya karar vermez. Her adım tek başına makul görünür: “şimdilik bir interface açalım”, “bu duruma bir flag koyalım”, “araya bir katman daha”. Her biri küçük, her biri savunulabilir.</p>\n<p>Toplamları savunulabilir değil. Karmaşıklık bir olay değil, bir birikim — ve birikim, adı konmadığı sürece görünmez. Bu yüzden sadeleştirme bir his ile başlayamaz; somut sinyallerle başlar.</p>\n<h2 id=\"somut-sinyaller\">Somut sinyaller</h2>\n<p>Bir sistemin gereğinden karmaşık olduğunu söyleyen, tartışmaya kapalı kırmızı çizgiler:</p>\n<ul>\n<li><strong>Tek bir özelliği anlamak için çok sayıda dosya açıyorsunuz.</strong> Bir isteğin nereye gittiğini görmek altı dosya ve dört dolaylılık gerektiriyorsa, problem sizde değil, yapıdadır.</li>\n<li><strong>Tek satırlık bir değişiklik bir düzine dosyaya dokunuyor.</strong> Bir alan eklemek için migration, model, DTO, mapper, interface, factory, test fixture… Değişimin maliyeti, değişimin büyüklüğüyle orantılı değil.</li>\n<li><strong>Tek implementasyonlu interface’ler.</strong> “İhtimale karşı” açılmış, asla ikinci implementasyonu gelmemiş soyutlamalar. Bu, <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> kod tabanında bıraktığı tortudur.</li>\n<li><strong>Onboarding haftalar sürüyor.</strong> Olgun ve sade bir sistemde yeni mühendis ilk günlerde üretkendir. Haftalar sürüyorsa, kod kendini anlatmıyordur.</li>\n<li><strong>Kimse sistemi tahtaya çizemiyor.</strong> “Şu kutu şuraya bağlı” diye anlatılamayan bir mimari, anlaşılmıyor demektir.</li>\n<li><strong>Test kurulumu testin kendisinden uzun.</strong> Bir davranışı test etmek için kurulması gereken on mock, o davranışın çok fazla şeye bağlı olduğunun itirafıdır.</li>\n<li><strong>“Oraya dokunma, kırılır.”</strong> Bir kod bölgesi dokunulmaz ilan edilmişse, o bölge zaten kırıktır — sadece henüz patlamamıştır.</li>\n<li><strong>Kimsenin kullanmadığı config, flag ve seçenekler.</strong> Açılıp unutulmuş her seçenek, kodu her okuyanın hesaba katması gereken bir daldır.</li>\n</ul>\n<p>Bir sistemde bu sinyallerden ikisi-üçü varsa, tartışma “karmaşık mı” değil, “nereden başlayacağız”dır.</p>\n<h2 id=\"sadeleştirmeye-nereden-başlanır\">Sadeleştirmeye nereden başlanır?</h2>\n<p>Sadeleştirme bir yeniden yazım değildir; çoğu zaman bir <strong>çıkarma</strong> işlemidir. Sırayla:</p>\n<ol>\n<li><strong>Ölü kodu sil.</strong> Kullanılmayan flag, çağrılmayan method, ulaşılmayan branch. En düşük riskli, en yüksek getirili adım — silinen kodun bug’ı olmaz.</li>\n<li><strong>Tek implementasyonlu interface’leri çökert.</strong> Soyutlama gerçek bir ikinci kullanım kazanana kadar somut sınıfı doğrudan kullanın. Dikiş gerçekten gerektiğinde — <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">ihtiyaç kanıtlandığında</a> — birkaç saatlik bir refactor’la geri gelir.</li>\n<li><strong>Erken dolaylılığı satır içine al.</strong> Tek yerden çağrılan, sadece argümanı bir başka fonksiyona geçiren katmanları kaldırın.</li>\n<li><strong>Sınırları netleştir.</strong> Geriye kalan karmaşıklığı <a href=\"/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum/\">modüler monolitteki</a> gibi net sınırların arkasına toplayın — böylece bir dahaki birikim erken görünür.</li>\n</ol>\n<p>Her adımdan sonra durun ve ölçün. Sadeleştirme de aşırıya kaçabilir; amaç “en az kod” değil, “en az gereksiz kod”.</p>\n<h2 id=\"karmaşıklık-her-zaman-kötü-değil\">Karmaşıklık her zaman kötü değil</h2>\n<p>Önemli bir ayrım: karmaşıklığın iki türü var. Fred Brooks’un kavramlarıyla — <strong>özsel (essential)</strong> ve <strong>tesadüfi (accidental)</strong>.</p>\n<p>Özsel karmaşıklık domain’in kendisinden gelir. Vergi hesabı karmaşıktır, çünkü vergi mevzuatı karmaşıktır; bunu silemezsiniz, sadece dürüstçe modelleyebilirsiniz. Tesadüfi karmaşıklık ise sizin eklediğinizdir: gereksiz katman, erken soyutlama, unutulmuş flag.</p>\n<p>Sadeleştirmenin hedefi tesadüfi olanı temizlemektir — özsel olanı değil. Bir sistemi “çok basit” yapıp domain’in gerçek karmaşıklığını görmezden gelmek de bir hatadır; o karmaşıklık silinmez, sadece yanlış yere — genelde çağıran kodun içine — taşınır.</p>\n<p>İyi mimari, domain ne kadar karmaşıksa o kadar karmaşıktır; daha fazlası değil.</p>\n<hr/>\n<p>Karmaşıklık fark edilmeden birikir; bu yüzden onu fark edilir kılan sinyalleri ezbere bilmek bir mühendislik becerisidir. Sinyali adlandıramıyorsanız, sadeleştirmeye de başlayamazsınız.</p>\n<p>Bir sistemi basit tutmak, onu basit kurmaktan daha çok emek ister — ve o emeğe değer.</p>",
      "content_text": "Ekibe yeni katılan bir mühendis ilk haftasının sonunda sordu: \"Tek bir alan eklemek neden iki gün sürüyor?\" Soru masumdu, teşhis netti. Kimse o sistemi bir gün oturup \"karmaşıklaştıralım\" diye kararlaştırmamıştı. Karmaşıklık öylece birikmişti.\n\nSorun da burada: karmaşıklık tek bir kararla gelmez, fark edilmeden birikir. Onu geri almak için önce onu **görebilmek** gerekir.\n\n## Karmaşıklık nasıl birikir?\n\nHiçbir karmaşık sistem, karmaşık olmaya karar vermez. Her adım tek başına makul görünür: \"şimdilik bir interface açalım\", \"bu duruma bir flag koyalım\", \"araya bir katman daha\". Her biri küçük, her biri savunulabilir.\n\nToplamları savunulabilir değil. Karmaşıklık bir olay değil, bir birikim — ve birikim, adı konmadığı sürece görünmez. Bu yüzden sadeleştirme bir his ile başlayamaz; somut sinyallerle başlar.\n\n## Somut sinyaller\n\nBir sistemin gereğinden karmaşık olduğunu söyleyen, tartışmaya kapalı kırmızı çizgiler:\n\n- **Tek bir özelliği anlamak için çok sayıda dosya açıyorsunuz.** Bir isteğin nereye gittiğini görmek altı dosya ve dört dolaylılık gerektiriyorsa, problem sizde değil, yapıdadır.\n- **Tek satırlık bir değişiklik bir düzine dosyaya dokunuyor.** Bir alan eklemek için migration, model, DTO, mapper, interface, factory, test fixture... Değişimin maliyeti, değişimin büyüklüğüyle orantılı değil.\n- **Tek implementasyonlu interface'ler.** \"İhtimale karşı\" açılmış, asla ikinci implementasyonu gelmemiş soyutlamalar. Bu, [spekülatif genelliğin](/tr/journal/ileride-lazim-olur-kodunun-faturasi) kod tabanında bıraktığı tortudur.\n- **Onboarding haftalar sürüyor.** Olgun ve sade bir sistemde yeni mühendis ilk günlerde üretkendir. Haftalar sürüyorsa, kod kendini anlatmıyordur.\n- **Kimse sistemi tahtaya çizemiyor.** \"Şu kutu şuraya bağlı\" diye anlatılamayan bir mimari, anlaşılmıyor demektir.\n- **Test kurulumu testin kendisinden uzun.** Bir davranışı test etmek için kurulması gereken on mock, o davranışın çok fazla şeye bağlı olduğunun itirafıdır.\n- **\"Oraya dokunma, kırılır.\"** Bir kod bölgesi dokunulmaz ilan edilmişse, o bölge zaten kırıktır — sadece henüz patlamamıştır.\n- **Kimsenin kullanmadığı config, flag ve seçenekler.** Açılıp unutulmuş her seçenek, kodu her okuyanın hesaba katması gereken bir daldır.\n\nBir sistemde bu sinyallerden ikisi-üçü varsa, tartışma \"karmaşık mı\" değil, \"nereden başlayacağız\"dır.\n\n## Sadeleştirmeye nereden başlanır?\n\nSadeleştirme bir yeniden yazım değildir; çoğu zaman bir **çıkarma** işlemidir. Sırayla:\n\n1. **Ölü kodu sil.** Kullanılmayan flag, çağrılmayan method, ulaşılmayan branch. En düşük riskli, en yüksek getirili adım — silinen kodun bug'ı olmaz.\n2. **Tek implementasyonlu interface'leri çökert.** Soyutlama gerçek bir ikinci kullanım kazanana kadar somut sınıfı doğrudan kullanın. Dikiş gerçekten gerektiğinde — [ihtiyaç kanıtlandığında](/tr/journal/ileride-lazim-olur-kodunun-faturasi) — birkaç saatlik bir refactor'la geri gelir.\n3. **Erken dolaylılığı satır içine al.** Tek yerden çağrılan, sadece argümanı bir başka fonksiyona geçiren katmanları kaldırın.\n4. **Sınırları netleştir.** Geriye kalan karmaşıklığı [modüler monolitteki](/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum) gibi net sınırların arkasına toplayın — böylece bir dahaki birikim erken görünür.\n\nHer adımdan sonra durun ve ölçün. Sadeleştirme de aşırıya kaçabilir; amaç \"en az kod\" değil, \"en az gereksiz kod\".\n\n## Karmaşıklık her zaman kötü değil\n\nÖnemli bir ayrım: karmaşıklığın iki türü var. Fred Brooks'un kavramlarıyla — **özsel (essential)** ve **tesadüfi (accidental)**.\n\nÖzsel karmaşıklık domain'in kendisinden gelir. Vergi hesabı karmaşıktır, çünkü vergi mevzuatı karmaşıktır; bunu silemezsiniz, sadece dürüstçe modelleyebilirsiniz. Tesadüfi karmaşıklık ise sizin eklediğinizdir: gereksiz katman, erken soyutlama, unutulmuş flag.\n\nSadeleştirmenin hedefi tesadüfi olanı temizlemektir — özsel olanı değil. Bir sistemi \"çok basit\" yapıp domain'in gerçek karmaşıklığını görmezden gelmek de bir hatadır; o karmaşıklık silinmez, sadece yanlış yere — genelde çağıran kodun içine — taşınır.\n\nİyi mimari, domain ne kadar karmaşıksa o kadar karmaşıktır; daha fazlası değil.\n\n---\n\nKarmaşıklık fark edilmeden birikir; bu yüzden onu fark edilir kılan sinyalleri ezbere bilmek bir mühendislik becerisidir. Sinyali adlandıramıyorsanız, sadeleştirmeye de başlayamazsınız.\n\nBir sistemi basit tutmak, onu basit kurmaktan daha çok emek ister — ve o emeğe değer.",
      "date_published": "2026-07-25T00:00:00.000Z",
      "tags": [
        "architecture",
        "simplicity",
        "decisions",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/circuit-breaker-dis-servis-entegrasyonu/",
      "url": "https://sade.dev/tr/notes/circuit-breaker-dis-servis-entegrasyonu/",
      "title": "Dış Servis Entegrasyonlarında Circuit Breaker",
      "summary": "Yavaş bir dış servis, çökmüş birinden daha hızlı düşürür: çağrı process'i tutar, havuz dolar ve o servisle ilgisi olmayan endpoint'ler zaman aşımına uğramaya başlar. Sırayla üç savunma — her çağrıya açık ve kısa bir timeout, yalnızca exponential backoff ve jitter ile retry, ve hiç çağrı yapmamaya karar veren bir circuit breaker. Değerini, devre açıkken sunduğunuz fallback belirler.",
      "content_html": "<p>Bir SMS sağlayıcısı yavaşladı — çökmedi bile, sadece yanıt süresi 30 saniyeye çıktı. Yarım saat içinde bütün uygulama yanıt veremez hâle geldi; SMS ile hiç ilgisi olmayan endpoint’ler bile zaman aşımına uğruyordu. Tek bir yavaş dış servis, koca sistemi birlikte aşağı çekmişti.</p>\n<p>Bir dış servis er ya da geç çöker. Soru o değil. Soru şu: sizin sisteminiz onunla birlikte mi çökecek?</p>\n<h2 id=\"zincirleme-çöküş-nasıl-olur\">Zincirleme çöküş nasıl olur?</h2>\n<p>Mantık basit ve acımasız. Yavaş bir dış çağrı, onu yapan worker’ı — ya da PHP-FPM process’ini — kendine kilitler. Çağrı 30 saniye sürerse, o process 30 saniye boyunca başka hiçbir isteğe bakamaz.</p>\n<p>İstekler birikir, process havuzu dolar. Havuz dolunca — SMS ile hiç ilgisi olmayanlar dâhil — yeni istekler de bekler. Sağlıklı endpoint’leriniz, hasta bir bağımlılık yüzünden ölür. Buna <strong>cascading failure</strong> denir ve neredeyse her zaman bir yavaşlamayla başlar.</p>\n<h2 id=\"i̇lk-savunma-timeout\">İlk savunma: timeout</h2>\n<p>İlk ve en ucuz savunma, her dış çağrıya <strong>açık ve kısa</strong> bir timeout vermektir. Varsayılan timeout’lar — çoğu HTTP istemcisinde 30 saniye veya daha fazla — production için kabul edilemez.</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">$</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">response</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> Http</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">timeout</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">5</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">        // toplam süre</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    -&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">connectTimeout</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">2</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">             // bağlantı kurma süresi</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    -&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">get</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">https://sms-provider.example/send</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span></code></pre>\n<p><a href=\"/tr/notes/laravel-queue-production-yavaslama/\">Laravel queue yazısındaki</a> kural burada da geçerli: varsayılan 30 saniye değil, 5. Bir process’in bir bağımlılığı ne kadar süre bekleyeceğini şansa bırakmayın — bu sayı bir karar olmalı.</p>\n<h2 id=\"i̇kinci-savunma-retry--ama-dikkatli\">İkinci savunma: retry — ama dikkatli</h2>\n<p>Geçici hatalar için retry mantıklıdır, ama körlemesine retry zarar verir. Çöken bir servisi hemen yeniden denemek ona daha çok yük bindirir — buna <strong>retry storm</strong> denir; servisi tam ayağa kalkamadan tekrar devirir.</p>\n<p>Retry yapıyorsanız iki şey şart: exponential backoff (her denemede artan bekleme) ve jitter (rastgele sapma — tüm client’lar aynı anda denemesin).</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">Http</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">retry</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">3</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> function</span><span style=\"color:#999999;--shiki-dark:#666666\"> (</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">int</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">attempt</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">    // exponential backoff (200, 400, 800 ms) ve üstüne jitter</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 200</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> *</span><span style=\"color:#999999;--shiki-dark:#666666\"> (</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">2</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> **</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">attempt</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> -</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 1</span><span style=\"color:#999999;--shiki-dark:#666666\">))</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> random_int</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">0</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 100</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">},</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> function</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">exception</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">exception</span><span style=\"color:#999999;--shiki-dark:#666666\"> instanceof</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> ConnectionException</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">})</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">timeout</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">5</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">get</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span></code></pre>\n<p>Retry sayısını düşük tutun. Üç denemeden sonra hâlâ hata varsa, sorun geçici değildir; denemeye devam etmek yalnızca kendi process’inizi daha uzun bağlar.</p>\n<h2 id=\"üçüncü-savunma-circuit-breaker\">Üçüncü savunma: circuit breaker</h2>\n<p>Timeout tek bir çağrıyı sınırlar; circuit breaker <strong>çağrı yapmayı bırakmaya</strong> karar verir. Mantığı bir elektrik sigortasından gelir ve üç durumu vardır:</p>\n<ul>\n<li><strong>Closed</strong> — her şey normal, çağrılar geçer. Hatalar sayılır.</li>\n<li><strong>Open</strong> — son N çağrının çoğu başarısız oldu; devre açılır. Artık dış servise hiç gidilmez, çağrı <strong>anında</strong> hata döner.</li>\n<li><strong>Half-open</strong> — bir süre sonra devre tek bir deneme çağrısına izin verir. Başarılıysa <code>closed</code>’a döner, değilse tekrar <code>open</code>.</li>\n</ul>\n<p>Kazanç şu: dış servis hastayken siz onu beklemezsiniz. 5 saniyelik timeout bile process başına 5 saniyedir; <code>open</code> durumda bu süre 0’a iner. Fail-fast, yavaş-fail’den iyidir.</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">// Kavramsal akış — eşik, sayaç ve zamanlayıcıyı olgun bir pakete bırakın.</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">if</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">breaker</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">isOpen</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">sms</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">))</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">this</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">queueForLater</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">message</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">   // devre açık: hiç deneme yok</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">try</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">response</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> Http</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">timeout</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">5</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">post</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">smsUrl</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">payload</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">breaker</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">recordSuccess</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">sms</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">}</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> catch</span><span style=\"color:#999999;--shiki-dark:#666666\"> (\\</span><span style=\"color:#998418;--shiki-dark:#B8A965\">Throwable</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">e</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">breaker</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">recordFailure</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">sms</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">          // eşik aşılırsa devre açılır</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">this</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">queueForLater</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">message</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">}</span></span></code></pre>\n<p>Bu mantığı sıfırdan yazmak yerine olgun bir paket kullanın — durum saymayı, eşiği ve zamanlayıcıyı doğru kurmak göründüğünden inceliklidir.</p>\n<h2 id=\"fallback-devre-açıkken-ne-yapılır\">Fallback: devre açıkken ne yapılır?</h2>\n<p>Circuit breaker’ın değeri, <code>open</code> durumda ne yaptığınızla ortaya çıkar. “Hata döndür” en zayıf seçenektir. Daha iyileri:</p>\n<ul>\n<li><strong>Kuyruğa al, sonra gönder.</strong> SMS hemen gitmek zorunda değilse, devre kapanınca işlenmek üzere <a href=\"/tr/notes/senkron-mu-asenkron-mu/\">kuyruğa</a> bırakın.</li>\n<li><strong>Bayat ama geçerli veriyi sun.</strong> Bir döviz kuru servisi çökmüşse, son bilinen kuru göstermek hiç göstermemekten iyidir.</li>\n<li><strong>Özelliği zarifçe kıs.</strong> “Öneriler şu an yüklenemiyor” mesajı, koca sayfayı çökertmekten iyidir.</li>\n</ul>\n<p>Hangi fallback’in doğru olduğu işe bağlıdır — ama “fallback yok” bir cevap değildir.</p>\n<h2 id=\"ne-zaman-bu-kadarına-gerek-yok\">Ne zaman bu kadarına gerek yok?</h2>\n<p>Her dış çağrı circuit breaker istemez. Timeout <strong>her zaman</strong> şart. Retry, yalnızca işlem idempotent ise. Circuit breaker ise asıl olarak çağrı sık ve servis kullanıcı yolundayken değer katar — günde birkaç kez çağrılan bir entegrasyon için timeout ve makul bir retry çoğu zaman yeter.</p>\n<hr/>\n<p>Dış servis çökmesi bir “ihtimal” değil, bir “ne zaman” sorusudur. Onunla birlikte çökmek ise bir tasarım tercihidir — ve değiştirilebilir bir tercih.</p>\n<p>Sisteminizi, en zayıf bağımlılığınız kadar kırılgan bırakmayın.</p>",
      "content_text": "Bir SMS sağlayıcısı yavaşladı — çökmedi bile, sadece yanıt süresi 30 saniyeye çıktı. Yarım saat içinde bütün uygulama yanıt veremez hâle geldi; SMS ile hiç ilgisi olmayan endpoint'ler bile zaman aşımına uğruyordu. Tek bir yavaş dış servis, koca sistemi birlikte aşağı çekmişti.\n\nBir dış servis er ya da geç çöker. Soru o değil. Soru şu: sizin sisteminiz onunla birlikte mi çökecek?\n\n## Zincirleme çöküş nasıl olur?\n\nMantık basit ve acımasız. Yavaş bir dış çağrı, onu yapan worker'ı — ya da PHP-FPM process'ini — kendine kilitler. Çağrı 30 saniye sürerse, o process 30 saniye boyunca başka hiçbir isteğe bakamaz.\n\nİstekler birikir, process havuzu dolar. Havuz dolunca — SMS ile hiç ilgisi olmayanlar dâhil — yeni istekler de bekler. Sağlıklı endpoint'leriniz, hasta bir bağımlılık yüzünden ölür. Buna **cascading failure** denir ve neredeyse her zaman bir yavaşlamayla başlar.\n\n## İlk savunma: timeout\n\nİlk ve en ucuz savunma, her dış çağrıya **açık ve kısa** bir timeout vermektir. Varsayılan timeout'lar — çoğu HTTP istemcisinde 30 saniye veya daha fazla — production için kabul edilemez.\n\n```php\n$response = Http::timeout(5)        // toplam süre\n    ->connectTimeout(2)             // bağlantı kurma süresi\n    ->get('https://sms-provider.example/send');\n```\n\n[Laravel queue yazısındaki](/tr/notes/laravel-queue-production-yavaslama) kural burada da geçerli: varsayılan 30 saniye değil, 5. Bir process'in bir bağımlılığı ne kadar süre bekleyeceğini şansa bırakmayın — bu sayı bir karar olmalı.\n\n## İkinci savunma: retry — ama dikkatli\n\nGeçici hatalar için retry mantıklıdır, ama körlemesine retry zarar verir. Çöken bir servisi hemen yeniden denemek ona daha çok yük bindirir — buna **retry storm** denir; servisi tam ayağa kalkamadan tekrar devirir.\n\nRetry yapıyorsanız iki şey şart: exponential backoff (her denemede artan bekleme) ve jitter (rastgele sapma — tüm client'lar aynı anda denemesin).\n\n```php\nHttp::retry(3, function (int $attempt) {\n    // exponential backoff (200, 400, 800 ms) ve üstüne jitter\n    return 200 * (2 ** ($attempt - 1)) + random_int(0, 100);\n}, function ($exception) {\n    return $exception instanceof ConnectionException;\n})->timeout(5)->get($url);\n```\n\nRetry sayısını düşük tutun. Üç denemeden sonra hâlâ hata varsa, sorun geçici değildir; denemeye devam etmek yalnızca kendi process'inizi daha uzun bağlar.\n\n## Üçüncü savunma: circuit breaker\n\nTimeout tek bir çağrıyı sınırlar; circuit breaker **çağrı yapmayı bırakmaya** karar verir. Mantığı bir elektrik sigortasından gelir ve üç durumu vardır:\n\n- **Closed** — her şey normal, çağrılar geçer. Hatalar sayılır.\n- **Open** — son N çağrının çoğu başarısız oldu; devre açılır. Artık dış servise hiç gidilmez, çağrı **anında** hata döner.\n- **Half-open** — bir süre sonra devre tek bir deneme çağrısına izin verir. Başarılıysa `closed`'a döner, değilse tekrar `open`.\n\nKazanç şu: dış servis hastayken siz onu beklemezsiniz. 5 saniyelik timeout bile process başına 5 saniyedir; `open` durumda bu süre 0'a iner. Fail-fast, yavaş-fail'den iyidir.\n\n```php\n// Kavramsal akış — eşik, sayaç ve zamanlayıcıyı olgun bir pakete bırakın.\nif ($breaker->isOpen('sms')) {\n    return $this->queueForLater($message);   // devre açık: hiç deneme yok\n}\n\ntry {\n    $response = Http::timeout(5)->post($smsUrl, $payload);\n    $breaker->recordSuccess('sms');\n} catch (\\Throwable $e) {\n    $breaker->recordFailure('sms');          // eşik aşılırsa devre açılır\n    return $this->queueForLater($message);\n}\n```\n\nBu mantığı sıfırdan yazmak yerine olgun bir paket kullanın — durum saymayı, eşiği ve zamanlayıcıyı doğru kurmak göründüğünden inceliklidir.\n\n## Fallback: devre açıkken ne yapılır?\n\nCircuit breaker'ın değeri, `open` durumda ne yaptığınızla ortaya çıkar. \"Hata döndür\" en zayıf seçenektir. Daha iyileri:\n\n- **Kuyruğa al, sonra gönder.** SMS hemen gitmek zorunda değilse, devre kapanınca işlenmek üzere [kuyruğa](/tr/notes/senkron-mu-asenkron-mu) bırakın.\n- **Bayat ama geçerli veriyi sun.** Bir döviz kuru servisi çökmüşse, son bilinen kuru göstermek hiç göstermemekten iyidir.\n- **Özelliği zarifçe kıs.** \"Öneriler şu an yüklenemiyor\" mesajı, koca sayfayı çökertmekten iyidir.\n\nHangi fallback'in doğru olduğu işe bağlıdır — ama \"fallback yok\" bir cevap değildir.\n\n## Ne zaman bu kadarına gerek yok?\n\nHer dış çağrı circuit breaker istemez. Timeout **her zaman** şart. Retry, yalnızca işlem idempotent ise. Circuit breaker ise asıl olarak çağrı sık ve servis kullanıcı yolundayken değer katar — günde birkaç kez çağrılan bir entegrasyon için timeout ve makul bir retry çoğu zaman yeter.\n\n---\n\nDış servis çökmesi bir \"ihtimal\" değil, bir \"ne zaman\" sorusudur. Onunla birlikte çökmek ise bir tasarım tercihidir — ve değiştirilebilir bir tercih.\n\nSisteminizi, en zayıf bağımlılığınız kadar kırılgan bırakmayın.",
      "date_published": "2026-07-18T00:00:00.000Z",
      "tags": [
        "resilience",
        "architecture",
        "api",
        "production",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/serverless-gecis-karari/",
      "url": "https://sade.dev/tr/notes/serverless-gecis-karari/",
      "title": "Serverless'a Geçiş Kararı: Cold-Start ve Vendor Lock-in",
      "summary": "Serverless bir mimari modası değil, bir iş yükü profili için bir araçtır: değişken, seyrek, patlamalı. Sabit trafik, kullanım başına ödemeyi her saniye ödemeye çevirir; düşük trafikli bir yolda ise cold-start istisna değil kuraldır ve kullanıcı onu neredeyse her istekte öder. Asıl bağ runtime değil, etrafındaki yapıştırıcıdır — event kaynakları, IAM, gateway — ve çıkışı bir taşımaya değil, yeniden yazıma çevirir.",
      "content_html": "<p>Bir ekip yeni servisini “serverless first” kararıyla AWS Lambda üzerine kurdu. Oysa trafik sabit ve öngörülebilirdi — gün boyu aşağı yukarı aynı. Ay sonunda fatura, aynı işi yapan tek bir VPS’in birkaç katı çıktı; üstelik p95 latency de daha kötüydü.</p>\n<p>Serverless yanlış bir teknoloji değil. Yanlış iş yüküne uygulanmış bir teknolojiydi.</p>\n<h2 id=\"serverless-ne-satın-alır\">Serverless ne satın alır</h2>\n<p>FaaS’ın (Function as a Service) gerçek getirileri var:</p>\n<ul>\n<li><strong>Sunucu yönetimi yok.</strong> Yama, ölçekleme, kapasite planı sağlayıcıda.</li>\n<li><strong>Sıfıra inen ölçek.</strong> İstek yoksa çalışan hiçbir şey yok, ödenen hiçbir şey yok.</li>\n<li><strong>Kullanım başına ödeme.</strong> Atıl kapasite için para ödemezsiniz.</li>\n</ul>\n<p>Bu üçü, doğru iş yükünde gerçekten kazandırır. Mesele, o iş yükünü doğru teşhis etmek.</p>\n<h2 id=\"cold-startın-gerçek-maliyeti\">Cold-start’ın gerçek maliyeti</h2>\n<p>Bir fonksiyon bir süredir çağrılmadıysa, sağlayıcı onu sıfırdan ayağa kaldırır: runtime yüklenir, bağımlılıklar init olur, kod çalışır. Bu <strong>cold-start</strong>, çalışan bir process’e göre 100 ms’nin altından saniyelere kadar ek gecikmedir.</p>\n<p>Önemli ayrım: cold-start, düşük trafikli bir endpoint için <strong>istisna değil kural</strong>dır. Saatte birkaç çağrı alan bir fonksiyonda neredeyse her istek cold’dur. “Ölçeğe göre ödeme” cazip gelir, ama sıfıra yakın trafikte ödediğiniz şey, kullanıcının her seferinde gördüğü gecikmedir. Latency’ye duyarlı bir kullanıcı yolunda bu doğrudan bir UX maliyetidir.</p>\n<h2 id=\"vendor-lock-in\">Vendor lock-in</h2>\n<p>Lock-in yalnızca runtime değildir; asıl bağ etrafındaki yapıştırıcıdadır. Bir serverless uygulaması zamanla şunlara da bağlanır: sağlayıcının event kaynakları, kimlik/yetki modeli (IAM), yönetilen kuyruğu, API gateway’i, dağıtım araçları.</p>\n<p>Tek bir fonksiyonu taşımak kolaydır. Ama gerçek bir sistemde “serverless’tan çıkmak” çoğu zaman bir taşıma değil, bir yeniden yazımdır. Bu, <a href=\"/tr/journal/neden-boring-architecture/\">boring architecture</a> yazısındaki innovation token mantığıyla doğrudan ilgili: tek bir karar gibi görünen şey, aslında geri alınması pahalı bir bağdır.</p>\n<h2 id=\"hangi-iş-yükünde-kazandırır\">Hangi iş yükünde kazandırır?</h2>\n<p>Serverless, yükün <strong>değişken ve seyrek</strong> olduğu yerde parlar:</p>\n<ul>\n<li>Zamanlanmış işler — gecelik raporlar, cron benzeri görevler.</li>\n<li>Webhook alıcıları — ne zaman, ne sıklıkta geleceği belirsiz.</li>\n<li>Düşük hacimli iç araçlar — günde birkaç kez kullanılan paneller.</li>\n<li>Ani patlamalı (bursty) batch işleri — saatte bir, kısa süreliğine yüksek paralellik.</li>\n</ul>\n<p>Bu yüklerde sıfıra inen ölçek gerçek para tasarrufudur ve cold-start çoğu zaman önemsizdir — kimse bir gece raporunun 800 ms geç başlamasını dert etmez.</p>\n<h2 id=\"hangi-iş-yükünde-kaybettirir\">Hangi iş yükünde kaybettirir?</h2>\n<p>Bunun tersi de net:</p>\n<ul>\n<li><strong>Sabit, sürekli trafik.</strong> Her zaman çalışan bir API’de “kullanım başına ödeme”, “her saniye ödeme”ye dönüşür — ve genelde sürekli çalışan bir sunucudan pahalıdır.</li>\n<li><strong>Latency’ye duyarlı yollar.</strong> Cold-start, kullanıcının beklediği bir istekte kabul edilemez.</li>\n<li><strong>Uzun süren işler.</strong> FaaS’ın çalışma süresi sınırları vardır; uzun işler ya bölünür ya da uymaz.</li>\n</ul>\n<p>Bu profildeki bir iş yükü için sıkıcı bir VPS — <a href=\"/tr/systems/tek-vps-coklu-proje-mimarisi/\">tek VPS üzerinde çoklu proje</a> yazısındaki kurulum gibi — hem daha ucuz hem daha öngörülebilirdir. Sabit yükün maliyeti, sabit bir sunucuyla eşleşir.</p>\n<h2 id=\"laravel-ve-serverless\">Laravel ve serverless</h2>\n<p>Laravel tarafında Laravel Vapor, uygulamayı Lambda üzerinde çalıştırmayı mümkün kılar ve iyi de çalışır. Ama çalışması, doğru karar olduğu anlamına gelmez: Vapor’la birlikte cold-start’ı ve AWS bağını da miras alırsınız.</p>\n<p>Karar yine iş yüküdür. Trafiği patlamalı, seyrek ya da öngörülemez bir Laravel uygulaması için Vapor mantıklı. Sürekli ve latency’ye duyarlı bir uygulama için, klasik bir sunucu hâlâ daha sade ve ucuz cevaptır.</p>\n<h2 id=\"karışık-kullanın\">Karışık kullanın</h2>\n<p>Bu, ikili bir seçim değil. Sürekli yükü taşıyan sıkıcı bir sunucu + yalnızca seyrek/patlamalı işleri (webhook, gece raporu) serverless’a vermek çoğu zaman en dürüst dağılımdır. Her iş yükünü, kendi maliyet profiline ait olduğu yere koyun.</p>\n<hr/>\n<p>Serverless bir mimari modası değil, belirli bir iş yükü profili için bir araçtır. O profil sizde varsa kazandırır; yoksa cold-start ve lock-in olarak geri ödenir.</p>\n<p>Önce yükünüzü ölçün; araç sonra gelir.</p>",
      "content_text": "Bir ekip yeni servisini \"serverless first\" kararıyla AWS Lambda üzerine kurdu. Oysa trafik sabit ve öngörülebilirdi — gün boyu aşağı yukarı aynı. Ay sonunda fatura, aynı işi yapan tek bir VPS'in birkaç katı çıktı; üstelik p95 latency de daha kötüydü.\n\nServerless yanlış bir teknoloji değil. Yanlış iş yüküne uygulanmış bir teknolojiydi.\n\n## Serverless ne satın alır\n\nFaaS'ın (Function as a Service) gerçek getirileri var:\n\n- **Sunucu yönetimi yok.** Yama, ölçekleme, kapasite planı sağlayıcıda.\n- **Sıfıra inen ölçek.** İstek yoksa çalışan hiçbir şey yok, ödenen hiçbir şey yok.\n- **Kullanım başına ödeme.** Atıl kapasite için para ödemezsiniz.\n\nBu üçü, doğru iş yükünde gerçekten kazandırır. Mesele, o iş yükünü doğru teşhis etmek.\n\n## Cold-start'ın gerçek maliyeti\n\nBir fonksiyon bir süredir çağrılmadıysa, sağlayıcı onu sıfırdan ayağa kaldırır: runtime yüklenir, bağımlılıklar init olur, kod çalışır. Bu **cold-start**, çalışan bir process'e göre 100 ms'nin altından saniyelere kadar ek gecikmedir.\n\nÖnemli ayrım: cold-start, düşük trafikli bir endpoint için **istisna değil kural**dır. Saatte birkaç çağrı alan bir fonksiyonda neredeyse her istek cold'dur. \"Ölçeğe göre ödeme\" cazip gelir, ama sıfıra yakın trafikte ödediğiniz şey, kullanıcının her seferinde gördüğü gecikmedir. Latency'ye duyarlı bir kullanıcı yolunda bu doğrudan bir UX maliyetidir.\n\n## Vendor lock-in\n\nLock-in yalnızca runtime değildir; asıl bağ etrafındaki yapıştırıcıdadır. Bir serverless uygulaması zamanla şunlara da bağlanır: sağlayıcının event kaynakları, kimlik/yetki modeli (IAM), yönetilen kuyruğu, API gateway'i, dağıtım araçları.\n\nTek bir fonksiyonu taşımak kolaydır. Ama gerçek bir sistemde \"serverless'tan çıkmak\" çoğu zaman bir taşıma değil, bir yeniden yazımdır. Bu, [boring architecture](/tr/journal/neden-boring-architecture) yazısındaki innovation token mantığıyla doğrudan ilgili: tek bir karar gibi görünen şey, aslında geri alınması pahalı bir bağdır.\n\n## Hangi iş yükünde kazandırır?\n\nServerless, yükün **değişken ve seyrek** olduğu yerde parlar:\n\n- Zamanlanmış işler — gecelik raporlar, cron benzeri görevler.\n- Webhook alıcıları — ne zaman, ne sıklıkta geleceği belirsiz.\n- Düşük hacimli iç araçlar — günde birkaç kez kullanılan paneller.\n- Ani patlamalı (bursty) batch işleri — saatte bir, kısa süreliğine yüksek paralellik.\n\nBu yüklerde sıfıra inen ölçek gerçek para tasarrufudur ve cold-start çoğu zaman önemsizdir — kimse bir gece raporunun 800 ms geç başlamasını dert etmez.\n\n## Hangi iş yükünde kaybettirir?\n\nBunun tersi de net:\n\n- **Sabit, sürekli trafik.** Her zaman çalışan bir API'de \"kullanım başına ödeme\", \"her saniye ödeme\"ye dönüşür — ve genelde sürekli çalışan bir sunucudan pahalıdır.\n- **Latency'ye duyarlı yollar.** Cold-start, kullanıcının beklediği bir istekte kabul edilemez.\n- **Uzun süren işler.** FaaS'ın çalışma süresi sınırları vardır; uzun işler ya bölünür ya da uymaz.\n\nBu profildeki bir iş yükü için sıkıcı bir VPS — [tek VPS üzerinde çoklu proje](/tr/systems/tek-vps-coklu-proje-mimarisi) yazısındaki kurulum gibi — hem daha ucuz hem daha öngörülebilirdir. Sabit yükün maliyeti, sabit bir sunucuyla eşleşir.\n\n## Laravel ve serverless\n\nLaravel tarafında Laravel Vapor, uygulamayı Lambda üzerinde çalıştırmayı mümkün kılar ve iyi de çalışır. Ama çalışması, doğru karar olduğu anlamına gelmez: Vapor'la birlikte cold-start'ı ve AWS bağını da miras alırsınız.\n\nKarar yine iş yüküdür. Trafiği patlamalı, seyrek ya da öngörülemez bir Laravel uygulaması için Vapor mantıklı. Sürekli ve latency'ye duyarlı bir uygulama için, klasik bir sunucu hâlâ daha sade ve ucuz cevaptır.\n\n## Karışık kullanın\n\nBu, ikili bir seçim değil. Sürekli yükü taşıyan sıkıcı bir sunucu + yalnızca seyrek/patlamalı işleri (webhook, gece raporu) serverless'a vermek çoğu zaman en dürüst dağılımdır. Her iş yükünü, kendi maliyet profiline ait olduğu yere koyun.\n\n---\n\nServerless bir mimari modası değil, belirli bir iş yükü profili için bir araçtır. O profil sizde varsa kazandırır; yoksa cold-start ve lock-in olarak geri ödenir.\n\nÖnce yükünüzü ölçün; araç sonra gelir.",
      "date_published": "2026-07-11T00:00:00.000Z",
      "tags": [
        "serverless",
        "architecture",
        "cloud",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/bff-backend-for-frontend/",
      "url": "https://sade.dev/tr/notes/bff-backend-for-frontend/",
      "title": "BFF Pattern: Mobil ve Web İçin Ayrı API Katmanı",
      "summary": "Tek bir genel API'yi her istemciye uydurmanın bedeli over-fetching, chatty akış, koşullu alanlar ve web ile mobili tek kontrata kilitleyen bir versiyonlama düğümüdür. BFF bu faturayı istemci başına ince bir uyarlama katmanıyla keser — agregasyon ve yanıt şekillendirme, asla iş kuralı değil. Katmanı haklı çıkaran şey istemci sayısı değil, istemciler arasındaki mesafedir.",
      "content_html": "<p>Bir mobil uygulamanın tek bir ana ekranı çizmek için API’ye dokuz ayrı istek attığını gördüm. API aslında web için tasarlanmıştı; mobil, web’in böldüğü veriyi parça parça toplayıp ekranda birleştiriyordu. Her açılış dokuz round-trip — üstelik mobil ağ koşullarında.</p>\n<p>Bu, “tek API herkese yeter” varsayımının faturasıydı. BFF — Backend for Frontend — tam da bu faturayı kesmek için var.</p>\n<h2 id=\"bff-nedir\">BFF nedir</h2>\n<p>BFF, her istemci türü için ince ve ona özel bir API katmanıdır. Web’in kendi BFF’i, mobilin kendi BFF’i olur. Her biri arkadaki asıl servisleri/domain’i çağırır, ama yanıtı kendi istemcisinin ekranına göre şekillendirir.</p>\n<p>Asıl iş mantığı BFF’te değildir. BFF sadece bir <strong>uyarlama katmanı</strong>: agregasyon yapar, gereksiz alanı kırpar, istemcinin tek ekranda ihtiyaç duyduğu şeyi tek yanıtta toplar.</p>\n<h2 id=\"tek-apiyi-herkese-uydurmanın-gizli-maliyeti\">Tek API’yi herkese uydurmanın gizli maliyeti</h2>\n<p>Tek bir genel API, zamanla iki istemciyi de tam memnun edemez:</p>\n<ul>\n<li><strong>Over-fetching.</strong> Mobil, 40 alanlı bir kullanıcı nesnesi indirir, 4’ünü kullanır. Geri kalanı boşa giden bant genişliği ve pil.</li>\n<li><strong>Chatty akış.</strong> Web’in normal bulduğu “önce listeyi çek, sonra her satır için detay çek” akışı, mobil ağda dokuz round-trip’e dönüşür.</li>\n<li><strong>Koşullu alanlar.</strong> Yanıt <code>if mobile then ... else ...</code> ile dallanmaya başlar. Tek endpoint, iki istemcinin gereksinimini aynı anda taşımaya çalışır ve ikisinde de orta karar bir iş çıkarır.</li>\n<li><strong>Versiyonlama düğümü.</strong> Web için yapılması gereken bir değişiklik, mobilin yayınlanmış sürümünü kırma riski taşır. İki istemci tek kontrata kilitlenir.</li>\n</ul>\n<p>Bu maliyetler tek tek küçük görünür; toplamı bir mimari problemdir.</p>\n<h2 id=\"bff-ne-zaman-gerekli\">BFF ne zaman gerekli?</h2>\n<p>BFF, istemcilerin ihtiyacı <strong>gerçekten</strong> ayrıştığında gerekçelidir:</p>\n<ul>\n<li>Mobil ile web belirgin biçimde farklı ekranlara, farklı veri şekillerine ihtiyaç duyuyor.</li>\n<li>Mobil ağ ve pil kısıtları, round-trip sayısını ve payload boyutunu birinci sınıf bir kaygı yapıyor.</li>\n<li>İstemciler farklı tempoda yayınlanıyor — mobil sürümü mağaza onayına takılıyken web her gün deploy ediliyor; tek kontrat ikisini birbirine kilitliyor.</li>\n</ul>\n<p>Bu farklar varsa, her istemciye kendi BFF’ini vermek, tek API’yi sürekli ikisine birden uydurmaya çalışmaktan ucuzdur.</p>\n<h2 id=\"bff-ne-zaman-gereksiz\">BFF ne zaman gereksiz?</h2>\n<p>Tek bir istemciniz varsa BFF, çözdüğünden fazla katman ekler — sadece fazladan bir sıçrama.</p>\n<p>İki istemciniz var ama ikisi de neredeyse aynı veriyi aynı şekilde istiyorsa, yine gereksiz. BFF’i haklı çıkaran şey istemci sayısı değil, istemciler <strong>arasındaki farktır</strong>. Fark yoksa ayrı katman da olmamalı — bu, <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> API katmanındaki hâli olur.</p>\n<h2 id=\"bff-mikroservis-değildir\">BFF mikroservis değildir</h2>\n<p>Sık karışan nokta: BFF eklemek sizi dağıtık mimariye sokmaz. BFF bir sunum/agregasyon katmanıdır, bir domain servisi değil. İçine iş kuralı koymak, kuralı istemci sayısı kadar kopyalamak demektir.</p>\n<p>BFF’in mikroservisle tek ilişkisi şu: ikisi de “ayrı bir deployable’a değer mi?” sorusunu sorar. Cevap çoğu zaman “hayır” — <a href=\"/tr/journal/mikroservise-ne-zaman-gecerim/\">mikroservise geçiş kararındaki</a> ölçülmüş-sinyal eşiği burada da geçerli.</p>\n<h2 id=\"hafif-başlayın\">Hafif başlayın</h2>\n<p>“BFF” demek, ayrı bir sunucu kurmak demek zorunda değil. Aynı monolit içinde:</p>\n<ul>\n<li>İstemci başına bir route namespace — <code>routes/web-api.php</code>, <code>routes/mobile-api.php</code> — her biri kendi controller’larıyla, kendi yanıt şekliyle.</li>\n<li>Ya da tek bir GraphQL katmanı: her istemci ihtiyacı kadarını sorgular, over-fetching kendiliğinden çözülür.</li>\n</ul>\n<p>Asıl fikir bir kutu değil, bir sınırdır: istemciye özel uyarlamayı tek bir yerde toplayın, domain’i ondan temiz tutun. Ayrı bir deployable’a ancak <a href=\"/tr/journal/mikroservise-ne-zaman-gecerim/\">ölçülmüş bir sinyal</a> zorladığında geçin.</p>\n<hr/>\n<p>BFF, “her istemci aynı API’yle idare etsin” inadının panzehiridir — ama yalnızca istemciler gerçekten ayrıştığında. Fark yoksa katman da olmamalı.</p>\n<p>Katmanı ekleyen şey istemci sayısı değil, aralarındaki mesafedir.</p>",
      "content_text": "Bir mobil uygulamanın tek bir ana ekranı çizmek için API'ye dokuz ayrı istek attığını gördüm. API aslında web için tasarlanmıştı; mobil, web'in böldüğü veriyi parça parça toplayıp ekranda birleştiriyordu. Her açılış dokuz round-trip — üstelik mobil ağ koşullarında.\n\nBu, \"tek API herkese yeter\" varsayımının faturasıydı. BFF — Backend for Frontend — tam da bu faturayı kesmek için var.\n\n## BFF nedir\n\nBFF, her istemci türü için ince ve ona özel bir API katmanıdır. Web'in kendi BFF'i, mobilin kendi BFF'i olur. Her biri arkadaki asıl servisleri/domain'i çağırır, ama yanıtı kendi istemcisinin ekranına göre şekillendirir.\n\nAsıl iş mantığı BFF'te değildir. BFF sadece bir **uyarlama katmanı**: agregasyon yapar, gereksiz alanı kırpar, istemcinin tek ekranda ihtiyaç duyduğu şeyi tek yanıtta toplar.\n\n## Tek API'yi herkese uydurmanın gizli maliyeti\n\nTek bir genel API, zamanla iki istemciyi de tam memnun edemez:\n\n- **Over-fetching.** Mobil, 40 alanlı bir kullanıcı nesnesi indirir, 4'ünü kullanır. Geri kalanı boşa giden bant genişliği ve pil.\n- **Chatty akış.** Web'in normal bulduğu \"önce listeyi çek, sonra her satır için detay çek\" akışı, mobil ağda dokuz round-trip'e dönüşür.\n- **Koşullu alanlar.** Yanıt `if mobile then ... else ...` ile dallanmaya başlar. Tek endpoint, iki istemcinin gereksinimini aynı anda taşımaya çalışır ve ikisinde de orta karar bir iş çıkarır.\n- **Versiyonlama düğümü.** Web için yapılması gereken bir değişiklik, mobilin yayınlanmış sürümünü kırma riski taşır. İki istemci tek kontrata kilitlenir.\n\nBu maliyetler tek tek küçük görünür; toplamı bir mimari problemdir.\n\n## BFF ne zaman gerekli?\n\nBFF, istemcilerin ihtiyacı **gerçekten** ayrıştığında gerekçelidir:\n\n- Mobil ile web belirgin biçimde farklı ekranlara, farklı veri şekillerine ihtiyaç duyuyor.\n- Mobil ağ ve pil kısıtları, round-trip sayısını ve payload boyutunu birinci sınıf bir kaygı yapıyor.\n- İstemciler farklı tempoda yayınlanıyor — mobil sürümü mağaza onayına takılıyken web her gün deploy ediliyor; tek kontrat ikisini birbirine kilitliyor.\n\nBu farklar varsa, her istemciye kendi BFF'ini vermek, tek API'yi sürekli ikisine birden uydurmaya çalışmaktan ucuzdur.\n\n## BFF ne zaman gereksiz?\n\nTek bir istemciniz varsa BFF, çözdüğünden fazla katman ekler — sadece fazladan bir sıçrama.\n\nİki istemciniz var ama ikisi de neredeyse aynı veriyi aynı şekilde istiyorsa, yine gereksiz. BFF'i haklı çıkaran şey istemci sayısı değil, istemciler **arasındaki farktır**. Fark yoksa ayrı katman da olmamalı — bu, [spekülatif genelliğin](/tr/journal/ileride-lazim-olur-kodunun-faturasi) API katmanındaki hâli olur.\n\n## BFF mikroservis değildir\n\nSık karışan nokta: BFF eklemek sizi dağıtık mimariye sokmaz. BFF bir sunum/agregasyon katmanıdır, bir domain servisi değil. İçine iş kuralı koymak, kuralı istemci sayısı kadar kopyalamak demektir.\n\nBFF'in mikroservisle tek ilişkisi şu: ikisi de \"ayrı bir deployable'a değer mi?\" sorusunu sorar. Cevap çoğu zaman \"hayır\" — [mikroservise geçiş kararındaki](/tr/journal/mikroservise-ne-zaman-gecerim) ölçülmüş-sinyal eşiği burada da geçerli.\n\n## Hafif başlayın\n\n\"BFF\" demek, ayrı bir sunucu kurmak demek zorunda değil. Aynı monolit içinde:\n\n- İstemci başına bir route namespace — `routes/web-api.php`, `routes/mobile-api.php` — her biri kendi controller'larıyla, kendi yanıt şekliyle.\n- Ya da tek bir GraphQL katmanı: her istemci ihtiyacı kadarını sorgular, over-fetching kendiliğinden çözülür.\n\nAsıl fikir bir kutu değil, bir sınırdır: istemciye özel uyarlamayı tek bir yerde toplayın, domain'i ondan temiz tutun. Ayrı bir deployable'a ancak [ölçülmüş bir sinyal](/tr/journal/mikroservise-ne-zaman-gecerim) zorladığında geçin.\n\n---\n\nBFF, \"her istemci aynı API'yle idare etsin\" inadının panzehiridir — ama yalnızca istemciler gerçekten ayrıştığında. Fark yoksa katman da olmamalı.\n\nKatmanı ekleyen şey istemci sayısı değil, aralarındaki mesafedir.",
      "date_published": "2026-07-04T00:00:00.000Z",
      "tags": [
        "architecture",
        "api",
        "bff",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/senkron-mu-asenkron-mu/",
      "url": "https://sade.dev/tr/notes/senkron-mu-asenkron-mu/",
      "title": "Senkron mu Asenkron mu? HTTP ve Kuyruk Arasındaki Sınır",
      "summary": "Senkron mu asenkron mu, bir performans sorusu değil, kim bekliyor sorusudur. Kullanıcı sonucu görmek zorundaysa iş istek içinde kalır; değilse kuyruğa gider ve istek 202 Accepted döner. Aynı çizgiyi hata keskinleştirir: senkron hata bir hata ekranıdır, asenkron hata bir retry'dır — kararsız bir üçüncü partinin senkron yolda durmamasının sebebi tam olarak budur.",
      "content_html": "<p>Bir kayıt formu sekiz saniye sürüyordu. İstek içinde sırayla şunlar oluyordu: kullanıcı kaydediliyor, hoş geldin e-postası gönderiliyor, bir PDF üretiliyor, bir CRM’e kayıt işleniyor. Kullanıcı bu sekiz saniye boyunca dönen bir spinner’a bakıyordu — oysa onu ilgilendiren tek şey, kaydının oluşup oluşmadığıydı.</p>\n<p>Bir işi HTTP isteği içinde anında mı yapmalı, kuyruğa mı atmalı? Çizgi göründüğünden net.</p>\n<h2 id=\"karar-çizgisi-kullanıcı-sonucu-bekliyor-mu\">Karar çizgisi: kullanıcı sonucu bekliyor mu?</h2>\n<p>Tek bir soru kararın çoğunu verir: <strong>kullanıcı bu işin sonucunu görmek zorunda mı?</strong></p>\n<ul>\n<li>Cevap “evet” ise iş senkron kalır — istek içinde, yanıt dönmeden biter.</li>\n<li>Cevap “hayır” ise iş kuyruğa gider — istek “kabul edildi” der ve döner.</li>\n</ul>\n<p>Yukarıdaki formda kullanıcı sadece “kaydım oluştu mu” cevabını bekliyor. E-posta, PDF, CRM — hiçbirinin sonucunu o ekranda görmesi gerekmiyor. Üçü de yanlış tarafta duruyordu.</p>\n<h2 id=\"hangi-işler-senkron-kalmalı\">Hangi işler senkron kalmalı?</h2>\n<p>Kullanıcının sonucunu doğrudan gördüğü işler istek içinde kalır:</p>\n<ul>\n<li>Girdi doğrulama — hata anında dönmeli.</li>\n<li>Asıl kaynağın yaratılması — kullanıcı oluşturma, sipariş oluşturma.</li>\n<li>Ödeme <strong>yetkilendirmesinin sonucu</strong> — kart geçti mi geçmedi mi, kullanıcı bunu o an bilmeli.</li>\n<li>Kullanıcının bir sonraki adımda göreceği veri.</li>\n</ul>\n<p>Bunları kuyruğa atmak, kullanıcıya “sonra bak” demektir — ve çoğu zaman kötü bir deneyimdir.</p>\n<h2 id=\"hangi-işler-kuyruğa-gitmeli\">Hangi işler kuyruğa gitmeli?</h2>\n<p>Kullanıcının sonucunu o an görmesi gerekmeyen her iş asenkrondur:</p>\n<ul>\n<li>E-posta ve bildirim.</li>\n<li>PDF ve rapor üretimi.</li>\n<li>Webhook gönderimi.</li>\n<li>Üçüncü parti senkronizasyonu — CRM, analitik, arama indeksi.</li>\n<li>Görsel işleme, dışa aktarma.</li>\n</ul>\n<p>Bunlar için doğru yanıt <code>202 Accepted</code>’dir: “isteğini aldım, işliyorum.” Kullanıcı beklemez, sistem işi kendi temposunda yapar.</p>\n<h2 id=\"çizgiyi-yanıt-süresi-keskinleştirir\">Çizgiyi yanıt süresi keskinleştirir</h2>\n<p>Her endpoint için bir p95 bütçeniz olmalı — diyelim 300 ms. Bir adım bu bütçeyi tek başına yiyorsa ve kullanıcı o adımın sonucunu görmek zorunda değilse, o adım kuyruğa gider.</p>\n<p>Senkron zincir, en yavaş halkası kadar hızlıdır. PDF üretimi 4 saniye sürüyorsa, kullanıcı oluşturma da 4 saniye sürer — PDF’in yanıtla hiçbir ilgisi olmasa bile.</p>\n<h2 id=\"hata-toleransı-asıl-ayrım\">Hata toleransı: asıl ayrım</h2>\n<p>Asıl mesele hız değil, hatadır. Senkron ve asenkron, başarısızlığı bambaşka karşılar:</p>\n<ul>\n<li><strong>Senkron hata</strong> = kullanıcı bir hata ekranı görür. İş kaybolur.</li>\n<li><strong>Asenkron hata</strong> = job retry edilir, gerekirse DLQ’ya düşer, <a href=\"/tr/notes/laravel-queue-production-yavaslama/\">kuyruk</a> sonra işler.</li>\n</ul>\n<p>Buradan tek bir kural çıkar: <strong>kararsız bir üçüncü parti asla senkron yolda durmamalı.</strong> O CRM çağrısı istek içindeyse, CRM her çöktüğünde sizin kayıt formunuz da çöker. Kuyruğa alınmış bir çağrı ise, CRM döndüğünde retry kendiliğinden tamamlanır. Üçüncü partinin uptime’ı sizin uptime’ınız olmaktan çıkar.</p>\n<h2 id=\"202-ve-durum-sorgusu-kalıbı\">202 ve durum sorgusu kalıbı</h2>\n<p>İş kuyruğa gidince kullanıcıya bir takip yolu bırakın:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>POST /exports          → 202 Accepted  { &quot;id&quot;: &quot;exp_1a2b&quot;, &quot;status&quot;: &quot;processing&quot; }</span></span>\n<span class=\"line\"><span>GET  /exports/exp_1a2b → 200 OK        { &quot;status&quot;: &quot;done&quot;, &quot;url&quot;: &quot;...&quot; }</span></span></code></pre>\n<p>Kullanıcı sonucu beklemiyordur ama bazen merak eder. Bir durum endpoint’i ya da hazır olunca gönderilen bir bildirim, bu boşluğu kapatır.</p>\n<h2 id=\"sınır-nerede-kalkar\">Sınır nerede kalkar?</h2>\n<p>Bazı işler ikisinin arasındadır: kullanıcı sonucu görmek ister ama iş de yavaştır. Burada üçüncü bir yol var — işi senkron başlatın, ilk anlamlı sonucu hemen döndürün, gerisini kuyruğa devredin. Sipariş kaydını anında oluşturun (senkron), faturayı arkada üretin (asenkron).</p>\n<p>Çizgiyi her iş için ayrı çizin; “her şey kuyruğa” da “her şey senkron” kadar yanlıştır.</p>\n<hr/>\n<p>Senkron mu asenkron mu sorusu bir performans sorusu değil, bir “kim bekliyor” sorusudur. Kullanıcının beklemediği hiçbir iş, onun isteğini bekletmemeli.</p>\n<p>İsteği yalnızca, cevabını gerçekten istediği işle meşgul edin.</p>",
      "content_text": "Bir kayıt formu sekiz saniye sürüyordu. İstek içinde sırayla şunlar oluyordu: kullanıcı kaydediliyor, hoş geldin e-postası gönderiliyor, bir PDF üretiliyor, bir CRM'e kayıt işleniyor. Kullanıcı bu sekiz saniye boyunca dönen bir spinner'a bakıyordu — oysa onu ilgilendiren tek şey, kaydının oluşup oluşmadığıydı.\n\nBir işi HTTP isteği içinde anında mı yapmalı, kuyruğa mı atmalı? Çizgi göründüğünden net.\n\n## Karar çizgisi: kullanıcı sonucu bekliyor mu?\n\nTek bir soru kararın çoğunu verir: **kullanıcı bu işin sonucunu görmek zorunda mı?**\n\n- Cevap \"evet\" ise iş senkron kalır — istek içinde, yanıt dönmeden biter.\n- Cevap \"hayır\" ise iş kuyruğa gider — istek \"kabul edildi\" der ve döner.\n\nYukarıdaki formda kullanıcı sadece \"kaydım oluştu mu\" cevabını bekliyor. E-posta, PDF, CRM — hiçbirinin sonucunu o ekranda görmesi gerekmiyor. Üçü de yanlış tarafta duruyordu.\n\n## Hangi işler senkron kalmalı?\n\nKullanıcının sonucunu doğrudan gördüğü işler istek içinde kalır:\n\n- Girdi doğrulama — hata anında dönmeli.\n- Asıl kaynağın yaratılması — kullanıcı oluşturma, sipariş oluşturma.\n- Ödeme **yetkilendirmesinin sonucu** — kart geçti mi geçmedi mi, kullanıcı bunu o an bilmeli.\n- Kullanıcının bir sonraki adımda göreceği veri.\n\nBunları kuyruğa atmak, kullanıcıya \"sonra bak\" demektir — ve çoğu zaman kötü bir deneyimdir.\n\n## Hangi işler kuyruğa gitmeli?\n\nKullanıcının sonucunu o an görmesi gerekmeyen her iş asenkrondur:\n\n- E-posta ve bildirim.\n- PDF ve rapor üretimi.\n- Webhook gönderimi.\n- Üçüncü parti senkronizasyonu — CRM, analitik, arama indeksi.\n- Görsel işleme, dışa aktarma.\n\nBunlar için doğru yanıt `202 Accepted`'dir: \"isteğini aldım, işliyorum.\" Kullanıcı beklemez, sistem işi kendi temposunda yapar.\n\n## Çizgiyi yanıt süresi keskinleştirir\n\nHer endpoint için bir p95 bütçeniz olmalı — diyelim 300 ms. Bir adım bu bütçeyi tek başına yiyorsa ve kullanıcı o adımın sonucunu görmek zorunda değilse, o adım kuyruğa gider.\n\nSenkron zincir, en yavaş halkası kadar hızlıdır. PDF üretimi 4 saniye sürüyorsa, kullanıcı oluşturma da 4 saniye sürer — PDF'in yanıtla hiçbir ilgisi olmasa bile.\n\n## Hata toleransı: asıl ayrım\n\nAsıl mesele hız değil, hatadır. Senkron ve asenkron, başarısızlığı bambaşka karşılar:\n\n- **Senkron hata** = kullanıcı bir hata ekranı görür. İş kaybolur.\n- **Asenkron hata** = job retry edilir, gerekirse DLQ'ya düşer, [kuyruk](/tr/notes/laravel-queue-production-yavaslama) sonra işler.\n\nBuradan tek bir kural çıkar: **kararsız bir üçüncü parti asla senkron yolda durmamalı.** O CRM çağrısı istek içindeyse, CRM her çöktüğünde sizin kayıt formunuz da çöker. Kuyruğa alınmış bir çağrı ise, CRM döndüğünde retry kendiliğinden tamamlanır. Üçüncü partinin uptime'ı sizin uptime'ınız olmaktan çıkar.\n\n## 202 ve durum sorgusu kalıbı\n\nİş kuyruğa gidince kullanıcıya bir takip yolu bırakın:\n\n```\nPOST /exports          → 202 Accepted  { \"id\": \"exp_1a2b\", \"status\": \"processing\" }\nGET  /exports/exp_1a2b → 200 OK        { \"status\": \"done\", \"url\": \"...\" }\n```\n\nKullanıcı sonucu beklemiyordur ama bazen merak eder. Bir durum endpoint'i ya da hazır olunca gönderilen bir bildirim, bu boşluğu kapatır.\n\n## Sınır nerede kalkar?\n\nBazı işler ikisinin arasındadır: kullanıcı sonucu görmek ister ama iş de yavaştır. Burada üçüncü bir yol var — işi senkron başlatın, ilk anlamlı sonucu hemen döndürün, gerisini kuyruğa devredin. Sipariş kaydını anında oluşturun (senkron), faturayı arkada üretin (asenkron).\n\nÇizgiyi her iş için ayrı çizin; \"her şey kuyruğa\" da \"her şey senkron\" kadar yanlıştır.\n\n---\n\nSenkron mu asenkron mu sorusu bir performans sorusu değil, bir \"kim bekliyor\" sorusudur. Kullanıcının beklemediği hiçbir iş, onun isteğini bekletmemeli.\n\nİsteği yalnızca, cevabını gerçekten istediği işle meşgul edin.",
      "date_published": "2026-06-27T00:00:00.000Z",
      "tags": [
        "architecture",
        "queue",
        "async",
        "api",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/systems/transactional-outbox-dual-write-ve-idempotent-tuketim/",
      "url": "https://sade.dev/tr/systems/transactional-outbox-dual-write-ve-idempotent-tuketim/",
      "title": "Transactional Outbox: Dual-Write, İdempotent Tüketim",
      "summary": "Outbox dual-write'ı tek write'a indirir ama bedava garanti vermez, at-least-once verir. Relay polling mi CDC mi, SKIP LOCKED ile çoklu relay, ordering, processed_messages budama, exactly-once yanılsaması ve alan-seviyesi şifreleme.",
      "content_html": "<p>Belirti küçüktü: günde bir-iki fatura veritabanında duruyor ama consumer onları hiç görmüyordu. Kullanıcı “kestim” diyor, karşı tarafta hiçbir iz yok. Hep gece, hep bir deploy ya da network dalgalanması anında. Tabloyu netleştirince sebep ortaya çıktı — veritabanına <code>INSERT</code> ile kuyruğa <code>publish</code> iki ayrı sistemdi ve aralarında hiçbir garanti yoktu.</p>\n<p>Bu yazı o boşluğu kapatan <strong>transactional outbox</strong> kalıbının, kapatınca açılan <strong>at-least-once</strong> tekrarının ve son olarak event’in içinden geçen kişisel veriyi <strong>alan seviyesinde</strong> şifrelemenin sistem tarafı. Karar zincirinin günlük tarafını <a href=\"https://muhammetsafak.com.tr/blog/outbox-idempotent-tuketim-ve-alan-sifrelemesi\">muhammetsafak.com.tr’de ayrı yazdım</a>; burada kalıbın production’da dayanıklı olması için verilmesi gereken kararları topluyorum.</p>\n<h2 id=\"dual-write-bir-transactionı-iki-sisteme-yayamazsınız\">Dual-write: bir transaction’ı iki sisteme yayamazsınız</h2>\n<p>Sorunun özü tek cümle: yerel veritabanı transaction’ınız RabbitMQ’yu kapsamaz. Klasik kod <em>çoğu zaman</em> çalıştığı için tehlikeli:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">DB</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">transaction</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#999999;--shiki-dark:#666666\"> ()</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> use</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">save</span><span style=\"color:#999999;--shiki-dark:#666666\">();</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">                 // 1) MySQL&#39;e yaz</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">this</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">publishToRabbit</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\"> // 2) kuyruğa publish</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">});</span></span></code></pre>\n<p>İki ayrı arıza modu var:</p>\n<ul>\n<li><code>save()</code> başarılı, <code>publish</code> network hatasıyla düşer → DB’de fatura var, event yok. <strong>Kayıp event.</strong></li>\n<li><code>publish</code> başarılı, ardından transaction başka sebeple <code>rollback</code> olur → event gitti, DB’de karşılığı yok. <strong>Hayalet event.</strong></li>\n</ul>\n<p><code>DB::transaction</code> kurtarmaz, çünkü commit/rollback yalnızca MySQL tarafını sarar; broker o transaction’ın parçası değildir. İki sistemi tek atomik adımda tutmanın pratik yolu, yazmayı <strong>tek bir sisteme</strong> indirmek.</p>\n<h2 id=\"outbox-önce-tek-sisteme-yaz-sonra-ayrı-bir-süreç-yayımlasın\">Outbox: önce tek sisteme yaz, sonra ayrı bir süreç yayımlasın</h2>\n<p>Kalıbın fikri sade: mesajı kuyruğa doğrudan basmak yerine, <strong>aynı transaction içinde</strong> bir <code>outbox</code> tablosuna satır olarak yaz. Fatura ile event aynı commit’te ya birlikte var olur ya da birlikte yok olur — dual-write tek write’a iner.</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">CREATE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> TABLE</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> outbox</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> (</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    id            </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">BINARY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">16</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">)   </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,        </span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- event id = idempotency anahtarı</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    aggregate_id  </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">BIGINT</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">       NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,        </span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- ordering ve partition anahtarı</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    topic         </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">VARCHAR</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">120</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">) </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    payload       </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">JSON</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">         NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    status</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">        ENUM(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pending</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">published</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">) </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NOT NULL</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> DEFAULT</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pending</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    created_at    </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">DATETIME</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">6</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">)  </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NOT NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">    published_at  </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">DATETIME</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">6</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">)  </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">NULL</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    PRIMARY KEY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> (id),</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    KEY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> idx_dispatch (</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">status</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">, created_at)        </span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- relay taraması bu indeksten gider</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">);</span></span></code></pre>\n<p>Yazma artık iş kodunun derdi değil:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">DB</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">transaction</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#999999;--shiki-dark:#666666\"> ()</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> use</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">save</span><span style=\"color:#999999;--shiki-dark:#666666\">();</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">    Outbox</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">write</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">invoice.issued</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">id</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">this</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">payload</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">invoice</span><span style=\"color:#999999;--shiki-dark:#666666\">));</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">});</span></span></code></pre>\n<p>Kuyruğa basmayı ayrı bir <strong>relay</strong> yapar: <code>pending</code> satırları okur, RabbitMQ’ya <code>publish</code> eder, başarınca <code>published</code> damgalar. Burada kritik gerçek şu: relay “publish ettim ama <code>published</code> damgasını vuramadan çöktüm” durumuna düşebilir. Yani outbox dual-write’ı çözerken size bedava bir garanti <strong>vermez</strong> — <strong>at-least-once</strong> verir. Mesaj kaybolmaz ama tekrar edebilir. Geri kalan her şey bu gerçeğin etrafına dizilir.</p>\n<h2 id=\"relayi-nasıl-beslersiniz-polling-mi-cdc-mi\">Relay’i nasıl beslersiniz: polling mi, CDC mi?</h2>\n<p>Relay <code>pending</code> satırları iki yoldan görebilir.</p>\n<p><strong>Polling</strong> — relay tabloyu periyodik tarar:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">SELECT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> id, topic, payload</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">FROM</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> outbox</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">WHERE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> status</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pending</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">ORDER BY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> created_at</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">LIMIT</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 100</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<p>Basit, her veritabanında çalışır, operasyonel yükü düşük. Bedeli iki şey: tarama aralığı kadar <strong>gecikme</strong> (1 sn’lik poll = ~1 sn’lik kuyruk gecikmesi) ve boşta dönen sorgu. <code>idx_dispatch</code> indeksi olmadan bu tarama tablo büyüdükçe pahalılaşır; indeksle bile poll sıklığı ile DB yükü arasında pazarlık yaparsınız. Küçük-orta hacimde polling <strong>doğru</strong> cevap — gecikmeyi kabul edilebilir tutacak kadar sık, DB’yi yormayacak kadar seyrek (pratikte 200 ms–1 sn) ayarlayın.</p>\n<p><strong>CDC</strong> (change data capture) — relay tabloyu değil, veritabanının <strong>WAL/binlog</strong>’unu dinler (Debezium tipik araç). <code>outbox</code>’a düşen her <code>INSERT</code> neredeyse anında bir event’e döner; polling gecikmesi ve boşa tarama ortadan kalkar. Bedeli operasyonel: binlog’a erişim, bir connector süreci, ek bir hareketli parça. CDC’yi gecikme gerçekten önemliyse (saniye altı) ya da hacim polling’i zorluyorsa açarım; aksi halde polling’in sadeliğini bir avantaj sayarım.</p>\n<blockquote>\n<p>Kural: gecikme bütçeniz ile operasyonel bütçeniz çatışır. CDC gecikmeyi satın alır, karşılığında bir altyapı parçası ödetir. Önce polling’le başlayın; CDC’ye ölçülmüş bir sebep çıkınca geçin.</p>\n</blockquote>\n<h2 id=\"çoklu-relay-skip-locked-ile-çakışmasız-dağıtım\">Çoklu relay: <code>SKIP LOCKED</code> ile çakışmasız dağıtım</h2>\n<p>Tek relay bir bottleneck ve tek arıza noktasıdır. Birden çok relay aynı tabloyu tarayınca yeni bir risk doğar: ikisi aynı satırı kapıp aynı mesajı iki kez yayımlar. Naif çözüm tabloyu kilitlemektir — ki bu paralelliği öldürür.</p>\n<p>Doğru araç <code>SELECT ... FOR UPDATE SKIP LOCKED</code>. Her relay bir batch satırı <strong>kilitleyerek</strong> claim eder; başka bir relay’in kilitlediği satırları sıraya girip beklemek yerine <strong>atlar</strong>:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">BEGIN</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  SELECT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> id, topic, payload</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  FROM</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> outbox</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  WHERE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> status</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">pending</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  ORDER BY</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> created_at</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  LIMIT</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 100</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  FOR</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> UPDATE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> SKIP</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> LOCKED;     </span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- başka relay&#39;in tuttuğu satırları atla, bekleme</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">  -- bu batch&#39;i publish et, sonra:</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  UPDATE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> outbox </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">SET</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> status</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">published</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">, published_at </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">=</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> NOW</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">6</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">)</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  WHERE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> id </span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">IN</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> (...);</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">COMMIT</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<p><code>SKIP LOCKED</code> olmadan relay’ler aynı satırlar için kuyruğa girer ve paralellik fiilen seri yürür. <code>SKIP LOCKED</code> ile her relay disjoint bir küme çeker, hepsi birden ilerler. PostgreSQL ve MySQL 8+ bunu destekler.</p>\n<p>Bir incelik: <code>publish</code> ile <code>UPDATE ... published</code> arasında relay çökerse satır <code>pending</code> kalır ve <strong>yeniden yayımlanır</strong>. Bu kabul edilebilir — zaten at-least-once’tayız; çözüm tüketici tarafında, aşağıda. Tehlikeli olan tersidir: önce <code>published</code> damgalayıp sonra <code>publish</code> etmek. O zaman çökme <strong>kayıp event</strong> üretir ki tam kaçtığımız şey. Yani sıra <strong>değişmez</strong>: önce <code>publish</code>, sonra damga.</p>\n<h2 id=\"ordering-neyi-garanti-edebilirsiniz-neyi-edemezsiniz\">Ordering: neyi garanti edebilirsiniz, neyi edemezsiniz</h2>\n<p>“Event’ler yazıldığı sırada gelsin” sezgisel bir beklenti ama global ordering pahalı ve çoğu zaman gereksiz. Gerçekte ihtiyaç duyduğunuz şey <strong>aggregate başına</strong> sıradır: aynı faturanın <code>created</code> event’i <code>updated</code>’dan önce gelsin; iki farklı faturanın birbirine göre sırası kimsenin umurunda değil.</p>\n<p>Bunu <code>aggregate_id</code> ile partition’layarak alırsınız: aynı <code>aggregate_id</code>’ye sahip event’ler aynı kuyruk partition’ına (ya da RabbitMQ’da consistent hash exchange ile aynı queue’ya) gider; o partition’ı <strong>tek consumer</strong> sırayla işler. Farklı aggregate’ler paralel akar.</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>outbox (created_at sırasıyla)</span></span>\n<span class=\"line\"><span>   │  hash(aggregate_id) % N</span></span>\n<span class=\"line\"><span>   ├── partition 0 ──► consumer-0   (aggregate A,D olayları — sıralı)</span></span>\n<span class=\"line\"><span>   ├── partition 1 ──► consumer-1   (aggregate B olayları   — sıralı)</span></span>\n<span class=\"line\"><span>   └── partition 2 ──► consumer-2   (aggregate C olayları   — sıralı)</span></span></code></pre>\n<p>İki şey ordering’i yine de bozar ve tasarımın bunlara hazır olması gerekir: çoklu relay <code>SKIP LOCKED</code> ile satırları farklı sırada yayımlayabilir ve at-least-once tekrarlar araya girebilir. Bu yüzden global sıraya bel bağlamak yerine, tüketiciyi <strong>sırasız ve tekrarlı</strong> event’e dayanıklı yazmak gerekir. Pratik kalkan: event’e monotonik bir <code>version</code>/<code>sequence</code> koyup tüketicide eskiyi (daha düşük versiyonu) düşürmek. Sıkı global ordering gerekiyorsa tek partition + tek consumer’a inersiniz — ki bu da throughput’u o tek consumer’a bağlar; çoğu sistem bu bedeli ödemek istemez.</p>\n<h2 id=\"at-least-once-gelince-tüketici-idempotent-olmalı\">At-least-once gelince: tüketici idempotent olmalı</h2>\n<p>Relay tekrar yayımlayabildiği için her event’in en az bir, bazen birden çok kez geleceğini varsaymak gerekir. Çözüm event’i tekil yapmaya çalışmak değil — <strong>tüketiciyi</strong> aynı event’i iki kez işlese de tek kez işlemiş gibi davranacak şekilde kurmak. İşlenen her event <code>id</code>’sini bir dedupe tablosuna yazıp işi <strong>aynı transaction içinde</strong> yapmak:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">DB</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">transaction</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#999999;--shiki-dark:#666666\"> ()</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> use</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">event</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">inserted</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> DB</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">table</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">processed_messages</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">insertOrIgnore</span><span style=\"color:#999999;--shiki-dark:#666666\">([</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">        &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">message_id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">   =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">event</span><span style=\"color:#999999;--shiki-dark:#666666\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">        &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">processed_at</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> now</span><span style=\"color:#999999;--shiki-dark:#666666\">(),</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    ]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    if</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">inserted</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ===</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">        return</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\"> // tekrar gelmiş; hiçbir yan etki üretme</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    $</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">this</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">applyBusinessEffect</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">event</span><span style=\"color:#999999;--shiki-dark:#666666\">);</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\"> // asıl iş — yalnızca bir kez</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">});</span></span></code></pre>\n<p><code>processed_messages.message_id</code> üstündeki <strong>unique</strong> kısıt işin kalbi: ikinci kez gelen aynı <code>id</code>, <code>insertOrIgnore</code> ile sessizce elenir. İş etkisini aynı transaction’a almak şart — yoksa “işledim ama damgalamadan çöktüm” boşluğu açılır. Dedupe anahtarının nereden geldiği ve çökmeden sağ çıkan dedupe tasarımı başlı başına bir konu; oraya <a href=\"/tr/notes/idempotency-ayni-mesaj-iki-kez/\">ayrı bir notta</a> girdim.</p>\n<p>Yan etki bir dış sistemse (e-fatura entegratörü, ödeme API’si) dedupe tek başına yetmez — çünkü yan etki transaction’ın dışındadır. Orada anahtarı <strong>dış servise taşımak</strong> gerekir: aynı <code>id</code>’yi entegratöre idempotency-key olarak geçmek, ya da göndermeden önce “bu zaten var mı?” diye sormak. Bu sınırı, numarayı dış çağrıdan önce mühürleyip recovery’yi bir lookup’a indiren <a href=\"/tr/systems/ardisik-numara-uretiminde-race-condition-ve-gap/\">JIT rezervasyon yazısında</a> ayrıntılı işledim.</p>\n<h2 id=\"processed_messages-sonsuza-kadar-büyüyemez\"><code>processed_messages</code> sonsuza kadar büyüyemez</h2>\n<p>Dedupe tablosu her event için bir satır biriktirir; budanmazsa kendisi bir performans sorununa döner. Anahtar gözlem: bir event <code>id</code>’sini sonsuza kadar tutmanız gerekmez — yalnızca <strong>bir tekrarın gelebileceği pencere</strong> kadar tutmanız yeter. Relay retry’ları ve broker redelivery’leri saatler mertebesindedir, günler değil.</p>\n<p>Pratikte sabit bir <strong>retention penceresi</strong> belirleyip (örneğin 7 gün — en uzun olası redelivery’nin rahatça üstünde) eskisini budarım:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">DELETE</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> FROM</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> processed_messages</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">WHERE</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> processed_at </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">&lt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> NOW</span><span style=\"color:#999999;--shiki-dark:#666666\">()</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> -</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> INTERVAL </span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">7</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> DAY</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">LIMIT</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 10000</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;   </span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">-- tek seferde küçük batch&#39;ler, replica lag&#39;i ve kilidi kontrol altında</span></span></code></pre>\n<p><code>message_id</code> UUID ise tablo ve indeks büyür; budama bunu sınırlı tutar. Pencereyi seçerken kuralı tersinden kurun: <em>en uzun gecikmeli redelivery ne kadar sonra gelebilir?</em> Cevabın güvenli üstünü retention yapın. Pencereyi çok kısa tutarsanız geç gelen bir tekrar dedupe’a takılmaz ve yan etki ikinci kez çalışır.</p>\n<h2 id=\"exactly-once-neden-çoğu-zaman-bir-yanılsama\">Exactly-once neden çoğu zaman bir yanılsama</h2>\n<p>Dağıtık sistemde “tam bir kez teslim” cazip ama uçtan uca garanti edilemez. Sebep aynı dual-write’ın broker sınırındaki hali: tüketici işi yaptı ama <code>ack</code>’i broker’a ulaştıramadan çöktü mü, broker mesajı yeniden teslim eder — çünkü <code>ack</code> görmemiştir. “İşledim” ile “işlediğimi bildirdim” iki ayrı adımdır ve aralarına çökme girebilir. Bu, iki general probleminin pratikteki yüzü.</p>\n<p>Bu yüzden hedef exactly-once <strong>delivery</strong> değil, exactly-once <strong>effect</strong>. Bunu da iki ucuz garantiyi birleştirerek alırsınız:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>at-least-once delivery  +  idempotent consumer  =  effectively-once</span></span></code></pre>\n<p>Yani teslimi tam bir kez yapmaya çalışmayı bırakıp <strong>tekrarı zararsız</strong> kılarsınız. “Exactly-once” pazarlayan sistemlerin altında çoğu zaman tam olarak bu vardır: at-least-once + bir dedupe katmanı. Garantiyi teslimde değil, etkide arayın.</p>\n<h2 id=\"eventin-içinden-geçen-kişisel-veri-alan-seviyesi-şifreleme\">Event’in içinden geçen kişisel veri: alan seviyesi şifreleme</h2>\n<p>Outbox oturunca yeni bir yüzey çıkar: event’lerin payload’ında müşteri adı, vergi numarası, adres gibi kişisel veri var ve bu payload artık <strong>kalıcı</strong> bir tabloda (<code>outbox</code>) duruyor, üstelik broker’dan geçiyor. Veriyi yerinde bırakırsam, GDPR/KVKK kapsamındaki alanları şifresiz biçimde birden çok yere kopyalamış olurum.</p>\n<p>Bütün payload’ı şifrelemek istemedim — <code>topic</code>, <code>aggregate_id</code>, <code>invoice_id</code> gibi alanları relay yönlendirmesi ve gözlemlenebilirlik için açık görmem gerekiyordu. İhtiyaç <strong>alan seviyesi</strong> şifreleme: yalnızca hassas alanlar şifreli, gerisi açık. Hassas alanları şemada <code>x-gdpr-sensitive</code> ile işaretleyip serileştirmede yalnızca onları şifreledim:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">$</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">schema</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">invoice_id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">x-gdpr-sensitive</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> false</span><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">customer_name</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">x-gdpr-sensitive</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> true</span><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">tax_id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">        =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">x-gdpr-sensitive</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> true</span><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    &#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">total</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">         =&gt;</span><span style=\"color:#999999;--shiki-dark:#666666\"> [</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">x-gdpr-sensitive</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> =&gt;</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> false</span><span style=\"color:#999999;--shiki-dark:#666666\">],</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">$</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">payload</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> collect</span><span style=\"color:#999999;--shiki-dark:#666666\">($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">raw</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">map</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">fn</span><span style=\"color:#999999;--shiki-dark:#666666\"> ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">field</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =&gt;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    ($</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">schema</span><span style=\"color:#999999;--shiki-dark:#666666\">[$</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">field</span><span style=\"color:#999999;--shiki-dark:#666666\">][</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">x-gdpr-sensitive</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">]</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ??</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> false</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">        ?</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> Crypt</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">encryptString</span><span style=\"color:#999999;--shiki-dark:#666666\">((</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">string</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#5F6E5F;--shiki-dark:#8A9A8A\">  // yalnızca hassas alan şifreli</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">        :</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">all</span><span style=\"color:#999999;--shiki-dark:#666666\">();</span></span></code></pre>\n<p>Şemanın hassas alanı beyan etmesi tesadüf değil — bunu kuyruğun kenarında şema doğrulamasıyla birleştirince, “hangi alan kişisel veri ve doğru tipte mi?” sorularının ikisi de <a href=\"/tr/notes/semayi-kuyrugun-kenarinda-dogrulamak/\">kenarda</a> cevaplanır. Asıl mühendislik kararı şifrelemenin kendisinde değil, çevresindeki üç soruda:</p>\n<p><strong>Anahtar yönetimi ve rotasyon.</strong> Laravel <code>Crypt</code>, <code>APP_KEY</code> ile AES-256 kullanır. Tek bir anahtara bağlanırsanız rotasyon kabusa döner: eski anahtarla yazılmış outbox satırlarını yeni anahtar açamaz. Çözüm, şifreli değerin yanına <strong>anahtar versiyonu</strong> (key id) yazmak ve birden çok anahtarı aynı anda tanıyan bir keyring tutmak — yeni yazımlar güncel anahtarı kullanır, eski değerler kendi versiyonuyla açılır. Laravel’de <code>config/app.php</code>’deki <code>key</code> + <code>previous_keys</code> tam bunun içindir; decrypt sırasıyla eski anahtarları da dener. Rotasyonu bu yüzden veri taşımadan yapabilirsiniz.</p>\n<p><strong>Şifreli alanda arama.</strong> <code>Crypt::encryptString</code> her çağrıda farklı ciphertext üretir (rastgele IV), ki bu güvenlik için doğru ama <code>WHERE tax_id = ?</code> ile aramayı imkânsız kılar. Eşitlik araması gerekiyorsa <strong>blind index</strong> eklersiniz: aranabilir alanın deterministik bir HMAC’ini ayrı bir kolonda tutup onu sorgularsınız. Şifreli kolon gizliliği, blind index aranabilirliği taşır; ikisi ayrı kolondur çünkü iki ayrı işi vardır.</p>\n<p><strong>Anahtarı kaybetmek veriyi kaybetmektir.</strong> Alan şifrelemesi <code>APP_KEY</code>’i bir <strong>availability</strong> bağımlılığına çevirir: anahtar giderse şifreli alanlar kalıcı çöptür. Anahtar yedeği ve erişim kontrolü, şifrelemenin kendisi kadar tasarımın parçasıdır.</p>\n<h2 id=\"ne-zaman-bu-kalıbı-kurmazsınız\">Ne zaman bu kalıbı kurmazsınız?</h2>\n<p>Outbox + idempotent tüketim + alan şifrelemesi ucuz değil: bir tablo, bir relay süreci (ya da CDC connector), bir dedupe tablosu ve onun budama işi, bir de anahtar yönetimi getirir. Karşılığında ne aldığınızı bilmiyorsanız ödemeyin. Şu durumlarda kurmuyorum:</p>\n<ul>\n<li><strong>Event kaybı tolere edilebiliyorsa.</strong> Yayımladığınız şey best-effort bir bildirimse (cache invalidation, “yeni içerik var” sinyali) ve bir-iki kayıp önemsizse, doğrudan <code>publish</code> yeterli. Outbox’ı kayıp önemli olduğu için kurarsınız.</li>\n<li><strong>Yazma ile yayım aynı sistemdeyse.</strong> Hedefiniz de aynı veritabanıysa (ayrı bir broker yoksa), dual-write zaten yok; outbox’a gerek kalmaz.</li>\n<li><strong>Hacim tek consumer’a sığıyorsa ve ordering kritikse.</strong> Düşük hacimde tek tüketicili bir kuyruk hem sırayı hem tekilliği basitçe verir; çoklu relay ve partition makinesini kurmak olmayan bir problemi çözmektir.</li>\n</ul>\n<p>Kalıba ihtiyaç üç koşul <strong>birlikte</strong> doğunca başlar: event kaybı kabul edilemez, üretim/tüketim paralel ve veri ayrı bir sisteme (broker) gidiyor. Üçü birden yoksa daha sade bir çözüm vardır.</p>\n<hr/>\n<p>Üç parça tek bir disipline iner: <strong>veriyi tek bir sisteme yaz, tekrarı tüketicide bastır, hassas alanı yola çıkmadan mühürle.</strong> Outbox kuyruğa olan güveni, idempotent tüketim teslime olan güveni, alan şifrelemesi de payload’a olan güveni gereksiz kılar — her biri bir garantiyi koddan dışarı, tasarımın içine taşır.</p>",
      "content_text": "Belirti küçüktü: günde bir-iki fatura veritabanında duruyor ama consumer onları hiç görmüyordu. Kullanıcı \"kestim\" diyor, karşı tarafta hiçbir iz yok. Hep gece, hep bir deploy ya da network dalgalanması anında. Tabloyu netleştirince sebep ortaya çıktı — veritabanına `INSERT` ile kuyruğa `publish` iki ayrı sistemdi ve aralarında hiçbir garanti yoktu.\n\nBu yazı o boşluğu kapatan **transactional outbox** kalıbının, kapatınca açılan **at-least-once** tekrarının ve son olarak event'in içinden geçen kişisel veriyi **alan seviyesinde** şifrelemenin sistem tarafı. Karar zincirinin günlük tarafını [muhammetsafak.com.tr'de ayrı yazdım](https://muhammetsafak.com.tr/blog/outbox-idempotent-tuketim-ve-alan-sifrelemesi); burada kalıbın production'da dayanıklı olması için verilmesi gereken kararları topluyorum.\n\n## Dual-write: bir transaction'ı iki sisteme yayamazsınız\n\nSorunun özü tek cümle: yerel veritabanı transaction'ınız RabbitMQ'yu kapsamaz. Klasik kod *çoğu zaman* çalıştığı için tehlikeli:\n\n```php\nDB::transaction(function () use ($invoice) {\n    $invoice->save();                 // 1) MySQL'e yaz\n    $this->publishToRabbit($invoice); // 2) kuyruğa publish\n});\n```\n\nİki ayrı arıza modu var:\n\n- `save()` başarılı, `publish` network hatasıyla düşer → DB'de fatura var, event yok. **Kayıp event.**\n- `publish` başarılı, ardından transaction başka sebeple `rollback` olur → event gitti, DB'de karşılığı yok. **Hayalet event.**\n\n`DB::transaction` kurtarmaz, çünkü commit/rollback yalnızca MySQL tarafını sarar; broker o transaction'ın parçası değildir. İki sistemi tek atomik adımda tutmanın pratik yolu, yazmayı **tek bir sisteme** indirmek.\n\n## Outbox: önce tek sisteme yaz, sonra ayrı bir süreç yayımlasın\n\nKalıbın fikri sade: mesajı kuyruğa doğrudan basmak yerine, **aynı transaction içinde** bir `outbox` tablosuna satır olarak yaz. Fatura ile event aynı commit'te ya birlikte var olur ya da birlikte yok olur — dual-write tek write'a iner.\n\n```sql\nCREATE TABLE outbox (\n    id            BINARY(16)   NOT NULL,        -- event id = idempotency anahtarı\n    aggregate_id  BIGINT       NOT NULL,        -- ordering ve partition anahtarı\n    topic         VARCHAR(120) NOT NULL,\n    payload       JSON         NOT NULL,\n    status        ENUM('pending','published') NOT NULL DEFAULT 'pending',\n    created_at    DATETIME(6)  NOT NULL,\n    published_at  DATETIME(6)  NULL,\n    PRIMARY KEY (id),\n    KEY idx_dispatch (status, created_at)        -- relay taraması bu indeksten gider\n);\n```\n\nYazma artık iş kodunun derdi değil:\n\n```php\nDB::transaction(function () use ($invoice) {\n    $invoice->save();\n    Outbox::write('invoice.issued', $invoice->id, $this->payload($invoice));\n});\n```\n\nKuyruğa basmayı ayrı bir **relay** yapar: `pending` satırları okur, RabbitMQ'ya `publish` eder, başarınca `published` damgalar. Burada kritik gerçek şu: relay \"publish ettim ama `published` damgasını vuramadan çöktüm\" durumuna düşebilir. Yani outbox dual-write'ı çözerken size bedava bir garanti **vermez** — **at-least-once** verir. Mesaj kaybolmaz ama tekrar edebilir. Geri kalan her şey bu gerçeğin etrafına dizilir.\n\n## Relay'i nasıl beslersiniz: polling mi, CDC mi?\n\nRelay `pending` satırları iki yoldan görebilir.\n\n**Polling** — relay tabloyu periyodik tarar:\n\n```sql\nSELECT id, topic, payload\nFROM outbox\nWHERE status = 'pending'\nORDER BY created_at\nLIMIT 100;\n```\n\nBasit, her veritabanında çalışır, operasyonel yükü düşük. Bedeli iki şey: tarama aralığı kadar **gecikme** (1 sn'lik poll = ~1 sn'lik kuyruk gecikmesi) ve boşta dönen sorgu. `idx_dispatch` indeksi olmadan bu tarama tablo büyüdükçe pahalılaşır; indeksle bile poll sıklığı ile DB yükü arasında pazarlık yaparsınız. Küçük-orta hacimde polling **doğru** cevap — gecikmeyi kabul edilebilir tutacak kadar sık, DB'yi yormayacak kadar seyrek (pratikte 200 ms–1 sn) ayarlayın.\n\n**CDC** (change data capture) — relay tabloyu değil, veritabanının **WAL/binlog**'unu dinler (Debezium tipik araç). `outbox`'a düşen her `INSERT` neredeyse anında bir event'e döner; polling gecikmesi ve boşa tarama ortadan kalkar. Bedeli operasyonel: binlog'a erişim, bir connector süreci, ek bir hareketli parça. CDC'yi gecikme gerçekten önemliyse (saniye altı) ya da hacim polling'i zorluyorsa açarım; aksi halde polling'in sadeliğini bir avantaj sayarım.\n\n> Kural: gecikme bütçeniz ile operasyonel bütçeniz çatışır. CDC gecikmeyi satın alır, karşılığında bir altyapı parçası ödetir. Önce polling'le başlayın; CDC'ye ölçülmüş bir sebep çıkınca geçin.\n\n## Çoklu relay: `SKIP LOCKED` ile çakışmasız dağıtım\n\nTek relay bir bottleneck ve tek arıza noktasıdır. Birden çok relay aynı tabloyu tarayınca yeni bir risk doğar: ikisi aynı satırı kapıp aynı mesajı iki kez yayımlar. Naif çözüm tabloyu kilitlemektir — ki bu paralelliği öldürür.\n\nDoğru araç `SELECT ... FOR UPDATE SKIP LOCKED`. Her relay bir batch satırı **kilitleyerek** claim eder; başka bir relay'in kilitlediği satırları sıraya girip beklemek yerine **atlar**:\n\n```sql\nBEGIN;\n  SELECT id, topic, payload\n  FROM outbox\n  WHERE status = 'pending'\n  ORDER BY created_at\n  LIMIT 100\n  FOR UPDATE SKIP LOCKED;     -- başka relay'in tuttuğu satırları atla, bekleme\n\n  -- bu batch'i publish et, sonra:\n  UPDATE outbox SET status = 'published', published_at = NOW(6)\n  WHERE id IN (...);\nCOMMIT;\n```\n\n`SKIP LOCKED` olmadan relay'ler aynı satırlar için kuyruğa girer ve paralellik fiilen seri yürür. `SKIP LOCKED` ile her relay disjoint bir küme çeker, hepsi birden ilerler. PostgreSQL ve MySQL 8+ bunu destekler.\n\nBir incelik: `publish` ile `UPDATE ... published` arasında relay çökerse satır `pending` kalır ve **yeniden yayımlanır**. Bu kabul edilebilir — zaten at-least-once'tayız; çözüm tüketici tarafında, aşağıda. Tehlikeli olan tersidir: önce `published` damgalayıp sonra `publish` etmek. O zaman çökme **kayıp event** üretir ki tam kaçtığımız şey. Yani sıra **değişmez**: önce `publish`, sonra damga.\n\n## Ordering: neyi garanti edebilirsiniz, neyi edemezsiniz\n\n\"Event'ler yazıldığı sırada gelsin\" sezgisel bir beklenti ama global ordering pahalı ve çoğu zaman gereksiz. Gerçekte ihtiyaç duyduğunuz şey **aggregate başına** sıradır: aynı faturanın `created` event'i `updated`'dan önce gelsin; iki farklı faturanın birbirine göre sırası kimsenin umurunda değil.\n\nBunu `aggregate_id` ile partition'layarak alırsınız: aynı `aggregate_id`'ye sahip event'ler aynı kuyruk partition'ına (ya da RabbitMQ'da consistent hash exchange ile aynı queue'ya) gider; o partition'ı **tek consumer** sırayla işler. Farklı aggregate'ler paralel akar.\n\n```\noutbox (created_at sırasıyla)\n   │  hash(aggregate_id) % N\n   ├── partition 0 ──► consumer-0   (aggregate A,D olayları — sıralı)\n   ├── partition 1 ──► consumer-1   (aggregate B olayları   — sıralı)\n   └── partition 2 ──► consumer-2   (aggregate C olayları   — sıralı)\n```\n\nİki şey ordering'i yine de bozar ve tasarımın bunlara hazır olması gerekir: çoklu relay `SKIP LOCKED` ile satırları farklı sırada yayımlayabilir ve at-least-once tekrarlar araya girebilir. Bu yüzden global sıraya bel bağlamak yerine, tüketiciyi **sırasız ve tekrarlı** event'e dayanıklı yazmak gerekir. Pratik kalkan: event'e monotonik bir `version`/`sequence` koyup tüketicide eskiyi (daha düşük versiyonu) düşürmek. Sıkı global ordering gerekiyorsa tek partition + tek consumer'a inersiniz — ki bu da throughput'u o tek consumer'a bağlar; çoğu sistem bu bedeli ödemek istemez.\n\n## At-least-once gelince: tüketici idempotent olmalı\n\nRelay tekrar yayımlayabildiği için her event'in en az bir, bazen birden çok kez geleceğini varsaymak gerekir. Çözüm event'i tekil yapmaya çalışmak değil — **tüketiciyi** aynı event'i iki kez işlese de tek kez işlemiş gibi davranacak şekilde kurmak. İşlenen her event `id`'sini bir dedupe tablosuna yazıp işi **aynı transaction içinde** yapmak:\n\n```php\nDB::transaction(function () use ($event) {\n    $inserted = DB::table('processed_messages')->insertOrIgnore([\n        'message_id'   => $event['id'],\n        'processed_at' => now(),\n    ]);\n\n    if ($inserted === 0) {\n        return; // tekrar gelmiş; hiçbir yan etki üretme\n    }\n\n    $this->applyBusinessEffect($event); // asıl iş — yalnızca bir kez\n});\n```\n\n`processed_messages.message_id` üstündeki **unique** kısıt işin kalbi: ikinci kez gelen aynı `id`, `insertOrIgnore` ile sessizce elenir. İş etkisini aynı transaction'a almak şart — yoksa \"işledim ama damgalamadan çöktüm\" boşluğu açılır. Dedupe anahtarının nereden geldiği ve çökmeden sağ çıkan dedupe tasarımı başlı başına bir konu; oraya [ayrı bir notta](/tr/notes/idempotency-ayni-mesaj-iki-kez) girdim.\n\nYan etki bir dış sistemse (e-fatura entegratörü, ödeme API'si) dedupe tek başına yetmez — çünkü yan etki transaction'ın dışındadır. Orada anahtarı **dış servise taşımak** gerekir: aynı `id`'yi entegratöre idempotency-key olarak geçmek, ya da göndermeden önce \"bu zaten var mı?\" diye sormak. Bu sınırı, numarayı dış çağrıdan önce mühürleyip recovery'yi bir lookup'a indiren [JIT rezervasyon yazısında](/tr/systems/ardisik-numara-uretiminde-race-condition-ve-gap) ayrıntılı işledim.\n\n## `processed_messages` sonsuza kadar büyüyemez\n\nDedupe tablosu her event için bir satır biriktirir; budanmazsa kendisi bir performans sorununa döner. Anahtar gözlem: bir event `id`'sini sonsuza kadar tutmanız gerekmez — yalnızca **bir tekrarın gelebileceği pencere** kadar tutmanız yeter. Relay retry'ları ve broker redelivery'leri saatler mertebesindedir, günler değil.\n\nPratikte sabit bir **retention penceresi** belirleyip (örneğin 7 gün — en uzun olası redelivery'nin rahatça üstünde) eskisini budarım:\n\n```sql\nDELETE FROM processed_messages\nWHERE processed_at < NOW() - INTERVAL 7 DAY\nLIMIT 10000;   -- tek seferde küçük batch'ler, replica lag'i ve kilidi kontrol altında\n```\n\n`message_id` UUID ise tablo ve indeks büyür; budama bunu sınırlı tutar. Pencereyi seçerken kuralı tersinden kurun: *en uzun gecikmeli redelivery ne kadar sonra gelebilir?* Cevabın güvenli üstünü retention yapın. Pencereyi çok kısa tutarsanız geç gelen bir tekrar dedupe'a takılmaz ve yan etki ikinci kez çalışır.\n\n## Exactly-once neden çoğu zaman bir yanılsama\n\nDağıtık sistemde \"tam bir kez teslim\" cazip ama uçtan uca garanti edilemez. Sebep aynı dual-write'ın broker sınırındaki hali: tüketici işi yaptı ama `ack`'i broker'a ulaştıramadan çöktü mü, broker mesajı yeniden teslim eder — çünkü `ack` görmemiştir. \"İşledim\" ile \"işlediğimi bildirdim\" iki ayrı adımdır ve aralarına çökme girebilir. Bu, iki general probleminin pratikteki yüzü.\n\nBu yüzden hedef exactly-once **delivery** değil, exactly-once **effect**. Bunu da iki ucuz garantiyi birleştirerek alırsınız:\n\n```\nat-least-once delivery  +  idempotent consumer  =  effectively-once\n```\n\nYani teslimi tam bir kez yapmaya çalışmayı bırakıp **tekrarı zararsız** kılarsınız. \"Exactly-once\" pazarlayan sistemlerin altında çoğu zaman tam olarak bu vardır: at-least-once + bir dedupe katmanı. Garantiyi teslimde değil, etkide arayın.\n\n## Event'in içinden geçen kişisel veri: alan seviyesi şifreleme\n\nOutbox oturunca yeni bir yüzey çıkar: event'lerin payload'ında müşteri adı, vergi numarası, adres gibi kişisel veri var ve bu payload artık **kalıcı** bir tabloda (`outbox`) duruyor, üstelik broker'dan geçiyor. Veriyi yerinde bırakırsam, GDPR/KVKK kapsamındaki alanları şifresiz biçimde birden çok yere kopyalamış olurum.\n\nBütün payload'ı şifrelemek istemedim — `topic`, `aggregate_id`, `invoice_id` gibi alanları relay yönlendirmesi ve gözlemlenebilirlik için açık görmem gerekiyordu. İhtiyaç **alan seviyesi** şifreleme: yalnızca hassas alanlar şifreli, gerisi açık. Hassas alanları şemada `x-gdpr-sensitive` ile işaretleyip serileştirmede yalnızca onları şifreledim:\n\n```php\n$schema = [\n    'invoice_id'    => ['x-gdpr-sensitive' => false],\n    'customer_name' => ['x-gdpr-sensitive' => true],\n    'tax_id'        => ['x-gdpr-sensitive' => true],\n    'total'         => ['x-gdpr-sensitive' => false],\n];\n\n$payload = collect($raw)->map(fn ($value, $field) =>\n    ($schema[$field]['x-gdpr-sensitive'] ?? false)\n        ? Crypt::encryptString((string) $value)  // yalnızca hassas alan şifreli\n        : $value\n)->all();\n```\n\nŞemanın hassas alanı beyan etmesi tesadüf değil — bunu kuyruğun kenarında şema doğrulamasıyla birleştirince, \"hangi alan kişisel veri ve doğru tipte mi?\" sorularının ikisi de [kenarda](/tr/notes/semayi-kuyrugun-kenarinda-dogrulamak) cevaplanır. Asıl mühendislik kararı şifrelemenin kendisinde değil, çevresindeki üç soruda:\n\n**Anahtar yönetimi ve rotasyon.** Laravel `Crypt`, `APP_KEY` ile AES-256 kullanır. Tek bir anahtara bağlanırsanız rotasyon kabusa döner: eski anahtarla yazılmış outbox satırlarını yeni anahtar açamaz. Çözüm, şifreli değerin yanına **anahtar versiyonu** (key id) yazmak ve birden çok anahtarı aynı anda tanıyan bir keyring tutmak — yeni yazımlar güncel anahtarı kullanır, eski değerler kendi versiyonuyla açılır. Laravel'de `config/app.php`'deki `key` + `previous_keys` tam bunun içindir; decrypt sırasıyla eski anahtarları da dener. Rotasyonu bu yüzden veri taşımadan yapabilirsiniz.\n\n**Şifreli alanda arama.** `Crypt::encryptString` her çağrıda farklı ciphertext üretir (rastgele IV), ki bu güvenlik için doğru ama `WHERE tax_id = ?` ile aramayı imkânsız kılar. Eşitlik araması gerekiyorsa **blind index** eklersiniz: aranabilir alanın deterministik bir HMAC'ini ayrı bir kolonda tutup onu sorgularsınız. Şifreli kolon gizliliği, blind index aranabilirliği taşır; ikisi ayrı kolondur çünkü iki ayrı işi vardır.\n\n**Anahtarı kaybetmek veriyi kaybetmektir.** Alan şifrelemesi `APP_KEY`'i bir **availability** bağımlılığına çevirir: anahtar giderse şifreli alanlar kalıcı çöptür. Anahtar yedeği ve erişim kontrolü, şifrelemenin kendisi kadar tasarımın parçasıdır.\n\n## Ne zaman bu kalıbı kurmazsınız?\n\nOutbox + idempotent tüketim + alan şifrelemesi ucuz değil: bir tablo, bir relay süreci (ya da CDC connector), bir dedupe tablosu ve onun budama işi, bir de anahtar yönetimi getirir. Karşılığında ne aldığınızı bilmiyorsanız ödemeyin. Şu durumlarda kurmuyorum:\n\n- **Event kaybı tolere edilebiliyorsa.** Yayımladığınız şey best-effort bir bildirimse (cache invalidation, \"yeni içerik var\" sinyali) ve bir-iki kayıp önemsizse, doğrudan `publish` yeterli. Outbox'ı kayıp önemli olduğu için kurarsınız.\n- **Yazma ile yayım aynı sistemdeyse.** Hedefiniz de aynı veritabanıysa (ayrı bir broker yoksa), dual-write zaten yok; outbox'a gerek kalmaz.\n- **Hacim tek consumer'a sığıyorsa ve ordering kritikse.** Düşük hacimde tek tüketicili bir kuyruk hem sırayı hem tekilliği basitçe verir; çoklu relay ve partition makinesini kurmak olmayan bir problemi çözmektir.\n\nKalıba ihtiyaç üç koşul **birlikte** doğunca başlar: event kaybı kabul edilemez, üretim/tüketim paralel ve veri ayrı bir sisteme (broker) gidiyor. Üçü birden yoksa daha sade bir çözüm vardır.\n\n---\n\nÜç parça tek bir disipline iner: **veriyi tek bir sisteme yaz, tekrarı tüketicide bastır, hassas alanı yola çıkmadan mühürle.** Outbox kuyruğa olan güveni, idempotent tüketim teslime olan güveni, alan şifrelemesi de payload'a olan güveni gereksiz kılar — her biri bir garantiyi koddan dışarı, tasarımın içine taşır.",
      "date_published": "2026-06-22T00:00:00.000Z",
      "tags": [
        "architecture",
        "messaging",
        "idempotency",
        "reliability",
        "security",
        "System"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/event-driven-mimari-ne-zaman/",
      "url": "https://sade.dev/tr/notes/event-driven-mimari-ne-zaman/",
      "title": "Event-Driven Mimari: Ne Çözer, Neyi Gizler?",
      "summary": "Event'ler, üreticinin tüketicilerini tanımamasını sağlayarak gevşek bağ kazandırır; aynı hamle akışı da görünmez kılar, çünkü bir event kontratını derleyici bir method çağrısı gibi doğrulamaz. İki bedel ya peşin ödenir ya hiç: versiyonlanmış ve şemayla doğrulanan payload, ve her log'a ve her sonraki event'e taşınan bir correlation id. Tek listener'lı bir event, kılık değiştirmiş bir method çağrısıdır.",
      "content_html": "<p>“Sipariş oluşturulduğunda ne oluyor?” sorusuna bir ekipte kimsenin net cevap verememesini izledim. Kod tabanında <code>OrderCreated</code> event’ini sekiz ayrı listener dinliyordu; hangisinin hangi sırada, hangi koşulda çalıştığı tek bir yerden okunamıyordu. Akış kodda değil, kafalarda — hem de eksik — yaşıyordu.</p>\n<p>Event-driven mimari bağı gevşetir. Bu yazı, gevşeyen başka bir şey daha olduğunu hatırlatmak için: nedensellik.</p>\n<h2 id=\"event-ne-çözer\">Event ne çözer?</h2>\n<p>Bir event yayınlayan kod, onu kimin dinlediğini bilmez. <code>OrderCreated</code> yayınlanır; faturalama, bildirim ve analitik onu ayrı ayrı dinler. Sipariş kodu bu üçünden habersizdir.</p>\n<p>Kazanç gerçek: yeni bir tüketici eklemek, üreticiye dokunmadan olur. Bağımsız geliştirme, bağımsız dağıtım, fan-out. Birbirini tanımayan parçalar.</p>\n<h2 id=\"event-neyi-gizler\">Event neyi gizler?</h2>\n<p>Aynı madalyonun öbür yüzü: bağ gevşedikçe <strong>akış görünmez olur</strong>.</p>\n<p>Doğrudan bir method çağrısında akış kodun kendisidir — tanımına gidersiniz, devamını okursunuz. Event’te öyle değil. <code>OrderCreated</code> yayınlandığında ne olduğunu görmek için tüm listener’ları elle bulmanız gerekir. Compiler size yardım etmez: bir method çağrısının kontratını derleyici doğrular, bir event’in kontratını kimse doğrulamaz.</p>\n<p><a href=\"/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum/\">Modüler monolit yazısındaki</a> cümle burada da geçerli — bir network kontratı gibi, bir event kontratı da derleyicinin kör noktasındadır.</p>\n<h2 id=\"ödenmeden-seçilmemeli-iki-ön-koşul\">Ödenmeden seçilmemeli: iki ön koşul</h2>\n<p>Event-driven mimariye geçmeden önce iki bedel peşin ödenir.</p>\n<p><strong>Schema disiplini.</strong> Event bir kontrattır. <code>OrderCreated</code>’in payload’ı değiştiğinde, onu dinleyen sekiz yerin haberi olmaz — ta ki production’da kırılana kadar. Event’ler versiyonlanmalı, alanlar geriye uyumlu eklenmeli, payload bir şema ile doğrulanmalı. Bu disiplin yoksa, gevşek bağ “sessiz kırılma” demektir.</p>\n<p><strong>İzlenebilirlik.</strong> Bir isteğin sistemden geçişini görebilmek için her event bir correlation ID taşımalı:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">event</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">new</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> OrderCreated</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span></span>\n<span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">    orderId</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> $</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">order</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">id</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">    correlationId</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> request</span><span style=\"color:#999999;--shiki-dark:#666666\">()</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">-&gt;</span><span style=\"color:#59873A;--shiki-dark:#80A665\">header</span><span style=\"color:#999999;--shiki-dark:#666666\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">X-Correlation-Id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">&#39;</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ??</span><span style=\"color:#999999;--shiki-dark:#666666\"> (</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">string</span><span style=\"color:#999999;--shiki-dark:#666666\">)</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> Str</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">::</span><span style=\"color:#59873A;--shiki-dark:#80A665\">uuid</span><span style=\"color:#999999;--shiki-dark:#666666\">(),</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">));</span></span></code></pre>\n<p>Bu ID log’lara ve sonraki event’lere taşınmazsa, çok-listener’lı bir akışta “bu iş neden çalıştı?” sorusunun cevabı yoktur. Tracing, event-driven’ın isteğe bağlı bir eklentisi değil, ön koşuludur.</p>\n<h2 id=\"laravelde-in-process-event--event-driven-mimari\">Laravel’de: in-process event ≠ event-driven mimari</h2>\n<p>Laravel’in <code>event()</code>/listener mekanizması, senkron çalıştığında aslında düzenlenmiş bir method çağrısıdır — aynı process, aynı transaction, aynı stack trace. Bu güvenli ve izlenebilir; bunu rahatça kullanın.</p>\n<p>Event-driven mimarinin getirdiği zorluklar, event bir <strong>sınırı</strong> geçtiğinde başlar: bir queue’ya, bir message broker’a, başka bir process’e. Listener’a <code>ShouldQueue</code> ekleyip event’i RabbitMQ’ya gönderdiğiniz an, schema ve tracing borcu devreye girer. İkisini birbirine karıştırmayın: in-process listener kullanmak sizi “event-driven mimariye” geçirmez.</p>\n<h2 id=\"ne-zaman-event-drivena-geçilir\">Ne zaman event-driven’a geçilir?</h2>\n<p>Event’i gerçekten bir sınırın ötesine taşımak şu durumlarda gerekçeli:</p>\n<ul>\n<li><strong>Gerçek bir fan-out var.</strong> Bir olayı, birbirini tanımaması gereken çok sayıda bağımsız tüketici dinliyor.</li>\n<li><strong>Tüketici asenkron olmalı.</strong> İş, üreticinin yanıtını bekletmemeli; “kabul edildi” yeterli.</li>\n<li><strong>Tüketiciler bağımsız ölçeklenmeli ya da dağıtılmalı.</strong> Bu da bizi <a href=\"/tr/journal/mikroservise-ne-zaman-gecerim/\">mikroservis kararına</a> götürür — aynı ölçülmüş-sinyal eşiği.</li>\n</ul>\n<p>Bunlar yoksa, doğrudan bir service çağrısı hem daha okunur hem daha güvenlidir. Tek bir listener’ı olan bir event, sadece kılık değiştirmiş bir method çağrısıdır — üstelik izi sürülmesi daha zor olanı.</p>\n<hr/>\n<p>Event-driven mimari bağı gevşetir; bunu yaparken nedenselliği de gevşetir. Birincisini istiyorsanız, ikincisinin faturasını — schema ve tracing — peşin ödeyin.</p>\n<p>Gevşek bağ bedava değildir; görünmez akışla ödenir.</p>",
      "content_text": "\"Sipariş oluşturulduğunda ne oluyor?\" sorusuna bir ekipte kimsenin net cevap verememesini izledim. Kod tabanında `OrderCreated` event'ini sekiz ayrı listener dinliyordu; hangisinin hangi sırada, hangi koşulda çalıştığı tek bir yerden okunamıyordu. Akış kodda değil, kafalarda — hem de eksik — yaşıyordu.\n\nEvent-driven mimari bağı gevşetir. Bu yazı, gevşeyen başka bir şey daha olduğunu hatırlatmak için: nedensellik.\n\n## Event ne çözer?\n\nBir event yayınlayan kod, onu kimin dinlediğini bilmez. `OrderCreated` yayınlanır; faturalama, bildirim ve analitik onu ayrı ayrı dinler. Sipariş kodu bu üçünden habersizdir.\n\nKazanç gerçek: yeni bir tüketici eklemek, üreticiye dokunmadan olur. Bağımsız geliştirme, bağımsız dağıtım, fan-out. Birbirini tanımayan parçalar.\n\n## Event neyi gizler?\n\nAynı madalyonun öbür yüzü: bağ gevşedikçe **akış görünmez olur**.\n\nDoğrudan bir method çağrısında akış kodun kendisidir — tanımına gidersiniz, devamını okursunuz. Event'te öyle değil. `OrderCreated` yayınlandığında ne olduğunu görmek için tüm listener'ları elle bulmanız gerekir. Compiler size yardım etmez: bir method çağrısının kontratını derleyici doğrular, bir event'in kontratını kimse doğrulamaz.\n\n[Modüler monolit yazısındaki](/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum) cümle burada da geçerli — bir network kontratı gibi, bir event kontratı da derleyicinin kör noktasındadır.\n\n## Ödenmeden seçilmemeli: iki ön koşul\n\nEvent-driven mimariye geçmeden önce iki bedel peşin ödenir.\n\n**Schema disiplini.** Event bir kontrattır. `OrderCreated`'in payload'ı değiştiğinde, onu dinleyen sekiz yerin haberi olmaz — ta ki production'da kırılana kadar. Event'ler versiyonlanmalı, alanlar geriye uyumlu eklenmeli, payload bir şema ile doğrulanmalı. Bu disiplin yoksa, gevşek bağ \"sessiz kırılma\" demektir.\n\n**İzlenebilirlik.** Bir isteğin sistemden geçişini görebilmek için her event bir correlation ID taşımalı:\n\n```php\nevent(new OrderCreated(\n    orderId: $order->id,\n    correlationId: request()->header('X-Correlation-Id') ?? (string) Str::uuid(),\n));\n```\n\nBu ID log'lara ve sonraki event'lere taşınmazsa, çok-listener'lı bir akışta \"bu iş neden çalıştı?\" sorusunun cevabı yoktur. Tracing, event-driven'ın isteğe bağlı bir eklentisi değil, ön koşuludur.\n\n## Laravel'de: in-process event ≠ event-driven mimari\n\nLaravel'in `event()`/listener mekanizması, senkron çalıştığında aslında düzenlenmiş bir method çağrısıdır — aynı process, aynı transaction, aynı stack trace. Bu güvenli ve izlenebilir; bunu rahatça kullanın.\n\nEvent-driven mimarinin getirdiği zorluklar, event bir **sınırı** geçtiğinde başlar: bir queue'ya, bir message broker'a, başka bir process'e. Listener'a `ShouldQueue` ekleyip event'i RabbitMQ'ya gönderdiğiniz an, schema ve tracing borcu devreye girer. İkisini birbirine karıştırmayın: in-process listener kullanmak sizi \"event-driven mimariye\" geçirmez.\n\n## Ne zaman event-driven'a geçilir?\n\nEvent'i gerçekten bir sınırın ötesine taşımak şu durumlarda gerekçeli:\n\n- **Gerçek bir fan-out var.** Bir olayı, birbirini tanımaması gereken çok sayıda bağımsız tüketici dinliyor.\n- **Tüketici asenkron olmalı.** İş, üreticinin yanıtını bekletmemeli; \"kabul edildi\" yeterli.\n- **Tüketiciler bağımsız ölçeklenmeli ya da dağıtılmalı.** Bu da bizi [mikroservis kararına](/tr/journal/mikroservise-ne-zaman-gecerim) götürür — aynı ölçülmüş-sinyal eşiği.\n\nBunlar yoksa, doğrudan bir service çağrısı hem daha okunur hem daha güvenlidir. Tek bir listener'ı olan bir event, sadece kılık değiştirmiş bir method çağrısıdır — üstelik izi sürülmesi daha zor olanı.\n\n---\n\nEvent-driven mimari bağı gevşetir; bunu yaparken nedenselliği de gevşetir. Birincisini istiyorsanız, ikincisinin faturasını — schema ve tracing — peşin ödeyin.\n\nGevşek bağ bedava değildir; görünmez akışla ödenir.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "tags": [
        "architecture",
        "event-driven",
        "messaging",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/systems/cok-kumeli-gitops-sifir-statik-yetki/",
      "url": "https://sade.dev/tr/systems/cok-kumeli-gitops-sifir-statik-yetki/",
      "title": "Çok Kümeli GitOps’ta Sıfır Statik Yetki",
      "summary": "Diskte duran bir kubeconfig, tüm filonuza nişan almış uzun ömürlü bir kimlik bilgisidir — saldırganın tam da istediği şey. Onu, bir saatte sona eren ve hiç diske yazılmayan bir workload kimliğiyle değiştirin. İşte kural, sıra ve karşılığında üstlendiğiniz yeni kontrol düzlemi.",
      "content_html": "<p>Bir GitOps controller’ı — Argo CD, Flux — bir kümede oturur ve birçoğunu reconcile eder. Diğerlerine ulaşmak için her uzak <code>kube-apiserver</code>’a kimlik doğrulaması yapması gerekir. Bunu yapmanın varsayılan yolu bir kimlik bilgisini saklamaktır: kontrol kümesinde bir Kubernetes <code>Secret</code> olarak tutulan bir bearer token, bir client sertifikası, bir kubeconfig. Argo CD’de bunlar <code>argocd.argoproj.io/secret-type: cluster</code> etiketli nesnelerdir.</p>\n<p>Dur ve o <code>Secret</code>’ın ne olduğuna bak. O, <strong>genelde geniş yetkili, tüm filonuza nişan almış, tek bir yerde atıl duran, uzun ömürlü bir kimlik bilgisidir.</strong> Kendi kendine rotate olmaz. Yedeklerinizdedir. Kontrol kümenizde bir saldırganın bulabileceği en değerli tek şeydir, çünkü ona sahip olmak, eriştiği her kümeye sahip olmaktır. <a href=\"/tr/journal/neden-boring-architecture/\">Boring architecture</a> disiplininin tamamı, tutmak zorunda olmadığınız yükümlülükleri tutmamakla ilgilidir — ve saklanan filo geneli bir token, sırf kullanım anında taze ve kısa ömürlü bir tane basmak daha fazla iş gibi göründüğü için tuttuğunuz bir yükümlülüktür.</p>\n<p>Bu yazı o fazladan işi, sırasıyla yapmakla ilgili. Hedef <strong>sıfır statik yetki</strong>: controller’ı bir uzak kümeye doğrulayan hiçbir uzun ömürlü kimlik bilgisi asla yazılı kalmaz. Controller’ın sunduğu kimlik talep anında basılır, yaklaşık bir saatte sona erer ve yalnızca bellekte yaşar. Mekanizma, kimlik modeli için SPIFFE ve onu basmak için SPIRE’dır.</p>\n<p>Her şeyden önce: bu, bir elin parmaklarıyla sayabileceğiniz üç küme için bir araç değil. SPIRE, import ettiğiniz bir kütüphane değil, işlettiğiniz bir kontrol düzlemidir — tam da yavaş üstlenmeniz gereken türden bir yetenek; tıpkı <a href=\"/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum/\">bölünmeyi hak edene kadar modüler monolitin mikroservisi yendiği gibi</a>. Ağırlığını ancak gerçekten bir filo işlettiğinizde hak eder — çok küme, çok ekip, çalınan bir <code>Secret</code>’ın geçemeyeceği bir denetim gereksinimi. Tek paylaşılan bir küme size yetiyorsa, en ucuz güvenli kimlik bilgisi, basmak için makineyi hiç kurmadığınız olandır. Geri kalanı, filoya gerçekten ulaştığınızı varsayar.</p>\n<h2 id=\"tek-kuralı-baştan-söyleyeyim\">Tek kuralı baştan söyleyeyim</h2>\n<p>Mekanizmadan önce netleştireyim: <strong>bir küme sınırını geçen kimlik bilgisi kısa ömürlü olmalı ve asla saklanmamalı.</strong> Aşağıdaki her şey bu tek cümlenin hizmetinde. Sonunda kimlik doğrulama yolunda hâlâ bir yere diske yazılmış uzun ömürlü bir token kalıyorsa, uğruna geldiğiniz özelliği satın almadan karmaşıklık inşa etmişsinizdir — eksik bir index’ten kaçmak için read replica eklemekle aynı yanlış dönüş.</p>\n<p>“Kısa ömürlü ve asla saklanmaz”ın burada kesin bir anlamı var. Kimlik, varsayılan ömrü <strong>bir saat</strong> olan bir X.509 sertifikası (ya da varsayılan ömrü beş dakika olan bir JWT); workload tarafından yerel bir socket’ten, <strong>sunacak kendine ait bir token’ı olmadan</strong> çekilir ve ömrünün yarısında otomatik rotate edilir. Yarın çalmaya değecek bir sır yok.</p>\n<h2 id=\"spiffe-ve-spire-gerçekte-ne\">SPIFFE ve SPIRE gerçekte ne</h2>\n<p>SPIFFE bir spesifikasyon; SPIRE ise spesifikasyonun tarif ettiği şeyleri basan referans implementasyon. Dört isim tüm tasarımı taşıyor.</p>\n<p>Bir <strong>SPIFFE ID</strong> bir URI’dir: <code>spiffe://&lt;trust-domain&gt;/&lt;path&gt;</code>, örneğin <code>spiffe://prod.example.org/ns/argocd/sa/application-controller</code>. Otorite kısmı <strong>trust domain</strong>’dir — tek bir mantıksal güven kökü, tek bir CA. Path workload’u tanımlar. Bir workload’un elinde tuttuğunu kanıtladığı isim budur.</p>\n<p>Bir <strong>SVID</strong> (SPIFFE Verifiable Identity Document) ise kanıttır. İki biçim:</p>\n<ul>\n<li><strong>X.509-SVID</strong> — SPIFFE ID’si URI SAN’ında (CN’de değil, ki bu sonra önemli olacak) bulunan bir X.509 sertifikası. Onunla mTLS yaparsınız.</li>\n<li><strong>JWT-SVID</strong> — <code>sub</code>’ı SPIFFE ID, <code>aud</code>’unu hedef doğrulayıcıya scope’ladığın imzalı bir JWT. Onu bearer token olarak sunarsınız.</li>\n</ul>\n<p>Asıl mesele ömürler. SPIRE’ın sunucu konfigürasyonunda varsayılanlar <code>default_x509_svid_ttl: 1h</code> ve <code>default_jwt_svid_ttl: 5m</code>’dir; bu SVID’ler, kendi <code>ca_ttl</code>’i varsayılan <code>24h</code> olan bir CA tarafından imzalanır. Bunlar bilerek kısadır: sızdırılmış bir SVID bir saat içinde değersizleşir, ki tam da bu yüzden eskiden token sakladığınız yerde onu kullanmak güvenlidir.</p>\n<p>“Asla saklanmaz”ı mümkün kılan şey, bir SPIRE <strong>agent</strong>’ı tarafından yerel bir Unix domain socket üzerinden sunulan <strong>Workload API</strong>’dir (belgelenmiş varsayılan <code>/tmp/spire-agent/public/api.sock</code>; çoğu kurulum onu <code>/run/spire/sockets/...</code> altında mount eder). Workload o socket’e bağlanır ve SVID’sini ister. <strong>Bunu yapmak için hiçbir kimlik bilgisi sunmaz.</strong> Agent, çağıran process’i kernel üzerinden inceleyerek tanır — PID’sini, oradan da container’ını, namespace’ini, service account’unu — ve bir dizi selector’a karşı <em>workload attestation</em> yapar. Kimlik, <em>hangi sırrı tuttuğuyla</em> değil, <em>process’in doğrulanabilir biçimde ne olduğuyla</em> kurulur. Saklanacak bir şeyin olmamasının tüm nedeni o tersine çevirme.</p>\n<p>Altında SPIRE iki bileşen ve iki attestation’dır:</p>\n<ul>\n<li><strong>spire-server</strong> CA’yı tutar, SVID’leri imzalar ve registration entry’leri saklar. CA’sı varsayılan olarak self-signed, ya da bir <code>UpstreamAuthority</code> plugin’i aracılığıyla kurumsal PKI’nıza zincirlenmiş bir intermediate’tir — bir kez verdiğiniz ve birlikte yaşadığınız bir karar.</li>\n<li><strong>spire-agent</strong> her node’da çalışır. Önce <em>node’u</em> sunucuya kanıtlar — <strong>node attestation</strong> — <code>k8s_psat</code> gibi bir plugin’le; bu, projeli bir service-account token’ını Kubernetes <code>TokenReview</code> API’si üzerinden doğrular. Node’a yerleştirilmiş bir paylaşılan sır yoktur. Sonra her yerel process için, ona bir SVID vermeden önce <strong>workload attestation</strong> yapar.</li>\n</ul>\n<p>Bir <strong>registration entry</strong>, bir SPIFFE ID’yi bir parent’a (node/agent) ve bir selector kümesine (namespace, service account, image) bağlayan sunucu tarafı kayıttır. Politika odur: <em>bu workload, bu şekilde attest edilmiş, bu kimliği alır.</em> Bu entry’lerin yaşam döngüsünü yönetmek gerçek bir iştir ve süregelen operasyonel maliyetin çoğu oradadır.</p>\n<h2 id=\"federasyon-başka-bir-kümeden-gelen-bir-svidye-güvenmek\">Federasyon: başka bir kümeden gelen bir SVID’ye güvenmek</h2>\n<p>Bir trust domain, bir güven köküdür. Çok kümeli bir filo genellikle birden fazla trust domain demektir — küme başına ya da bölge başına bir tane — ve artık A domain’indeki bir workload, B domain’inde basılmış bir SVID’yi doğrulayabilmek zorundadır. İşte bu <strong>federasyon</strong>’dur ve birimi <strong>trust bundle</strong>’dır: bir domain’in SVID’lerini doğrulamanızı sağlayan public CA sertifikaları ve JWT imzalama anahtarları (bir JWKS).</p>\n<p>Her domain bir <strong>bundle endpoint</strong> ayağa kaldırır — güncel bundle’ını sunan bir URL, OIDC’nin <code>jwks_uri</code>’sinin SPIFFE karşılığı — ve her peer güncel kalmak için onu poll eder. İki profil var ve fark, tam da bootstrap sorusudur:</p>\n<ul>\n<li><strong><code>https_web</code></strong> — endpoint, public bir CA’dan bir Web-PKI sertifikasıyla cephelenir. Peer onu sıradan public güven deposuyla doğrular, yani <strong>bant dışında hiçbir başlangıç bundle’ının değiş tokuş edilmesine gerek yoktur.</strong> Güven mevcut Web PKI üzerinden bootstrap olur.</li>\n<li><strong><code>https_spiffe</code></strong> — endpoint kendi X.509-SVID’siyle kimlik doğrular. Onunla ilk kez konuşmak için peer’in o domain’in başlangıç bundle’ını zaten elinde tutması gerekir. Yani <strong>ilk bundle bant dışında gelmek zorundadır</strong>, ondan sonra ileriye dönük olarak en son çekilen bundle kullanılır.</li>\n</ul>\n<p>Bir registration entry üzerinde <code>federatesWith</code>, bir workload’un kimlik doğrulamasına izin verilen yabancı trust domain’leri listeler; agent sonra o yabancı bundle’ları workload’a Workload API üzerinden teslim eder. Bundle’lar <code>spiffe_refresh_hint</code> aralığında (genelde beş dakika civarı) yenilenir ve yeni bir imzalama anahtarını, kullanmaya başlamadan birkaç yenileme döngüsü <em>önce</em> yayınlarsınız ki rotation indiğinde federe peer’ler onu çoktan öğrenmiş olsun.</p>\n<p>“Sıfır statik yetki”nin dürüst şekline dikkat: federasyon, bootstrap güven kararının <strong>yerini değiştirir</strong>, onu silmez. <code>https_spiffe</code> ile bir başlangıç bundle’ını elle gönderirsiniz; <code>https_web</code> ile public CA sistemine yaslanırsınız. Her iki yolda da güven kökü hâlâ <em>bir yerden</em> gelir — ortadan kaldırdığınız şey, uzun ömürlü, geniş kapsamlı, küme başına kimlik bilgisidir; tek seferlik güven çıpası değil. Bu doğru takastır, ama olduğu gibi adlandırın.</p>\n<h2 id=\"apiservera-bir-svidyi-kabul-ettirmek\">apiserver’a bir SVID’yi kabul ettirmek</h2>\n<p>Niyetin Kubernetes API’siyle buluştuğu ve yanılmanın kolay olduğu yer burası. İki mekanizma var ve bugün yalnızca biri temiz.</p>\n<p><strong>JWT-SVID-olarak-OIDC yolu (işleyen).</strong> Kubernetes’e harici bir OIDC issuer’a güvenmesi söylenebilir. SPIRE, <code>/.well-known/openid-configuration</code> ve SPIRE’ın JWT-SVID imzalama anahtarlarıyla beslenen bir JWKS sunan bir <strong>OIDC Discovery Provider</strong> ile gelir. Uzak (spoke) kümenin apiserver’ını o provider’a bir OIDC issuer olarak yöneltirsiniz. Controller, yerel Workload API’sinden taze bir JWT-SVID çeker ve onu bir bearer token olarak sunar; apiserver onu SPIRE’ın JWKS’ine karşı doğrular ve bir subject’e map eder. <code>Secret</code> yok, saklanan token yok — istek başına yeni bir JWT, dakikalar içinde sona eren.</p>\n<p>Bu bir düşünce deneyi değil. Red Hat, SPIFFE/SPIRE desteğini OpenShift 4.20’den itibaren Zero Trust Workload Identity Manager operatörü olarak sevk eder ve tam olarak bu bağlantıyı, OpenShift GitOps’un sevk ettiği bir ürün özelliği olarak değil, bir nasıl-yapılır rehberi olarak belgeler: Argo CD, <code>execProviderConfig</code> aracılığıyla bir client-go <code>ExecCredential</code> plugin’i (<code>apiVersion: client.authentication.k8s.io/v1beta1</code>) kullanır; bu plugin her çağrıda SPIFFE socket’ini (<code>SPIFFE_ENDPOINT_SOCKET</code>) okur, spoke apiserver’ın beklediği audience’la (<code>SPIFFE_JWT_AUDIENCE</code>) bir JWT-SVID ister ve onu teslim eder. Kimlik bilgisi kullanım anında imal edilir ve atılır.</p>\n<p><strong>X.509-SVID-ile-mTLS yolu (tuzak).</strong> İçgüdü düz mTLS yapmaktır: controller X.509-SVID’sini sunar, apiserver SPIFFE bundle’ına client CA olarak güvenir, bitti. Bitmedi. Kubernetes client-cert kimlik doğrulaması <strong>kullanıcı adını sertifikanın Subject CN’inden, grupları Subject O’sundan</strong> türetir — ve SPIFFE ID <strong>URI SAN</strong>’ında yaşar, ki apiserver client-cert kimlik doğrulaması onu okumaz. Bu yüzden ham bir SPIFFE X.509-SVID bir Kubernetes kullanıcısına map olmaz. mTLS kullanmak için apiserver’ın önüne bir proxy — Ghostunnel ya da Envoy — koyarsınız: SPIFFE mTLS’i terminate eder, URI-SAN SPIFFE ID’sini doğrular ve apiserver’ın anladığı bir kimlikle iletir. Ghostunnel kendi rotate olan SVID’leri için Workload API’yi doğrudan tüketir. Bu işler, ama her apiserver’ın önünde bir hareketli parçadır ve OIDC yolu onu hiç gerektirmez.</p>\n<p>Olgunluk konusunda dürüst olun. Upstream Argo CD’nin native SPIFFE kimlik doğrulaması yoktur; jenerik <code>ExecCredential</code> plugin örüntüsüyle çalışır ve en olgun ürünleşme OpenShift’inkidir. Flux, SPIRE’ı <em>deploy etmek</em> için GitOps ile yönetilebilir ve OCI registry kimlik doğrulaması bir JWT-SVID tüketebilir, ama “Flux’u bir uzak kümeye kubeconfig yerine SPIFFE ile doğrula” şeklinde birinci sınıf bir özellik belgelenmemiştir — aynı OIDC/exec-credential/proxy tesisatının üzerinden gider. Native uzak küme SPIFFE kimlik doğrulamasını, etkinleştirdiğiniz bir kutu değil, monte ettiğiniz bir örüntü olarak görün.</p>\n<h2 id=\"ne-kırılır-ve-maliyeti-ne\">Ne kırılır ve maliyeti ne</h2>\n<p>Mimari diyagramların atladığı kısım bu. Saklanan bir kimlik bilgisini kaldırdınız; karşılığında bir kimlik servisini kritik yolda sıkı bir bağımlılık hâline getirdiniz ve kısa ömürlü şeyler, uzun ömürlülerin kırılmadığı yerlerde kırılır.</p>\n<ul>\n<li><strong>SVID rotation artık bir erişilebilirlik bağımlılığı.</strong> SVID’ler kısadır — X.509 bir saat, JWT beş dakika — ve ömürlerinin yarısında rotate olur. Agent ya da Workload API ayakta değilse, ya da agent yenilemek için sunucuya ulaşamıyorsa, <strong>SVID sona erer ve kimlik doğrulama durur.</strong> SPIRE sunucusu her basım ve yenileme için kritik yoldadır. Agent’lar kimlik bilgilerini cache’ler ve <em>kısa</em> sunucu kesintilerine tolerans gösterir; agent’ın <code>availability_target</code> ayarı (ayarlanırsa ≥ 24h olmalı) onu erken rotate ettirerek nazik bir kesinti için pay biriktirir. Ama hata modu gerçek ve yeni: kimlik düzlemi ayakta değilse filo kimlik doğrulaması da ayakta değildir.</li>\n<li><strong>Saat kayması (clock skew) artık bir kesinti sınıfı.</strong> Beş dakikalık bir JWT affetmez. Bir controller ile bir spoke apiserver arasında birkaç dakikalık kayma, geçerli token’ları reddeder. Filo boyunca NTP disiplini, hijyen olmaktan çıkıp sıkı bir gereksinim olur.</li>\n<li><strong>Kararan bir federasyon bundle endpoint’i kümeler arası kimlik doğrulamasını kırar — sessizce, sonradan.</strong> Bir peer’in bundle endpoint’i yenileme penceresini aşacak kadar ulaşılamazsa ve o domain anahtarlarını rotate ederse, cache’lenmiş bundle’ınız bayatlar ve <strong>domain’ler arası SVID doğrulaması başarısız olur</strong>, her iki küme tek başına sağlıklı olsa bile. Hata, kesinti anında değil rotation anında ortaya çıkar, ki bu onu teşhis etmesi kötü kılar.</li>\n<li><strong>Kimlik düzlemi artık stateful, HA-kritik bir servis.</strong> HA’daki SPIRE sunucuları tek bir SQL datastore paylaşır; varsayılan SQLite tek-node’dur, yani production demek, <em>tüm filonun kimlik doğrulayabilme yeteneğinin</em> bağlı olduğu yüksek erişilebilir bir MySQL/PostgreSQL demektir. Çok küme için bir topoloji seçersiniz — nested SPIRE (bir root sunucunun downstream sunuculara intermediate basması, bir root kesintisinden sağ çıkması) ya da küme başına trust domain’ler arası federasyon — ve her biri kendi operasyonel yüzeyini ekler.</li>\n<li><strong>Operasyonel ağırlık baştan aşağı yeni bir kontrol düzlemi.</strong> Her kümede sunucu artı agent’lar; registration-entry yaşam döngüsü (workload başına selector’lar, deployment’larla senkron tutulan); upstream-CA kararı; bundle endpoint’ler ve federasyon ilişkileri; OIDC Discovery Provider; ve mTLS yolunda apiserver başına bir proxy. Cevap “bir kubeconfig sakla” olduğunda bunların hiçbiri yoktu.</li>\n</ul>\n<h2 id=\"sıra-neden-önemli\">Sıra neden önemli?</h2>\n<p>Adımlar bir menü değil; bir dizidir ve onu atlamak, ekiplerin maliyeti faydası olmadan nasıl aldığıdır.</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>1. SPIRE’ı kur: sunucu + agent’lar, node attestation        (temel)</span></span>\n<span class=\"line\"><span>2. Controller’a Workload API üzerinden workload SVID’leri bas</span></span>\n<span class=\"line\"><span>3. Spoke apiserver’a SPIRE’a güvendir (önce OIDC yolu)</span></span>\n<span class=\"line\"><span>4. Kümeler boyunca trust domain’leri federe et</span></span>\n<span class=\"line\"><span>5. Saklanan kubeconfig Secret’larını sil                     (kazanç)</span></span></code></pre>\n<ol start=\"5\">\n<li>adım tüm mesele ve yalnızca 1–4 gerçekten işlediğinde güvenli. Yaygın hata, eski <code>Secret</code>’ı “yedek olarak” tutmaktır — bu da uzun ömürlü filo geneli kimlik bilgisinin hâlâ atıl, hâlâ yedeklerinizde, hâlâ saldırganın aldığı şey olması ve artık ayrıca SPIRE çalıştırıyor olmanız demektir. Kontrol düzleminin parasını ödediniz ve yükümlülüğü tuttunuz. Ya saklanan kimlik bilgisi yok olur ya da bunu yapmadınız; yarım kredi yoktur.</li>\n</ol>\n<p>Tersi hata, sıra fayda verecek hâle gelmeden SPIRE’a uzanmaktır — <code>Secret</code>’ı gayet iyi olacak birkaç küme için federe bir kimlik düzlemi ayağa kaldırmak. Bu, güvenlik rozeti takmış <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">ileride-lazım-olur karmaşıklığıdır</a>: henüz sahip olmadığınız bir patlama yarıçapı için kurulmuş makine. Makineyi haklı çıkaran şey filo, denetim gereksinimi ve çalınan bir token’ın patlama yarıçapıdır. O çizginin altında, en ucuz güvenli kimlik bilgisi gerçekten de hiç basmak zorunda olmadığınız sıkıcı olanıdır.</p>\n<p>Bu yaklaşımın gerçekten bittiği yer açık: statik bir küme başına kimlik bilgisinin kabul edilemez bir patlama yarıçapı olduğu kadar büyük bir filo, stateful, HA, kritik yolda bir kimlik servisini çalıştıracak ve saatlerini senkron tutacak operasyonel olgunlukla. Oraya varmış bir ekip o olgunluğun parasını zaten başka yerde öder — ve o noktada sıfır statik yetki bir süsleme değil, patlama yarıçapının baştan beri talep ettiği kimlik bilgisi modelidir. Daha önce değil.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://spiffe.io/docs/latest/spiffe-about/overview/\">SPIFFE</a> ve <a href=\"https://spiffe.io/docs/latest/spire-about/\">SPIRE</a> belgeleri kimlik modelini ve sunucu/agent bileşenlerini tanımlar; <a href=\"https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/\">SPIFFE Federation spec’i</a> ise bu tasarımın dayandığı bundle-endpoint profillerini belirtir.</p>",
      "content_text": "Bir GitOps controller’ı — Argo CD, Flux — bir kümede oturur ve birçoğunu reconcile eder. Diğerlerine ulaşmak için her uzak `kube-apiserver`’a kimlik doğrulaması yapması gerekir. Bunu yapmanın varsayılan yolu bir kimlik bilgisini saklamaktır: kontrol kümesinde bir Kubernetes `Secret` olarak tutulan bir bearer token, bir client sertifikası, bir kubeconfig. Argo CD’de bunlar `argocd.argoproj.io/secret-type: cluster` etiketli nesnelerdir.\n\nDur ve o `Secret`’ın ne olduğuna bak. O, **genelde geniş yetkili, tüm filonuza nişan almış, tek bir yerde atıl duran, uzun ömürlü bir kimlik bilgisidir.** Kendi kendine rotate olmaz. Yedeklerinizdedir. Kontrol kümenizde bir saldırganın bulabileceği en değerli tek şeydir, çünkü ona sahip olmak, eriştiği her kümeye sahip olmaktır. [Boring architecture](/tr/journal/neden-boring-architecture) disiplininin tamamı, tutmak zorunda olmadığınız yükümlülükleri tutmamakla ilgilidir — ve saklanan filo geneli bir token, sırf kullanım anında taze ve kısa ömürlü bir tane basmak daha fazla iş gibi göründüğü için tuttuğunuz bir yükümlülüktür.\n\nBu yazı o fazladan işi, sırasıyla yapmakla ilgili. Hedef **sıfır statik yetki**: controller’ı bir uzak kümeye doğrulayan hiçbir uzun ömürlü kimlik bilgisi asla yazılı kalmaz. Controller’ın sunduğu kimlik talep anında basılır, yaklaşık bir saatte sona erer ve yalnızca bellekte yaşar. Mekanizma, kimlik modeli için SPIFFE ve onu basmak için SPIRE’dır.\n\nHer şeyden önce: bu, bir elin parmaklarıyla sayabileceğiniz üç küme için bir araç değil. SPIRE, import ettiğiniz bir kütüphane değil, işlettiğiniz bir kontrol düzlemidir — tam da yavaş üstlenmeniz gereken türden bir yetenek; tıpkı [bölünmeyi hak edene kadar modüler monolitin mikroservisi yendiği gibi](/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum). Ağırlığını ancak gerçekten bir filo işlettiğinizde hak eder — çok küme, çok ekip, çalınan bir `Secret`’ın geçemeyeceği bir denetim gereksinimi. Tek paylaşılan bir küme size yetiyorsa, en ucuz güvenli kimlik bilgisi, basmak için makineyi hiç kurmadığınız olandır. Geri kalanı, filoya gerçekten ulaştığınızı varsayar.\n\n## Tek kuralı baştan söyleyeyim\n\nMekanizmadan önce netleştireyim: **bir küme sınırını geçen kimlik bilgisi kısa ömürlü olmalı ve asla saklanmamalı.** Aşağıdaki her şey bu tek cümlenin hizmetinde. Sonunda kimlik doğrulama yolunda hâlâ bir yere diske yazılmış uzun ömürlü bir token kalıyorsa, uğruna geldiğiniz özelliği satın almadan karmaşıklık inşa etmişsinizdir — eksik bir index’ten kaçmak için read replica eklemekle aynı yanlış dönüş.\n\n\"Kısa ömürlü ve asla saklanmaz\"ın burada kesin bir anlamı var. Kimlik, varsayılan ömrü **bir saat** olan bir X.509 sertifikası (ya da varsayılan ömrü beş dakika olan bir JWT); workload tarafından yerel bir socket’ten, **sunacak kendine ait bir token’ı olmadan** çekilir ve ömrünün yarısında otomatik rotate edilir. Yarın çalmaya değecek bir sır yok.\n\n## SPIFFE ve SPIRE gerçekte ne\n\nSPIFFE bir spesifikasyon; SPIRE ise spesifikasyonun tarif ettiği şeyleri basan referans implementasyon. Dört isim tüm tasarımı taşıyor.\n\nBir **SPIFFE ID** bir URI’dir: `spiffe://<trust-domain>/<path>`, örneğin `spiffe://prod.example.org/ns/argocd/sa/application-controller`. Otorite kısmı **trust domain**’dir — tek bir mantıksal güven kökü, tek bir CA. Path workload’u tanımlar. Bir workload’un elinde tuttuğunu kanıtladığı isim budur.\n\nBir **SVID** (SPIFFE Verifiable Identity Document) ise kanıttır. İki biçim:\n\n- **X.509-SVID** — SPIFFE ID’si URI SAN’ında (CN’de değil, ki bu sonra önemli olacak) bulunan bir X.509 sertifikası. Onunla mTLS yaparsınız.\n- **JWT-SVID** — `sub`’ı SPIFFE ID, `aud`’unu hedef doğrulayıcıya scope’ladığın imzalı bir JWT. Onu bearer token olarak sunarsınız.\n\nAsıl mesele ömürler. SPIRE’ın sunucu konfigürasyonunda varsayılanlar `default_x509_svid_ttl: 1h` ve `default_jwt_svid_ttl: 5m`’dir; bu SVID’ler, kendi `ca_ttl`’i varsayılan `24h` olan bir CA tarafından imzalanır. Bunlar bilerek kısadır: sızdırılmış bir SVID bir saat içinde değersizleşir, ki tam da bu yüzden eskiden token sakladığınız yerde onu kullanmak güvenlidir.\n\n\"Asla saklanmaz\"ı mümkün kılan şey, bir SPIRE **agent**’ı tarafından yerel bir Unix domain socket üzerinden sunulan **Workload API**’dir (belgelenmiş varsayılan `/tmp/spire-agent/public/api.sock`; çoğu kurulum onu `/run/spire/sockets/...` altında mount eder). Workload o socket’e bağlanır ve SVID’sini ister. **Bunu yapmak için hiçbir kimlik bilgisi sunmaz.** Agent, çağıran process’i kernel üzerinden inceleyerek tanır — PID’sini, oradan da container’ını, namespace’ini, service account’unu — ve bir dizi selector’a karşı *workload attestation* yapar. Kimlik, *hangi sırrı tuttuğuyla* değil, *process’in doğrulanabilir biçimde ne olduğuyla* kurulur. Saklanacak bir şeyin olmamasının tüm nedeni o tersine çevirme.\n\nAltında SPIRE iki bileşen ve iki attestation’dır:\n\n- **spire-server** CA’yı tutar, SVID’leri imzalar ve registration entry’leri saklar. CA’sı varsayılan olarak self-signed, ya da bir `UpstreamAuthority` plugin’i aracılığıyla kurumsal PKI’nıza zincirlenmiş bir intermediate’tir — bir kez verdiğiniz ve birlikte yaşadığınız bir karar.\n- **spire-agent** her node’da çalışır. Önce *node’u* sunucuya kanıtlar — **node attestation** — `k8s_psat` gibi bir plugin’le; bu, projeli bir service-account token’ını Kubernetes `TokenReview` API’si üzerinden doğrular. Node’a yerleştirilmiş bir paylaşılan sır yoktur. Sonra her yerel process için, ona bir SVID vermeden önce **workload attestation** yapar.\n\nBir **registration entry**, bir SPIFFE ID’yi bir parent’a (node/agent) ve bir selector kümesine (namespace, service account, image) bağlayan sunucu tarafı kayıttır. Politika odur: *bu workload, bu şekilde attest edilmiş, bu kimliği alır.* Bu entry’lerin yaşam döngüsünü yönetmek gerçek bir iştir ve süregelen operasyonel maliyetin çoğu oradadır.\n\n## Federasyon: başka bir kümeden gelen bir SVID’ye güvenmek\n\nBir trust domain, bir güven köküdür. Çok kümeli bir filo genellikle birden fazla trust domain demektir — küme başına ya da bölge başına bir tane — ve artık A domain’indeki bir workload, B domain’inde basılmış bir SVID’yi doğrulayabilmek zorundadır. İşte bu **federasyon**’dur ve birimi **trust bundle**’dır: bir domain’in SVID’lerini doğrulamanızı sağlayan public CA sertifikaları ve JWT imzalama anahtarları (bir JWKS).\n\nHer domain bir **bundle endpoint** ayağa kaldırır — güncel bundle’ını sunan bir URL, OIDC’nin `jwks_uri`’sinin SPIFFE karşılığı — ve her peer güncel kalmak için onu poll eder. İki profil var ve fark, tam da bootstrap sorusudur:\n\n- **`https_web`** — endpoint, public bir CA’dan bir Web-PKI sertifikasıyla cephelenir. Peer onu sıradan public güven deposuyla doğrular, yani **bant dışında hiçbir başlangıç bundle’ının değiş tokuş edilmesine gerek yoktur.** Güven mevcut Web PKI üzerinden bootstrap olur.\n- **`https_spiffe`** — endpoint kendi X.509-SVID’siyle kimlik doğrular. Onunla ilk kez konuşmak için peer’in o domain’in başlangıç bundle’ını zaten elinde tutması gerekir. Yani **ilk bundle bant dışında gelmek zorundadır**, ondan sonra ileriye dönük olarak en son çekilen bundle kullanılır.\n\nBir registration entry üzerinde `federatesWith`, bir workload’un kimlik doğrulamasına izin verilen yabancı trust domain’leri listeler; agent sonra o yabancı bundle’ları workload’a Workload API üzerinden teslim eder. Bundle’lar `spiffe_refresh_hint` aralığında (genelde beş dakika civarı) yenilenir ve yeni bir imzalama anahtarını, kullanmaya başlamadan birkaç yenileme döngüsü *önce* yayınlarsınız ki rotation indiğinde federe peer’ler onu çoktan öğrenmiş olsun.\n\n\"Sıfır statik yetki\"nin dürüst şekline dikkat: federasyon, bootstrap güven kararının **yerini değiştirir**, onu silmez. `https_spiffe` ile bir başlangıç bundle’ını elle gönderirsiniz; `https_web` ile public CA sistemine yaslanırsınız. Her iki yolda da güven kökü hâlâ *bir yerden* gelir — ortadan kaldırdığınız şey, uzun ömürlü, geniş kapsamlı, küme başına kimlik bilgisidir; tek seferlik güven çıpası değil. Bu doğru takastır, ama olduğu gibi adlandırın.\n\n## apiserver’a bir SVID’yi kabul ettirmek\n\nNiyetin Kubernetes API’siyle buluştuğu ve yanılmanın kolay olduğu yer burası. İki mekanizma var ve bugün yalnızca biri temiz.\n\n**JWT-SVID-olarak-OIDC yolu (işleyen).** Kubernetes’e harici bir OIDC issuer’a güvenmesi söylenebilir. SPIRE, `/.well-known/openid-configuration` ve SPIRE’ın JWT-SVID imzalama anahtarlarıyla beslenen bir JWKS sunan bir **OIDC Discovery Provider** ile gelir. Uzak (spoke) kümenin apiserver’ını o provider’a bir OIDC issuer olarak yöneltirsiniz. Controller, yerel Workload API’sinden taze bir JWT-SVID çeker ve onu bir bearer token olarak sunar; apiserver onu SPIRE’ın JWKS’ine karşı doğrular ve bir subject’e map eder. `Secret` yok, saklanan token yok — istek başına yeni bir JWT, dakikalar içinde sona eren.\n\nBu bir düşünce deneyi değil. Red Hat, SPIFFE/SPIRE desteğini OpenShift 4.20’den itibaren Zero Trust Workload Identity Manager operatörü olarak sevk eder ve tam olarak bu bağlantıyı, OpenShift GitOps’un sevk ettiği bir ürün özelliği olarak değil, bir nasıl-yapılır rehberi olarak belgeler: Argo CD, `execProviderConfig` aracılığıyla bir client-go `ExecCredential` plugin’i (`apiVersion: client.authentication.k8s.io/v1beta1`) kullanır; bu plugin her çağrıda SPIFFE socket’ini (`SPIFFE_ENDPOINT_SOCKET`) okur, spoke apiserver’ın beklediği audience’la (`SPIFFE_JWT_AUDIENCE`) bir JWT-SVID ister ve onu teslim eder. Kimlik bilgisi kullanım anında imal edilir ve atılır.\n\n**X.509-SVID-ile-mTLS yolu (tuzak).** İçgüdü düz mTLS yapmaktır: controller X.509-SVID’sini sunar, apiserver SPIFFE bundle’ına client CA olarak güvenir, bitti. Bitmedi. Kubernetes client-cert kimlik doğrulaması **kullanıcı adını sertifikanın Subject CN’inden, grupları Subject O’sundan** türetir — ve SPIFFE ID **URI SAN**’ında yaşar, ki apiserver client-cert kimlik doğrulaması onu okumaz. Bu yüzden ham bir SPIFFE X.509-SVID bir Kubernetes kullanıcısına map olmaz. mTLS kullanmak için apiserver’ın önüne bir proxy — Ghostunnel ya da Envoy — koyarsınız: SPIFFE mTLS’i terminate eder, URI-SAN SPIFFE ID’sini doğrular ve apiserver’ın anladığı bir kimlikle iletir. Ghostunnel kendi rotate olan SVID’leri için Workload API’yi doğrudan tüketir. Bu işler, ama her apiserver’ın önünde bir hareketli parçadır ve OIDC yolu onu hiç gerektirmez.\n\nOlgunluk konusunda dürüst olun. Upstream Argo CD’nin native SPIFFE kimlik doğrulaması yoktur; jenerik `ExecCredential` plugin örüntüsüyle çalışır ve en olgun ürünleşme OpenShift’inkidir. Flux, SPIRE’ı *deploy etmek* için GitOps ile yönetilebilir ve OCI registry kimlik doğrulaması bir JWT-SVID tüketebilir, ama \"Flux’u bir uzak kümeye kubeconfig yerine SPIFFE ile doğrula\" şeklinde birinci sınıf bir özellik belgelenmemiştir — aynı OIDC/exec-credential/proxy tesisatının üzerinden gider. Native uzak küme SPIFFE kimlik doğrulamasını, etkinleştirdiğiniz bir kutu değil, monte ettiğiniz bir örüntü olarak görün.\n\n## Ne kırılır ve maliyeti ne\n\nMimari diyagramların atladığı kısım bu. Saklanan bir kimlik bilgisini kaldırdınız; karşılığında bir kimlik servisini kritik yolda sıkı bir bağımlılık hâline getirdiniz ve kısa ömürlü şeyler, uzun ömürlülerin kırılmadığı yerlerde kırılır.\n\n- **SVID rotation artık bir erişilebilirlik bağımlılığı.** SVID’ler kısadır — X.509 bir saat, JWT beş dakika — ve ömürlerinin yarısında rotate olur. Agent ya da Workload API ayakta değilse, ya da agent yenilemek için sunucuya ulaşamıyorsa, **SVID sona erer ve kimlik doğrulama durur.** SPIRE sunucusu her basım ve yenileme için kritik yoldadır. Agent’lar kimlik bilgilerini cache’ler ve *kısa* sunucu kesintilerine tolerans gösterir; agent’ın `availability_target` ayarı (ayarlanırsa ≥ 24h olmalı) onu erken rotate ettirerek nazik bir kesinti için pay biriktirir. Ama hata modu gerçek ve yeni: kimlik düzlemi ayakta değilse filo kimlik doğrulaması da ayakta değildir.\n- **Saat kayması (clock skew) artık bir kesinti sınıfı.** Beş dakikalık bir JWT affetmez. Bir controller ile bir spoke apiserver arasında birkaç dakikalık kayma, geçerli token’ları reddeder. Filo boyunca NTP disiplini, hijyen olmaktan çıkıp sıkı bir gereksinim olur.\n- **Kararan bir federasyon bundle endpoint’i kümeler arası kimlik doğrulamasını kırar — sessizce, sonradan.** Bir peer’in bundle endpoint’i yenileme penceresini aşacak kadar ulaşılamazsa ve o domain anahtarlarını rotate ederse, cache’lenmiş bundle’ınız bayatlar ve **domain’ler arası SVID doğrulaması başarısız olur**, her iki küme tek başına sağlıklı olsa bile. Hata, kesinti anında değil rotation anında ortaya çıkar, ki bu onu teşhis etmesi kötü kılar.\n- **Kimlik düzlemi artık stateful, HA-kritik bir servis.** HA’daki SPIRE sunucuları tek bir SQL datastore paylaşır; varsayılan SQLite tek-node’dur, yani production demek, *tüm filonun kimlik doğrulayabilme yeteneğinin* bağlı olduğu yüksek erişilebilir bir MySQL/PostgreSQL demektir. Çok küme için bir topoloji seçersiniz — nested SPIRE (bir root sunucunun downstream sunuculara intermediate basması, bir root kesintisinden sağ çıkması) ya da küme başına trust domain’ler arası federasyon — ve her biri kendi operasyonel yüzeyini ekler.\n- **Operasyonel ağırlık baştan aşağı yeni bir kontrol düzlemi.** Her kümede sunucu artı agent’lar; registration-entry yaşam döngüsü (workload başına selector’lar, deployment’larla senkron tutulan); upstream-CA kararı; bundle endpoint’ler ve federasyon ilişkileri; OIDC Discovery Provider; ve mTLS yolunda apiserver başına bir proxy. Cevap \"bir kubeconfig sakla\" olduğunda bunların hiçbiri yoktu.\n\n## Sıra neden önemli?\n\nAdımlar bir menü değil; bir dizidir ve onu atlamak, ekiplerin maliyeti faydası olmadan nasıl aldığıdır.\n\n```\n1. SPIRE’ı kur: sunucu + agent’lar, node attestation        (temel)\n2. Controller’a Workload API üzerinden workload SVID’leri bas\n3. Spoke apiserver’a SPIRE’a güvendir (önce OIDC yolu)\n4. Kümeler boyunca trust domain’leri federe et\n5. Saklanan kubeconfig Secret’larını sil                     (kazanç)\n```\n\n5. adım tüm mesele ve yalnızca 1–4 gerçekten işlediğinde güvenli. Yaygın hata, eski `Secret`’ı \"yedek olarak\" tutmaktır — bu da uzun ömürlü filo geneli kimlik bilgisinin hâlâ atıl, hâlâ yedeklerinizde, hâlâ saldırganın aldığı şey olması ve artık ayrıca SPIRE çalıştırıyor olmanız demektir. Kontrol düzleminin parasını ödediniz ve yükümlülüğü tuttunuz. Ya saklanan kimlik bilgisi yok olur ya da bunu yapmadınız; yarım kredi yoktur.\n\nTersi hata, sıra fayda verecek hâle gelmeden SPIRE’a uzanmaktır — `Secret`’ı gayet iyi olacak birkaç küme için federe bir kimlik düzlemi ayağa kaldırmak. Bu, güvenlik rozeti takmış [ileride-lazım-olur karmaşıklığıdır](/tr/journal/ileride-lazim-olur-kodunun-faturasi): henüz sahip olmadığınız bir patlama yarıçapı için kurulmuş makine. Makineyi haklı çıkaran şey filo, denetim gereksinimi ve çalınan bir token’ın patlama yarıçapıdır. O çizginin altında, en ucuz güvenli kimlik bilgisi gerçekten de hiç basmak zorunda olmadığınız sıkıcı olanıdır.\n\nBu yaklaşımın gerçekten bittiği yer açık: statik bir küme başına kimlik bilgisinin kabul edilemez bir patlama yarıçapı olduğu kadar büyük bir filo, stateful, HA, kritik yolda bir kimlik servisini çalıştıracak ve saatlerini senkron tutacak operasyonel olgunlukla. Oraya varmış bir ekip o olgunluğun parasını zaten başka yerde öder — ve o noktada sıfır statik yetki bir süsleme değil, patlama yarıçapının baştan beri talep ettiği kimlik bilgisi modelidir. Daha önce değil.\n\n---\n\n*İlgili:* [SPIFFE](https://spiffe.io/docs/latest/spiffe-about/overview/) ve [SPIRE](https://spiffe.io/docs/latest/spire-about/) belgeleri kimlik modelini ve sunucu/agent bileşenlerini tanımlar; [SPIFFE Federation spec’i](https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/) ise bu tasarımın dayandığı bundle-endpoint profillerini belirtir.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "tags": [
        "kubernetes",
        "gitops",
        "spiffe",
        "security",
        "architecture",
        "System"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/systems/ebpf-ile-gpu-finops/",
      "url": "https://sade.dev/tr/systems/ebpf-ile-gpu-finops/",
      "title": "eBPF ile GPU FinOps",
      "summary": "Bütün bir GPU için faturalandırılıyorsunuz ve onun %3’ünü kullanıyorsunuz — ve monitoring’iniz size o %3’ün kimin olduğunu söyleyemiyor. eBPF, syscall sınırını izleyerek GPU işini bir pod’a atfeder; DCGM ise silikonun ne kadar iyi koştuğunu ölçer. Farklı sorulara cevap verirler ve gerçek bir chargeback her ikisine de ihtiyaç duyar.",
      "content_html": "<p>Bir GPU node’u, bir raf dolusu CPU makinesinden saatte daha fazlaya mal olur ve finansın onun hakkında sorduğu soru, her şey hakkında sorduğu soruyla aynıdır: <strong>bunu kim kullandı ve ne kadar?</strong> CPU, bellek ve disk için bunu cevaplayabilirsiniz — cgroup’lar bunu process başına hesaplar, cAdvisor pod başına raporlar, Prometheus ekip başına toplar. GPU içinse çoğu kümede dürüst cevap, kimsenin bilmediğidir. Filodaki en pahalı kaynağın parasını ödüyorsunuz ve onun tek bir kuruşunu bile atfedemiyorsunuz.</p>\n<p>Bu, doldurmayı unuttuğunuz bir araç boşluğu değil. Yapısaldır: GPU, <strong>yığınınızın geri kalanının dayandığı muhasebe düzleminin dışındadır.</strong> Bu yazı, bunun nedeni ve atfetmeyi geri kazanmak için eBPF kullanımı ile ilgili — ve eBPF’in ne görebildiği ve ne göremediğine dair sert bir çizgiyle birlikte, çünkü buradaki en pahalı hata, bir aracın fiziksel olarak cevaplayamadığı bir soruyu cevapladığına inanmaktır.</p>\n<h2 id=\"gpu-mevcut-metriklerinize-neden-görünmez\">GPU mevcut metriklerinize neden görünmez?</h2>\n<p>Üç ayrı körlük üst üste yığılır.</p>\n<p><strong>cgroup’lar GPU’yu hesaplamaz.</strong> Bir cgroup, CPU, bellek, block I/O ve PID’leri hesaplayan ve sınırlayan bir kernel yapısıdır. GPU bunların hiçbiri değil. Compute ve framebuffer (VRAM), NVIDIA kernel driver’ı ve userspace CUDA runtime’ı tarafından yönetilir ve cgroup hesaplamasına opaktır. Bir <code>device</code> cgroup controller’ı var, ama o yalnızca device node’larına (<code>/dev/nvidia0</code> ve dostları) <em>erişimi geçitler</em> — izin ver ya da reddet. Tek bir saniye GPU zamanını ya da bir megabayt VRAM’i hesaplamaz. Yani size pod başına CPU ve bellek veren tüm cAdvisor → Prometheus boru hattının, kuruluş gereği, <strong>GPU hakkında söyleyecek hiçbir şeyi yoktur.</strong></p>\n<p><strong><code>nvidia.com/gpu</code> bir tahsis sayısıdır, bir kullanım sinyali değil.</strong> Kubernetes GPU’ları, onları kubelet’e extended resource <code>nvidia.com/gpu</code> olarak kaydeden NVIDIA device plugin’i üzerinden öğrenir. Pod’lar bunu bir integer olarak ister — bütün GPU’lar. Scheduler istenen <em>sayıyı</em> mevcut <em>sayıyla</em> eşler. Tahsis katmanının bildiği tek şey budur. <strong>Bir GPU istemek, bir GPU kullanmak değildir.</strong> Bir pod bütün bir cihazı tutabilir — ve bütün cihazın faturasını ödeyebilir — onu %3’te sürerken. Üzerinden chargeback yapabileceğiniz tahsis sayısı, tam da tüketim hakkında size hiçbir şey söylemeyen sayıdır.</p>\n<p><strong><code>nvidia-smi</code>’nin kullanım sayısı bile size yalan söyler.</strong> <code>nvidia-smi</code>’ye uzanırsınız ve <code>utilization.gpu</code> alanı kurtuluş gibi görünür. Sandığınız şey değil. NVML onu şöyle tanımlar: <em>örnekleme periyodu boyunca bir ya da daha fazla kernel’in çalıştığı zamanın yüzdesi.</em> <strong>Zamansal varlığı — GPU hiç meşgul muydu — ölçer, ne kadarının meşgul olduğunu değil.</strong> Onlarca SM’den birini işgal eden tek thread’lik bir kernel %100’e yakın raporlayabilir. Microsoft, A100’ler üzerinde 8B parametreli bir modeli servis ederken bellek-bağımlı decode fazında %10’un altında compute kullanımı raporlamıştır — naif bir “kullanım” okuması o GPU’ları dolu sayacakken. <code>utilization.gpu</code> üzerinden chargeback yaparsanız, silikon neredeyse boştayken “meşgul” diyen bir sayı üzerinden faturalandırıyorsunuzdur.</p>\n<p>Yani: tahsis katmanı kullanımı-değil-sayıyı bilir, cgroup’lar hiçbir şey bilmez ve tek kolay yüzde yanlış şeyi ölçer. Boşluk budur.</p>\n<h2 id=\"i̇ki-soru-ve-neden-farklı-araçlar-gerektirdikleri\">İki soru ve neden farklı araçlar gerektirdikleri</h2>\n<p>Herhangi bir araçtan önce iki soruyu ayırın, çünkü onları karıştırmak temel hatadır — <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">bir aracın çözdüğü problemi eldeki problemle karıştırmakla</a> aynı şekil.</p>\n<ul>\n<li><strong>Atfetme — GPU işini <em>kim</em> yaptı ve <em>ne kadar</em>?</strong> Bu FinOps sorusudur. Pod başına, ekip başına GPU aktivitesi hesabı gerektirir: hangi workload kernel başlattı, VRAM tahsis etti, veri taşıdı ve ne kadar süreyle. Chargeback bunun üzerinden koşar.</li>\n<li><strong>Verimlilik — silikon <em>ne kadar iyi</em> kullanıldı?</strong> SM’ler gerçekten doluydu mu, tensor core’lar aktif miydi, darboğaz bellek bant genişliği miydi? Bu performans sorusudur. Bir ekibin harcamasının <em>haklı</em> olup olmadığını söyler, ama <em>kimin</em> harcaması olduğunu söyleyemez.</li>\n</ul>\n<p>Bunlar iki farklı ölçüm düzlemine eşlenir ve aşağıdaki her şeyi örgütleyen kural şu: <strong>eBPF atfetmeyi cevaplar; GPU’nun kendi sayaçları verimliliği cevaplar. Hiçbiri diğerinin yerine geçmez.</strong></p>\n<h2 id=\"ebpfin-görebildiği-kontrol-düzlemi\">eBPF’in görebildiği: kontrol düzlemi</h2>\n<p>eBPF kernel’de çalışır ve kprobe’lara, uprobe’lara, tracepoint’lere ve syscall’lara takılır. Okunacak bir GPU-kullanımı tracepoint’i yok — ama GPU <em>işinin talep edildiği</em> iki yüzey var ve ikisi de kernel’den görünür:</p>\n<ul>\n<li><strong>Driver ioctl sınırı.</strong> Her parça GPU işi — kernel gönderimi, bellek tahsisi, senkronizasyon — sonunda <code>/dev/nvidiactl</code>’a ve <code>/dev/nvidia0…N</code>’e bir <code>ioctl()</code> olur. Driver’ın giriş noktalarındaki (<code>nvidia_unlocked_ioctl</code>, <code>nvidia_open</code>) bir kprobe o trafiği görür. (Kapalı driver tarihsel olarak yalnızca tek bir tracepoint, donanım hata olayları için <code>nvidia:nvidia_dev_xid</code>, sunar; geri kalan her şey kprobe’lanır.)</li>\n<li><strong>CUDA kütüphane sınırı.</strong> <code>libcuda.so</code> / <code>libcudart.so</code> üzerindeki bir uprobe API’nin kendisini izler: <code>cuLaunchKernel</code>, <code>cuMemAlloc</code>, <code>cuMemcpyHtoD</code>, <code>cuStreamSynchronize</code> ve dostları. Bir giriş uprobe’unu bir dönüş uretprobe’uyla eşlemek her çağrının süresini ölçer.</li>\n</ul>\n<p>Bunun <em>atfetme</em> olmasının nedeni, eBPF’in her hook noktasında çağıran PID/TGID’yi ve cgroup’u native olarak okumasıdır — <code>bpf_get_current_pid_tgid</code> ve cgroup id tam orada. Bu size <strong>PID → cgroup → pod → namespace → ekip</strong> zincirini verir, uygulama değişikliği olmadan ve NVIDIA’nın işbirliği olmadan. Pod başına şunları alırsınız: kernel-başlatma sayıları, başlatma boyutları, VRAM tahsis boyutları, memcpy hacmi ve yönü, çağrı zamanlaması. Bu, tamamen sınırın kernel tarafından türetilen, gerçek ve savunulabilir bir chargeback sinyalidir.</p>\n<p>Buradaki ekosistem genç ama gerçek. Primitifler bugün işliyor — CUDA kütüphanelerini uprobe’layan ve nvidia ioctl yolunu kprobe’layan çalışan yazılar ve eğitimler var ve <code>bpftime</code> eBPF mantığını GPU olaylarına bağlamayı keşfediyor. Tetragon (Cilium), Kubernetes pod kimliğiyle jenerik <code>process_uprobe</code>, <code>process_kprobe</code> ve ioctl izleme sevk eder, yani CUDA sembollerine ya da ioctl yoluna <em>yöneltilebilir</em> — ama bunun sevk edilen bir “GPU FinOps” özelliği değil, yazdığınız bir <code>TracingPolicy</code> olduğunu anlayın. Henüz baskın, anahtar-teslim bir eBPF chargeback ürünü yok. Size “tak-çalıştır eBPF GPU FinOps” satan biri, ekosistemin şu an ulaştığından öteye satıyordur.</p>\n<h2 id=\"ebpfin-göremediği-silikon\">eBPF’in göremediği: silikon</h2>\n<p>Dürüst limit bu ve bir sürümde düzeltilecek bir olgunluk problemi değil — verinin nerede yaşadığının fiziği.</p>\n<p>eBPF <strong>kontrol düzlemini</strong> görür: API çağrıları, ioctl’lar, tahsis boyutları, başlatma sayıları ve izleyebildiğiniz yerde gönderim-tamamlanma zamanlaması. GPU’nun <em>içini</em> <strong>görmez</strong>. SM occupancy’sini, tensor-core kullanımını ya da erişilen bellek bant genişliğini okuyamaz, çünkü bunlar <em>cihazda</em> yaşayan ve yalnızca NVML / DCGM / CUPTI üzerinden açığa çıkan donanım performans sayaçlarıdır. eBPF’in onlara giden bir yolu yoktur. Bir pod’un on bin kernel başlattığını ve 40 GB VRAM tahsis ettiğini söyleyebilir; o kernel’lerin SM’leri doyurup doyurmadığını ya da onları %90 boş bırakıp bırakmadığını söyleyemez.</p>\n<p>İkinci, daha incelikli bir limit var. eBPF’in takılabildiği ioctl sınırında bile, <em>payload’lar</em> büyük ölçüde opaktır. Driver’ın komut yapıları (<code>NV_ESC_*</code> Resource Manager API’si) karmaşık ve fiilen proprietary. Bir ioctl’ın <em>olduğunu</em> görebilirsiniz — komut numarasını, çağıran PID’yi, zamanlamayı — ama keyfi bir RM payload’unun semantik içeriğini decode etmek pratik dışı ve kırılgandır. İşin gerçeğini ve atfetmesini alırsınız, anlamının bedava bir okumasını değil.</p>\n<p>Cazip bir kapıyı kapatan bir not: NVIDIA’nın açık kernel modülleri (<code>open-gpu-kernel-modules</code>, Turing ve sonrası) <strong>kernel arayüz katmanını</strong> açar — modül init’ini, ioctl giriş noktalarını, <code>NV_ESC_*</code> komut yüzeyini. Bu, <em>neyi</em> hook’layacağınızı anlamanıza gerçekten yardımcı olur. Ama GPU’nun beyni kapalı kalır: Turing+’ta yönetimin çoğu GPU üzerindeki GSP firmware’inde koşar ve o firmware — modüllerin ihtiyaç duyduğu user-mode driver bileşenleriyle birlikte — hâlâ kapalı olarak dağıtılır. Kernel modüllerini açmak, eBPF’e donanım performans sayaçlarını <strong>açmaz</strong>. Cihaz-içi limit değişmemiştir. Açık modüllerin eBPF’e SM occupancy okuttuğunu iddia eden biri, mevcut kanıtlara göre yanılıyordur.</p>\n<h2 id=\"hâlâ-ihtiyaç-duyduğunuz-userspace-yolu-dcgm\">Hâlâ ihtiyaç duyduğunuz userspace yolu: DCGM</h2>\n<p>eBPF silikonu göremediği için, cevabın verimlilik yarısı userspace’ten gelir ve standart araç, Prometheus için <strong>dcgm-exporter</strong> ile NVIDIA <strong>DCGM</strong> (Data Center GPU Manager)’dır. DCGM, GPU’nun donanım sayaçlarını driver üzerinden okur ve eBPF’in erişemediği alanları açığa çıkarır:</p>\n<ul>\n<li><code>DCGM_FI_PROF_SM_ACTIVE</code> — bir multiprocessor’da en az bir warp’ın aktif olduğu zamanın oranı, hepsinin ortalaması.</li>\n<li><code>DCGM_FI_PROF_SM_OCCUPANCY</code> — resident warp’ların desteklenen maksimuma oranı: <em>gerçek</em> occupancy.</li>\n<li><code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE</code> — tensor pipe’ın aktif olduğu döngülerin oranı.</li>\n<li><code>DCGM_FI_PROF_DRAM_ACTIVE</code> — bir bellek-bant-genişliği vekili.</li>\n<li><code>DCGM_FI_DEV_FB_USED</code> — gerçekten kullanımda olan framebuffer (VRAM).</li>\n</ul>\n<p>Bunlar size bir ekibin pahalı tahsisinin ekmeğini kazanıp kazanmadığını söyleyen sayılardır. DCGM onları pod’lara, her GPU UUID’yi onu tutan pod’a eşleyen kubelet <strong>Pod Resources API</strong>’si (<code>/var/lib/kubelet/pod-resources</code> adresindeki bir gRPC servisi) üzerinden atfeder.</p>\n<p>Ama DCGM’in atfetmesinin, tam da eBPF’in yerini hak ettiği yerde sert bir kenarı var: <strong>DCGM cihaz-seviyesi metrikler üretir, bu yüzden tek bir fiziksel GPU’yu paylaşan tüketicileri ayırt edemez.</strong> Time-slicing ya da MPS (Multi-Process Service) altında, bir GPU’yu paylaşan tüm pod’lar <em>aynı, kopyalanmış</em> cihaz-seviyesi değerleri alır — <code>DCGM_FI_DEV_FB_USED</code> dahil. NVIDIA’nın kendi exporter’ı, time-slicing etkinken metrikleri container’larla ilişkilendirmediğini belgeler; <code>--kubernetes-virtual-gpus=true</code> ile her paylaşan pod tüm fiziksel GPU’nun durumunu aynalar. MIG altında atfetme, keyfi pod’lara değil GPU-instance seviyesine kayar. Yani tam da paylaşılan-GPU durumunda — ki bu durum, bütün-GPU tahsisi para israf ettiği <em>için</em> var olur — DCGM size kimin ne tükettiğini söyleyemez. eBPF’in PID-seviyesi izlemesi söyleyebilir. İki araç, her birinin en zayıf olduğu dikişte birbirini tamamlar.</p>\n<p>(En derin verimlilik profilleme için <strong>CUPTI</strong>, CUDA Profiling Tools Interface var; DCGM’in açığa çıkarmadığı sayaçları okuyabilir — ama Nsight Compute gibi metrik-replay profilleme kernel’leri birden çok kez yeniden çalıştırır ve ağır overhead taşır. Bu bir profilleme-oturumu aracıdır, her zaman açık tenant başına telemetri değil. DCGM düşük-overhead’li, örnekleme-bazlı sürekli yoldur; CUPTI ağır, derin olandır.)</p>\n<h2 id=\"kernel-vs-userspace-takası-açıkça\">Kernel-vs-userspace takası, açıkça</h2>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Düzlem</th><th>Araç</th><th>Görür</th><th>Göremez</th></tr></thead><tbody><tr><td>Kernel (kontrol)</td><td>eBPF</td><td>PID→pod atfetme, başlatma sayıları, tahsis boyutları, memcpy hacmi, çağrı zamanlaması — time-slicing altında pod başına <em>dahil</em></td><td>cihaz-içi SM/tensor occupancy, bellek bant genişliği, ioctl payload içleri</td></tr><tr><td>Userspace (cihaz)</td><td>DCGM</td><td>gerçek SM occupancy, tensor aktivitesi, DRAM aktivitesi, kullanılan VRAM — donanım sayaçlarından</td><td>paylaşılan GPU altında (time-slicing/MPS) pod başına atfetme: tüm paylaşanlar aynı değerleri alır</td></tr></tbody></table></div>\n<p>eBPF düşük-overhead’lidir, uygulama değişikliği gerektirmez, vendor işbirliği gerektirmez ve PID/cgroup üzerinden native atfeder — <em>kim ve ne kadar</em> için doğru araç odur. DCGM silikonu okur — <em>ne kadar iyi</em> için doğru araç odur. Maliyetler de dürüst: uprobe’lar gerçek overhead taşır ve CUDA sembolleri <strong>versiyonludur</strong> (<code>cuMemAlloc@CUDA_11.0</code> vs <code>@CUDA_12.0</code>), yani probe’lar dinamik sembol çözümü gerektirir ve driver yükseltmeleri boyunca kırılır; CUDA API çağrıları saniyede on binden fazla atabilir, yani handler’lar ucuz kalmalı yoksa ölçtükleri workload’u yavaşlatırlar. DCGM kendi örnekleme overhead’i olan userspace polling’dir ve yalnızca belirli sayaç gruplarının birlikte okunabildiği bir donanım kısıtı taşır.</p>\n<h2 id=\"i̇kisine-de-neden-hangi-sırayla-ihtiyacınız-var\">İkisine de neden, hangi sırayla ihtiyacınız var</h2>\n<p>Yanlış dönüş, bir aracı seçip ona diğer aracın sorusunu sormaktır — ekipleri DCGM’in cihaz-seviyesi sayıları üzerinden faturalandırıp bir GPU’yu paylaşan herkesi sessizce fazla yüklemek, ya da eBPF’in başlatma sayılarına verimlilik vekili olarak güvenip meşgul görünen bir workload’un iyi kullanıldığı sonucuna varmak. Gerçekten işleyen sıra:</p>\n<pre class=\"astro-code astro-code-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee;overflow-x:auto\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>1. eBPF: GPU işini pod/ekibe atfet           (kim, ne kadar — fatura)</span></span>\n<span class=\"line\"><span>2. DCGM: cihaz-içi verimliliği ölç           (ne kadar iyi — haklı mıydı)</span></span>\n<span class=\"line\"><span>3. Onları pod kimliğinde birleştir            (ekibin harcaması VE verimliliği)</span></span></code></pre>\n<p>Atfetme önce, çünkü finansın gerçekten sorduğu soru o ve mevcut yığınınızın hiç cevaplayamadığı o. Verimlilik sonra, çünkü faturayı bir karara dönüştürür — pahalı bir tahsisi %4 occupancy’de tutan bir ekip, artık <em>görebileceğiniz</em> bir <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">ileride-lazım-olur kapasite vergisi</a> ödüyordur; oysa önceden GPU, kimsenin açamadığı düz bir kalemdi. Pod kimliğinde birleşince, nihayet her diğer kaynak için sahip olduğunuz şeye sahip olursunuz: atfedilmiş harcama, üzerinde harekete geçecek verimlilik bağlamıyla. İki düzlem gerektirmesinin nedeni, görünmez olmasının nedeniyle aynıdır — silikon muhasebe düzleminde hiç yaşamadı ve tek bir probe bile sınırı kat etmiyor.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://docs.nvidia.com/datacenter/dcgm/latest/installation/install-dcgm-exporter.html\">DCGM exporter belgeleri</a> pod-atfetme yolunu, <a href=\"https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html\">DCGM özellik genel bakışı</a> ise profilleme alanlarını tanımlar; bunun yaslandığı eBPF primitifleri <a href=\"https://ebpf.io/what-is-ebpf/\">ebpf.io</a> ve <a href=\"https://tetragon.io/\">Tetragon</a>’da belgelenmiştir.</p>",
      "content_text": "Bir GPU node’u, bir raf dolusu CPU makinesinden saatte daha fazlaya mal olur ve finansın onun hakkında sorduğu soru, her şey hakkında sorduğu soruyla aynıdır: **bunu kim kullandı ve ne kadar?** CPU, bellek ve disk için bunu cevaplayabilirsiniz — cgroup’lar bunu process başına hesaplar, cAdvisor pod başına raporlar, Prometheus ekip başına toplar. GPU içinse çoğu kümede dürüst cevap, kimsenin bilmediğidir. Filodaki en pahalı kaynağın parasını ödüyorsunuz ve onun tek bir kuruşunu bile atfedemiyorsunuz.\n\nBu, doldurmayı unuttuğunuz bir araç boşluğu değil. Yapısaldır: GPU, **yığınınızın geri kalanının dayandığı muhasebe düzleminin dışındadır.** Bu yazı, bunun nedeni ve atfetmeyi geri kazanmak için eBPF kullanımı ile ilgili — ve eBPF’in ne görebildiği ve ne göremediğine dair sert bir çizgiyle birlikte, çünkü buradaki en pahalı hata, bir aracın fiziksel olarak cevaplayamadığı bir soruyu cevapladığına inanmaktır.\n\n## GPU mevcut metriklerinize neden görünmez?\n\nÜç ayrı körlük üst üste yığılır.\n\n**cgroup’lar GPU’yu hesaplamaz.** Bir cgroup, CPU, bellek, block I/O ve PID’leri hesaplayan ve sınırlayan bir kernel yapısıdır. GPU bunların hiçbiri değil. Compute ve framebuffer (VRAM), NVIDIA kernel driver’ı ve userspace CUDA runtime’ı tarafından yönetilir ve cgroup hesaplamasına opaktır. Bir `device` cgroup controller’ı var, ama o yalnızca device node’larına (`/dev/nvidia0` ve dostları) *erişimi geçitler* — izin ver ya da reddet. Tek bir saniye GPU zamanını ya da bir megabayt VRAM’i hesaplamaz. Yani size pod başına CPU ve bellek veren tüm cAdvisor → Prometheus boru hattının, kuruluş gereği, **GPU hakkında söyleyecek hiçbir şeyi yoktur.**\n\n**`nvidia.com/gpu` bir tahsis sayısıdır, bir kullanım sinyali değil.** Kubernetes GPU’ları, onları kubelet’e extended resource `nvidia.com/gpu` olarak kaydeden NVIDIA device plugin’i üzerinden öğrenir. Pod’lar bunu bir integer olarak ister — bütün GPU’lar. Scheduler istenen *sayıyı* mevcut *sayıyla* eşler. Tahsis katmanının bildiği tek şey budur. **Bir GPU istemek, bir GPU kullanmak değildir.** Bir pod bütün bir cihazı tutabilir — ve bütün cihazın faturasını ödeyebilir — onu %3’te sürerken. Üzerinden chargeback yapabileceğiniz tahsis sayısı, tam da tüketim hakkında size hiçbir şey söylemeyen sayıdır.\n\n**`nvidia-smi`’nin kullanım sayısı bile size yalan söyler.** `nvidia-smi`’ye uzanırsınız ve `utilization.gpu` alanı kurtuluş gibi görünür. Sandığınız şey değil. NVML onu şöyle tanımlar: *örnekleme periyodu boyunca bir ya da daha fazla kernel’in çalıştığı zamanın yüzdesi.* **Zamansal varlığı — GPU hiç meşgul muydu — ölçer, ne kadarının meşgul olduğunu değil.** Onlarca SM’den birini işgal eden tek thread’lik bir kernel %100’e yakın raporlayabilir. Microsoft, A100’ler üzerinde 8B parametreli bir modeli servis ederken bellek-bağımlı decode fazında %10’un altında compute kullanımı raporlamıştır — naif bir “kullanım” okuması o GPU’ları dolu sayacakken. `utilization.gpu` üzerinden chargeback yaparsanız, silikon neredeyse boştayken “meşgul” diyen bir sayı üzerinden faturalandırıyorsunuzdur.\n\nYani: tahsis katmanı kullanımı-değil-sayıyı bilir, cgroup’lar hiçbir şey bilmez ve tek kolay yüzde yanlış şeyi ölçer. Boşluk budur.\n\n## İki soru ve neden farklı araçlar gerektirdikleri\n\nHerhangi bir araçtan önce iki soruyu ayırın, çünkü onları karıştırmak temel hatadır — [bir aracın çözdüğü problemi eldeki problemle karıştırmakla](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) aynı şekil.\n\n- **Atfetme — GPU işini *kim* yaptı ve *ne kadar*?** Bu FinOps sorusudur. Pod başına, ekip başına GPU aktivitesi hesabı gerektirir: hangi workload kernel başlattı, VRAM tahsis etti, veri taşıdı ve ne kadar süreyle. Chargeback bunun üzerinden koşar.\n- **Verimlilik — silikon *ne kadar iyi* kullanıldı?** SM’ler gerçekten doluydu mu, tensor core’lar aktif miydi, darboğaz bellek bant genişliği miydi? Bu performans sorusudur. Bir ekibin harcamasının *haklı* olup olmadığını söyler, ama *kimin* harcaması olduğunu söyleyemez.\n\nBunlar iki farklı ölçüm düzlemine eşlenir ve aşağıdaki her şeyi örgütleyen kural şu: **eBPF atfetmeyi cevaplar; GPU’nun kendi sayaçları verimliliği cevaplar. Hiçbiri diğerinin yerine geçmez.**\n\n## eBPF’in görebildiği: kontrol düzlemi\n\neBPF kernel’de çalışır ve kprobe’lara, uprobe’lara, tracepoint’lere ve syscall’lara takılır. Okunacak bir GPU-kullanımı tracepoint’i yok — ama GPU *işinin talep edildiği* iki yüzey var ve ikisi de kernel’den görünür:\n\n- **Driver ioctl sınırı.** Her parça GPU işi — kernel gönderimi, bellek tahsisi, senkronizasyon — sonunda `/dev/nvidiactl`’a ve `/dev/nvidia0…N`’e bir `ioctl()` olur. Driver’ın giriş noktalarındaki (`nvidia_unlocked_ioctl`, `nvidia_open`) bir kprobe o trafiği görür. (Kapalı driver tarihsel olarak yalnızca tek bir tracepoint, donanım hata olayları için `nvidia:nvidia_dev_xid`, sunar; geri kalan her şey kprobe’lanır.)\n- **CUDA kütüphane sınırı.** `libcuda.so` / `libcudart.so` üzerindeki bir uprobe API’nin kendisini izler: `cuLaunchKernel`, `cuMemAlloc`, `cuMemcpyHtoD`, `cuStreamSynchronize` ve dostları. Bir giriş uprobe’unu bir dönüş uretprobe’uyla eşlemek her çağrının süresini ölçer.\n\nBunun *atfetme* olmasının nedeni, eBPF’in her hook noktasında çağıran PID/TGID’yi ve cgroup’u native olarak okumasıdır — `bpf_get_current_pid_tgid` ve cgroup id tam orada. Bu size **PID → cgroup → pod → namespace → ekip** zincirini verir, uygulama değişikliği olmadan ve NVIDIA’nın işbirliği olmadan. Pod başına şunları alırsınız: kernel-başlatma sayıları, başlatma boyutları, VRAM tahsis boyutları, memcpy hacmi ve yönü, çağrı zamanlaması. Bu, tamamen sınırın kernel tarafından türetilen, gerçek ve savunulabilir bir chargeback sinyalidir.\n\nBuradaki ekosistem genç ama gerçek. Primitifler bugün işliyor — CUDA kütüphanelerini uprobe’layan ve nvidia ioctl yolunu kprobe’layan çalışan yazılar ve eğitimler var ve `bpftime` eBPF mantığını GPU olaylarına bağlamayı keşfediyor. Tetragon (Cilium), Kubernetes pod kimliğiyle jenerik `process_uprobe`, `process_kprobe` ve ioctl izleme sevk eder, yani CUDA sembollerine ya da ioctl yoluna *yöneltilebilir* — ama bunun sevk edilen bir “GPU FinOps” özelliği değil, yazdığınız bir `TracingPolicy` olduğunu anlayın. Henüz baskın, anahtar-teslim bir eBPF chargeback ürünü yok. Size “tak-çalıştır eBPF GPU FinOps” satan biri, ekosistemin şu an ulaştığından öteye satıyordur.\n\n## eBPF’in göremediği: silikon\n\nDürüst limit bu ve bir sürümde düzeltilecek bir olgunluk problemi değil — verinin nerede yaşadığının fiziği.\n\neBPF **kontrol düzlemini** görür: API çağrıları, ioctl’lar, tahsis boyutları, başlatma sayıları ve izleyebildiğiniz yerde gönderim-tamamlanma zamanlaması. GPU’nun *içini* **görmez**. SM occupancy’sini, tensor-core kullanımını ya da erişilen bellek bant genişliğini okuyamaz, çünkü bunlar *cihazda* yaşayan ve yalnızca NVML / DCGM / CUPTI üzerinden açığa çıkan donanım performans sayaçlarıdır. eBPF’in onlara giden bir yolu yoktur. Bir pod’un on bin kernel başlattığını ve 40 GB VRAM tahsis ettiğini söyleyebilir; o kernel’lerin SM’leri doyurup doyurmadığını ya da onları %90 boş bırakıp bırakmadığını söyleyemez.\n\nİkinci, daha incelikli bir limit var. eBPF’in takılabildiği ioctl sınırında bile, *payload’lar* büyük ölçüde opaktır. Driver’ın komut yapıları (`NV_ESC_*` Resource Manager API’si) karmaşık ve fiilen proprietary. Bir ioctl’ın *olduğunu* görebilirsiniz — komut numarasını, çağıran PID’yi, zamanlamayı — ama keyfi bir RM payload’unun semantik içeriğini decode etmek pratik dışı ve kırılgandır. İşin gerçeğini ve atfetmesini alırsınız, anlamının bedava bir okumasını değil.\n\nCazip bir kapıyı kapatan bir not: NVIDIA’nın açık kernel modülleri (`open-gpu-kernel-modules`, Turing ve sonrası) **kernel arayüz katmanını** açar — modül init’ini, ioctl giriş noktalarını, `NV_ESC_*` komut yüzeyini. Bu, *neyi* hook’layacağınızı anlamanıza gerçekten yardımcı olur. Ama GPU’nun beyni kapalı kalır: Turing+’ta yönetimin çoğu GPU üzerindeki GSP firmware’inde koşar ve o firmware — modüllerin ihtiyaç duyduğu user-mode driver bileşenleriyle birlikte — hâlâ kapalı olarak dağıtılır. Kernel modüllerini açmak, eBPF’e donanım performans sayaçlarını **açmaz**. Cihaz-içi limit değişmemiştir. Açık modüllerin eBPF’e SM occupancy okuttuğunu iddia eden biri, mevcut kanıtlara göre yanılıyordur.\n\n## Hâlâ ihtiyaç duyduğunuz userspace yolu: DCGM\n\neBPF silikonu göremediği için, cevabın verimlilik yarısı userspace’ten gelir ve standart araç, Prometheus için **dcgm-exporter** ile NVIDIA **DCGM** (Data Center GPU Manager)’dır. DCGM, GPU’nun donanım sayaçlarını driver üzerinden okur ve eBPF’in erişemediği alanları açığa çıkarır:\n\n- `DCGM_FI_PROF_SM_ACTIVE` — bir multiprocessor’da en az bir warp’ın aktif olduğu zamanın oranı, hepsinin ortalaması.\n- `DCGM_FI_PROF_SM_OCCUPANCY` — resident warp’ların desteklenen maksimuma oranı: *gerçek* occupancy.\n- `DCGM_FI_PROF_PIPE_TENSOR_ACTIVE` — tensor pipe’ın aktif olduğu döngülerin oranı.\n- `DCGM_FI_PROF_DRAM_ACTIVE` — bir bellek-bant-genişliği vekili.\n- `DCGM_FI_DEV_FB_USED` — gerçekten kullanımda olan framebuffer (VRAM).\n\nBunlar size bir ekibin pahalı tahsisinin ekmeğini kazanıp kazanmadığını söyleyen sayılardır. DCGM onları pod’lara, her GPU UUID’yi onu tutan pod’a eşleyen kubelet **Pod Resources API**’si (`/var/lib/kubelet/pod-resources` adresindeki bir gRPC servisi) üzerinden atfeder.\n\nAma DCGM’in atfetmesinin, tam da eBPF’in yerini hak ettiği yerde sert bir kenarı var: **DCGM cihaz-seviyesi metrikler üretir, bu yüzden tek bir fiziksel GPU’yu paylaşan tüketicileri ayırt edemez.** Time-slicing ya da MPS (Multi-Process Service) altında, bir GPU’yu paylaşan tüm pod’lar *aynı, kopyalanmış* cihaz-seviyesi değerleri alır — `DCGM_FI_DEV_FB_USED` dahil. NVIDIA’nın kendi exporter’ı, time-slicing etkinken metrikleri container’larla ilişkilendirmediğini belgeler; `--kubernetes-virtual-gpus=true` ile her paylaşan pod tüm fiziksel GPU’nun durumunu aynalar. MIG altında atfetme, keyfi pod’lara değil GPU-instance seviyesine kayar. Yani tam da paylaşılan-GPU durumunda — ki bu durum, bütün-GPU tahsisi para israf ettiği *için* var olur — DCGM size kimin ne tükettiğini söyleyemez. eBPF’in PID-seviyesi izlemesi söyleyebilir. İki araç, her birinin en zayıf olduğu dikişte birbirini tamamlar.\n\n(En derin verimlilik profilleme için **CUPTI**, CUDA Profiling Tools Interface var; DCGM’in açığa çıkarmadığı sayaçları okuyabilir — ama Nsight Compute gibi metrik-replay profilleme kernel’leri birden çok kez yeniden çalıştırır ve ağır overhead taşır. Bu bir profilleme-oturumu aracıdır, her zaman açık tenant başına telemetri değil. DCGM düşük-overhead’li, örnekleme-bazlı sürekli yoldur; CUPTI ağır, derin olandır.)\n\n## Kernel-vs-userspace takası, açıkça\n\n| Düzlem | Araç | Görür | Göremez |\n| --- | --- | --- | --- |\n| Kernel (kontrol) | eBPF | PID→pod atfetme, başlatma sayıları, tahsis boyutları, memcpy hacmi, çağrı zamanlaması — time-slicing altında pod başına *dahil* | cihaz-içi SM/tensor occupancy, bellek bant genişliği, ioctl payload içleri |\n| Userspace (cihaz) | DCGM | gerçek SM occupancy, tensor aktivitesi, DRAM aktivitesi, kullanılan VRAM — donanım sayaçlarından | paylaşılan GPU altında (time-slicing/MPS) pod başına atfetme: tüm paylaşanlar aynı değerleri alır |\n\neBPF düşük-overhead’lidir, uygulama değişikliği gerektirmez, vendor işbirliği gerektirmez ve PID/cgroup üzerinden native atfeder — *kim ve ne kadar* için doğru araç odur. DCGM silikonu okur — *ne kadar iyi* için doğru araç odur. Maliyetler de dürüst: uprobe’lar gerçek overhead taşır ve CUDA sembolleri **versiyonludur** (`cuMemAlloc@CUDA_11.0` vs `@CUDA_12.0`), yani probe’lar dinamik sembol çözümü gerektirir ve driver yükseltmeleri boyunca kırılır; CUDA API çağrıları saniyede on binden fazla atabilir, yani handler’lar ucuz kalmalı yoksa ölçtükleri workload’u yavaşlatırlar. DCGM kendi örnekleme overhead’i olan userspace polling’dir ve yalnızca belirli sayaç gruplarının birlikte okunabildiği bir donanım kısıtı taşır.\n\n## İkisine de neden, hangi sırayla ihtiyacınız var\n\nYanlış dönüş, bir aracı seçip ona diğer aracın sorusunu sormaktır — ekipleri DCGM’in cihaz-seviyesi sayıları üzerinden faturalandırıp bir GPU’yu paylaşan herkesi sessizce fazla yüklemek, ya da eBPF’in başlatma sayılarına verimlilik vekili olarak güvenip meşgul görünen bir workload’un iyi kullanıldığı sonucuna varmak. Gerçekten işleyen sıra:\n\n```\n1. eBPF: GPU işini pod/ekibe atfet           (kim, ne kadar — fatura)\n2. DCGM: cihaz-içi verimliliği ölç           (ne kadar iyi — haklı mıydı)\n3. Onları pod kimliğinde birleştir            (ekibin harcaması VE verimliliği)\n```\n\nAtfetme önce, çünkü finansın gerçekten sorduğu soru o ve mevcut yığınınızın hiç cevaplayamadığı o. Verimlilik sonra, çünkü faturayı bir karara dönüştürür — pahalı bir tahsisi %4 occupancy’de tutan bir ekip, artık *görebileceğiniz* bir [ileride-lazım-olur kapasite vergisi](/tr/journal/ileride-lazim-olur-kodunun-faturasi) ödüyordur; oysa önceden GPU, kimsenin açamadığı düz bir kalemdi. Pod kimliğinde birleşince, nihayet her diğer kaynak için sahip olduğunuz şeye sahip olursunuz: atfedilmiş harcama, üzerinde harekete geçecek verimlilik bağlamıyla. İki düzlem gerektirmesinin nedeni, görünmez olmasının nedeniyle aynıdır — silikon muhasebe düzleminde hiç yaşamadı ve tek bir probe bile sınırı kat etmiyor.\n\n---\n\n*İlgili:* [DCGM exporter belgeleri](https://docs.nvidia.com/datacenter/dcgm/latest/installation/install-dcgm-exporter.html) pod-atfetme yolunu, [DCGM özellik genel bakışı](https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html) ise profilleme alanlarını tanımlar; bunun yaslandığı eBPF primitifleri [ebpf.io](https://ebpf.io/what-is-ebpf/) ve [Tetragon](https://tetragon.io/)’da belgelenmiştir.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "tags": [
        "ebpf",
        "gpu",
        "finops",
        "observability",
        "kubernetes",
        "System"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/dead-letter-kuyrugunu-guvenle-replay-etmek/",
      "url": "https://sade.dev/tr/notes/dead-letter-kuyrugunu-guvenle-replay-etmek/",
      "title": "Dead-Letter Kuyruğunu Daha Kötü Yapmadan Replay Etmek",
      "summary": "Bir dead-letter kuyruğundan mesaj çıkarmak byte taşımak değil, canlı trafiği yeniden işlemektir. Naif bir replay, gerçek bir nedenle ölmüş mesajlarla kuyruğu yeniden zehirler ve zaten yarısı çalışmış yan etkileri ikinci kez tetikler. Güvenli şekil sıralıdır: envelope'u sıfırla ama mesaj id'sini ve trace id'yi koru, kesintinin imzasına göre seç, dry-run yap, sandbox'a yönlendir, production en sona kalsın.",
      "content_html": "<p>Aşağı akıştaki bir API yirmi dakika düştü. Dört bin mesaj retry’larını tüketip dead-letter kuyruğuna düştü. API artık geri geldi, mesajlar hâlâ geçerli ve bariz hamle onları geldikleri yere geri koymak. Broker konsoluna uzanıp <code>orders.dlq</code>’dan <code>orders</code>’a mesaj taşımaya başlıyorsun.</p>\n<p>Orada dur. Bir dead-letter kuyruğu bir bekleme alanıdır, mezarlık değil — ama mesajları oradan çıkarmak yüklü bir operasyondur ve “sadece geri taşı”, atlatılmış bir kesintiyi ikinci bir olaya çevirmenin yoludur.</p>\n<h2 id=\"naif-bir-replayin-işi-daha-kötü-yapmasının-iki-yolu\">Naif bir replay’in işi daha kötü yapmasının iki yolu</h2>\n<p><strong>Kuyruğu yeniden zehirler.</strong> DLQ’n nadiren sadece kesintinin kurbanlarını tutar. Aralarına gerçek bir nedenle ölen mesajlar karışmıştır — bozuk bir payload, bir bug, yönlendirilemeyen bir URN — ve <em>onlar</em> replay ettiğin an yine başarısız olacak. Replay’in her şeyi körlemesine geri kürelerse, gerçekten bozuk mesajlar başarısız olur, tekrar DLQ’ya düşer, sen onları yine replay edersin. CPU yiyen, logları dolduran ve aslında başarılı olacak mesajları gömen bir döngü.</p>\n<p><strong>Yan etkileri yeniden tetikler.</strong> Üç denemeden sonra dead-letter’a düşen bir mesaj her seferinde <em>kısmen</em> çalışmış olabilir. Kart çekildi ama onay e-postası patladı; mesaj başarısız oldu, retry oldu, dead-letter’a düştü. Onu naifçe replay et, handler baştan sona yine çalışır — şimdi nihayet gönderdiği e-postanın üstüne ikinci bir tahsilat. Kuyruk bir mesaj taşıdı; müşterin iki kez faturalandı.</p>\n<p>İki tuzak da replay’i “byte’ları geri taşı” sanmaktan gelir. Öyle değil. “<strong>Bir mesajı sıfırla ve güvenle yeniden işle</strong>” — ve bu kelimelerin her biri bir emniyet kilidi.</p>\n<h2 id=\"sıfırla-ama-kimliği-koru\">Sıfırla, ama kimliği koru</h2>\n<p>Dead-letter’a düşmüş bir mesaj, nasıl öldüğünü anlatan ek bir blok taşır — neden, orijinal kuyruk, deneme sayısı. Onu replay etmek için <strong>sıfırlaman</strong> gerekir: o bloğu düşür ve deneme sayacını sıfıra geri al ki taze bir retry bütçesi alsın, anında yeniden tükenmesin.</p>\n<p>Ama reset, <em>yeni</em> ile aynı şey değil. Mesajı tanımlayan her şeyi koru — id’sini, payload’ını ve hepsinden önemlisi korelasyon kimliğini (<code>trace_id</code>). İki nedenle:</p>\n<ul>\n<li>İzleyemediğin replay edilmiş bir mesaj operasyonel bir kör nokta. <code>trace_id</code>’yi koru, replay orijinal hatayla aynı trace’te görünsün.</li>\n<li>Korunan mesaj id’si, replay’i <strong>retry’a güvenli</strong> kılan şey. Tüketicilerin <a href=\"/tr/notes/idempotency-ayni-mesaj-iki-kez/\">idempotent</a> ise — mesaj id’si üzerinden dedupe ediyorsa — zaten yarı başarılı olmuş bir mesajı replay etmek dedupe tarafından yakalanır, yeniden çalıştırılmaz. Idempotency ve replay ortaktır: idempotency, bir kuyruğu nefesini tutmadan replay etmeni sağlayan şeydir.</li>\n</ul>\n<h2 id=\"önce-productiona-doğrultma\">Önce production’a doğrultma</h2>\n<p>Reset edilmiş ve idempotent olsa bile, bir replay canlı bir sisteme yazmadır. Güveni aşamalarla kazan:</p>\n<ul>\n<li><strong>Dry-run.</strong> DLQ’yu oku ve neyin replay <em>edileceğini</em> raporla — sayılar, hedefler, nedenler — ve her mesajı dokunmadan geri koy. Tek el atmadan patlama yarıçapını öğrenirsin.</li>\n<li><strong>Sandbox.</strong> Replay’i, tüketicilerinin dış yan etkileri stub’lanmış production dışı bir kuyruğa yönlendir. Mesajlar gerçek handler mantığından akar; kimse faturalandırılmaz. “İyi” mesajların gerçekten iyi olduğunu burada öğrenirsin.</li>\n<li><strong>Sonra production</strong>, ilk ikisi sıkıcı hâle geldiğinde.</li>\n</ul>\n<p>Sıra, herhangi bir tek adımdan daha önemli. Mesajlar “iyi görünüyor” diye doğrudan production’a replay etmek, ilk paragraftaki broker konsolunda onları taşıyan güvenin aynısıdır.</p>\n<h2 id=\"select-et-saçma\">Select et; saçma</h2>\n<p>Kesintinin kurbanları ortak bir imza taşır — bir zaman penceresi, bir hata <code>reason</code>’ı, bir URN. <em>O kümeyi</em> replay et, tüm kuyruğu değil. Reason’a göre seçmek, “timeout’tan ölen 4.000’i replay et” ile “4.000 timeout artı yine geri gelecek 30 gerçekten zehirli mesajı replay et” arasındaki farktır. Zehirli mesajlar replay etme sorunun değil; <em>düzeltme</em> sorunun ve onları görebildiğin DLQ’da kalmalılar.</p>\n<h2 id=\"resetleyemeyeceğin-yan-etki\">Resetleyemeyeceğin yan etki</h2>\n<p>İşte dürüst olmak gerekirse zor kısım. Reset envelope’u temizler; idempotency birebir replay’leri dedupe eder; sandbox sen test ederken production’ı korur. Ama production’a <em>kasıtlı</em> bir replay — gerçek olanı, aslında istediğin — handler’ı çalıştırır ve handler ne yapıyorsa onu yapar: tahsilat çeker, e-posta atar, üçüncü tarafı çağırır. Idempotency bir <em>kopyayı</em> durdurur; <em>amaçlanan</em> yeniden işlemenin yan etkileriyle birlikte işini yapmasını durdurmaz.</p>\n<p>Temiz çözüm, bir handler’a replay çalıştırdığını bildirip zaten olmuş dış etkileri atlamasını sağlamak — veritabanı yazmasını yeniden yap, ama e-postayı yeniden gönderme. İş şu ki <em>o bayrağı nereye koyacaksın</em>: donmuş bir mesaj envelope’unda ekleyecek yer yok. Cevap, dağıtık izlemenin aynı kısıt için kullandığıyla aynı — onu <strong>bant dışında</strong> taşı, mesajın yanında giden ve runtime’ın handler’a sunduğu bir transport header olarak. Handler’ın kontrol edebileceği, yalnızca replay edilmiş mesajlarda set edilen, normal yolda hiçbir maliyeti olmayan bir <code>replay-bypass</code> işareti. En zahmetli parça ve en sona bırakılacak doğru parça — reset, dry-run, sandbox ve select replay’i rutin olacak kadar güvenli hâle getirdikten sonra.</p>\n<p>Bir dead-letter kuyruğu, çeyreğin en kötü gününde sistemindeki en faydalı şeydir. Onu replay etmeyi olduğu şey gibi ele al — canlı trafiği yeniden işlemek — ve faydalı kalır. Byte taşımak gibi ele al ve DLQ’nun devam filmi gelir.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://babelqueue.com/docs/spec/1.x/redrive-and-replay\">BabelQueue redrive-and-replay spec’i</a> bu sıfırla-ve-yeniden-işle şeklini standartlaştırır; <a href=\"https://github.com/BabelQueue/babelqueue-examples/tree/main/dlq-redrive\"><code>dlq-redrive</code> örneği</a> ise onun çalıştırılabilir bir hâlidir.</p>",
      "content_text": "Aşağı akıştaki bir API yirmi dakika düştü. Dört bin mesaj retry'larını tüketip dead-letter kuyruğuna düştü. API artık geri geldi, mesajlar hâlâ geçerli ve bariz hamle onları geldikleri yere geri koymak. Broker konsoluna uzanıp `orders.dlq`'dan `orders`'a mesaj taşımaya başlıyorsun.\n\nOrada dur. Bir dead-letter kuyruğu bir bekleme alanıdır, mezarlık değil — ama mesajları oradan çıkarmak yüklü bir operasyondur ve \"sadece geri taşı\", atlatılmış bir kesintiyi ikinci bir olaya çevirmenin yoludur.\n\n## Naif bir replay'in işi daha kötü yapmasının iki yolu\n\n**Kuyruğu yeniden zehirler.** DLQ'n nadiren sadece kesintinin kurbanlarını tutar. Aralarına gerçek bir nedenle ölen mesajlar karışmıştır — bozuk bir payload, bir bug, yönlendirilemeyen bir URN — ve *onlar* replay ettiğin an yine başarısız olacak. Replay'in her şeyi körlemesine geri kürelerse, gerçekten bozuk mesajlar başarısız olur, tekrar DLQ'ya düşer, sen onları yine replay edersin. CPU yiyen, logları dolduran ve aslında başarılı olacak mesajları gömen bir döngü.\n\n**Yan etkileri yeniden tetikler.** Üç denemeden sonra dead-letter'a düşen bir mesaj her seferinde *kısmen* çalışmış olabilir. Kart çekildi ama onay e-postası patladı; mesaj başarısız oldu, retry oldu, dead-letter'a düştü. Onu naifçe replay et, handler baştan sona yine çalışır — şimdi nihayet gönderdiği e-postanın üstüne ikinci bir tahsilat. Kuyruk bir mesaj taşıdı; müşterin iki kez faturalandı.\n\nİki tuzak da replay'i \"byte'ları geri taşı\" sanmaktan gelir. Öyle değil. \"**Bir mesajı sıfırla ve güvenle yeniden işle**\" — ve bu kelimelerin her biri bir emniyet kilidi.\n\n## Sıfırla, ama kimliği koru\n\nDead-letter'a düşmüş bir mesaj, nasıl öldüğünü anlatan ek bir blok taşır — neden, orijinal kuyruk, deneme sayısı. Onu replay etmek için **sıfırlaman** gerekir: o bloğu düşür ve deneme sayacını sıfıra geri al ki taze bir retry bütçesi alsın, anında yeniden tükenmesin.\n\nAma reset, *yeni* ile aynı şey değil. Mesajı tanımlayan her şeyi koru — id'sini, payload'ını ve hepsinden önemlisi korelasyon kimliğini (`trace_id`). İki nedenle:\n\n- İzleyemediğin replay edilmiş bir mesaj operasyonel bir kör nokta. `trace_id`'yi koru, replay orijinal hatayla aynı trace'te görünsün.\n- Korunan mesaj id'si, replay'i **retry'a güvenli** kılan şey. Tüketicilerin [idempotent](/tr/notes/idempotency-ayni-mesaj-iki-kez) ise — mesaj id'si üzerinden dedupe ediyorsa — zaten yarı başarılı olmuş bir mesajı replay etmek dedupe tarafından yakalanır, yeniden çalıştırılmaz. Idempotency ve replay ortaktır: idempotency, bir kuyruğu nefesini tutmadan replay etmeni sağlayan şeydir.\n\n## Önce production'a doğrultma\n\nReset edilmiş ve idempotent olsa bile, bir replay canlı bir sisteme yazmadır. Güveni aşamalarla kazan:\n\n- **Dry-run.** DLQ'yu oku ve neyin replay *edileceğini* raporla — sayılar, hedefler, nedenler — ve her mesajı dokunmadan geri koy. Tek el atmadan patlama yarıçapını öğrenirsin.\n- **Sandbox.** Replay'i, tüketicilerinin dış yan etkileri stub'lanmış production dışı bir kuyruğa yönlendir. Mesajlar gerçek handler mantığından akar; kimse faturalandırılmaz. \"İyi\" mesajların gerçekten iyi olduğunu burada öğrenirsin.\n- **Sonra production**, ilk ikisi sıkıcı hâle geldiğinde.\n\nSıra, herhangi bir tek adımdan daha önemli. Mesajlar \"iyi görünüyor\" diye doğrudan production'a replay etmek, ilk paragraftaki broker konsolunda onları taşıyan güvenin aynısıdır.\n\n## Select et; saçma\n\nKesintinin kurbanları ortak bir imza taşır — bir zaman penceresi, bir hata `reason`'ı, bir URN. *O kümeyi* replay et, tüm kuyruğu değil. Reason'a göre seçmek, \"timeout'tan ölen 4.000'i replay et\" ile \"4.000 timeout artı yine geri gelecek 30 gerçekten zehirli mesajı replay et\" arasındaki farktır. Zehirli mesajlar replay etme sorunun değil; *düzeltme* sorunun ve onları görebildiğin DLQ'da kalmalılar.\n\n## Resetleyemeyeceğin yan etki\n\nİşte dürüst olmak gerekirse zor kısım. Reset envelope'u temizler; idempotency birebir replay'leri dedupe eder; sandbox sen test ederken production'ı korur. Ama production'a *kasıtlı* bir replay — gerçek olanı, aslında istediğin — handler'ı çalıştırır ve handler ne yapıyorsa onu yapar: tahsilat çeker, e-posta atar, üçüncü tarafı çağırır. Idempotency bir *kopyayı* durdurur; *amaçlanan* yeniden işlemenin yan etkileriyle birlikte işini yapmasını durdurmaz.\n\nTemiz çözüm, bir handler'a replay çalıştırdığını bildirip zaten olmuş dış etkileri atlamasını sağlamak — veritabanı yazmasını yeniden yap, ama e-postayı yeniden gönderme. İş şu ki *o bayrağı nereye koyacaksın*: donmuş bir mesaj envelope'unda ekleyecek yer yok. Cevap, dağıtık izlemenin aynı kısıt için kullandığıyla aynı — onu **bant dışında** taşı, mesajın yanında giden ve runtime'ın handler'a sunduğu bir transport header olarak. Handler'ın kontrol edebileceği, yalnızca replay edilmiş mesajlarda set edilen, normal yolda hiçbir maliyeti olmayan bir `replay-bypass` işareti. En zahmetli parça ve en sona bırakılacak doğru parça — reset, dry-run, sandbox ve select replay'i rutin olacak kadar güvenli hâle getirdikten sonra.\n\nBir dead-letter kuyruğu, çeyreğin en kötü gününde sistemindeki en faydalı şeydir. Onu replay etmeyi olduğu şey gibi ele al — canlı trafiği yeniden işlemek — ve faydalı kalır. Byte taşımak gibi ele al ve DLQ'nun devam filmi gelir.\n\n---\n\n*İlgili:* [BabelQueue redrive-and-replay spec'i](https://babelqueue.com/docs/spec/1.x/redrive-and-replay) bu sıfırla-ve-yeniden-işle şeklini standartlaştırır; [`dlq-redrive` örneği](https://github.com/BabelQueue/babelqueue-examples/tree/main/dlq-redrive) ise onun çalıştırılabilir bir hâlidir.",
      "date_published": "2026-06-19T00:00:00.000Z",
      "tags": [
        "queue",
        "dead-letter-queue",
        "reliability",
        "operations",
        "architecture",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/systems/polyglot-kuyrukta-dagitik-izleme/",
      "url": "https://sade.dev/tr/systems/polyglot-kuyrukta-dagitik-izleme/",
      "title": "Polyglot Bir Kuyrukta Dağıtık İzleme",
      "summary": "Hüner enstrümantasyonda değil. Tel üzerinde zaten taşınan UUID bir trace_id, tam olarak 16 byte’lık bir OpenTelemetry TraceID’dir — onu yeniden kullan, her hop tek bir trace’i paylaşsın, tel hiç değişmesin. Bunun kazandırdığı ve bilinçle ikinci faza bıraktığı, sonradan kapanan hop’lar-arası span ebeveynliği.",
      "content_html": "<p>Bir mesaj bir PHP servisi tarafından üretiliyor, bir kuyruğa düşüyor, bir Go worker’ı onu tüketiyor, o da bir devam mesajı yayınlıyor ve onu bir Python servisi işliyor. Dört hop, üç dil, aralarda bir ya da daha fazla broker. O akış yavaşladığında ya da on binde bir mesaj öldüğünde soru hep aynı: <strong>bu mesaj gerçekte nereye gitti ve her adımda ne oldu?</strong></p>\n<p>Çoğu polyglot kuyruk sistemi için dürüst cevap: kimse bilmiyor. Log’larda bir korelasyon kimliğin var — üç dilde de loglamayı hatırladıysan — ve farklı host’lardaki üç log akışında <code>grep</code> çekecek sabrın var. Sahip olmadığın şey bir resim. “PHP’de 2ms’de üretildi → Redis’te 40ms bekledi → Go’da 210ms’de işlendi, iki kez retry oldu, DLQ’landı”yı <strong>tek bir bağlı şey</strong> olarak göremiyorsun.</p>\n<p>İşte o resim bir <strong>dağıtık trace</strong>’tir ve onu çizmenin standart yolu OpenTelemetry’dir. Bu yazı, onu tel formatı <strong>donmuş</strong> ve çekirdekleri <strong>sıfır bağımlılık</strong> taşıyan bir mesaj standardına eklemekle ilgili — bu iki kısıt, bir arada, en bariz yaklaşımı yasadışı kılıp daha ilginç olanı zorunlu hâle getiriyor.</p>\n<h2 id=\"tek-kuralı-baştan-söyleyeyim\">Tek kuralı baştan söyleyeyim</h2>\n<p>Her şeyden önce şunu netleştireyim: <strong>envelope’a bir alan ekleme.</strong></p>\n<p>Bir mesaj veriyolu boyunca dağıtık izleme istediğinde ilk içgüdü, bir W3C <code>traceparent</code> taşımaktır — bir trace id’yi, bir span id’yi ve flag’leri kodlayan o standart 55 karakterlik string. HTTP tam olarak bunu bir header’da yapar. Bu içgüdü HTTP için doğru, burada yanlış; çünkü envelope donmuş bir kontrattır. Her dildeki her SDK, byte-bazında aynı şekli yayınlar: <code>job</code>, <code>trace_id</code>, <code>data</code>, <code>meta</code>, <code>attempts</code>. Bir <code>traceparent</code> alanı eklemek — opsiyonel olsa bile — o şekli değiştirir, bu da bir versiyon artışı, bu da altı dil implementasyonu ve her broker bağlaması boyunca koordine edilen bir tel değişikliği demektir. <em>Opsiyonel gözlemlenebilirlik</em> olması gereken bir özellik için bu saçma bir bedel.</p>\n<p>Yani kural, kısıtın kendisi: izlemeyi <strong>tele dokunmadan</strong> çöz. Aşağıdaki her şey bunu ciddiye almaktan çıkıyor.</p>\n<h2 id=\"kimsenin-kullanmadığı-içgörü\">Kimsenin kullanmadığı içgörü</h2>\n<p>Envelope’un sana bedavaya verdiği şey şu. <code>trace_id</code> alanı bir korelasyon kimliği — üretim anında basılan ve <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">her hop boyunca değişmeden taşınan</a> bir UUID. Zaten tel üzerinde, zaten taşınıyor, zaten tüm akışı birbirine bağlayan o tek değer.</p>\n<p>Şimdi OpenTelemetry’nin bir trace’i birbirine bağlamak için kullandığı şeye bak: bir <strong>TraceID</strong>. Spesifikasyonda bir TraceID tam olarak <strong>16 byte</strong>’tır.</p>\n<p>Bir UUID tam olarak 16 byte’tır.</p>\n<p>Bütün hüner bu. UUID bir <code>trace_id</code>, bir OTel TraceID’ye birebir eşlenir — tireleri at, 32 hex karakteri 16 byte’lık kimlik olarak oku, bitti. (UUID <em>olmayan</em> bir <code>trace_id</code> — diyelim BabelQueue dışı bir üreticiden geldi — SHA-256 ile deterministik olarak 16 byte’a hash’lenir.) Dolayısıyla aynı <code>trace_id</code>’yi paylaşan her hop, <strong>aynı</strong> OTel TraceID’yi türetir — bir anlaşma protokolü ve yeni bir alan olmadan. Zaten taşıdığın korelasyon kimliği <em>dağıtık trace’in ta kendisidir.</em></p>\n<p>Tasarım da kendini yazıyor:</p>\n<ul>\n<li><strong>Tüketici</strong> tarafında handler’ı sar. Çalışmadan önce <code>process &lt;urn&gt;</code> adında bir span başlat, ama o span’i mesajın <code>trace_id</code>’sinden türetilen trace’in içine zorla. Onu messaging convention’larıyla etiketle — <code>messaging.system</code>, <code>messaging.destination.name</code>, <code>messaging.message.id</code> ve <code>trace_id</code>’nin kendisine eşitlenen <code>messaging.message.conversation_id</code> — handler’ı çalıştır ve herhangi bir exception’ı span’in hata durumu olarak kaydet. Runtime’ın retry / dead-letter davranışı el değmeden kalır; span sadece onu gözlemler.</li>\n<li><strong>Üretici</strong> tarafında aynası. Bir <code>publish &lt;urn&gt;</code> span’i aç, <em>onun</em> trace id’sini al, tekrar bir UUID’ye biçimlendir ve envelope’u kurarken mesajın <code>trace_id</code>’sine bas. Aşağı akıştaki tüketici, TraceID’sini aynı <code>trace_id</code>’den türeterek aynı trace’e iner.</li>\n</ul>\n<p>Jaeger, Tempo, Honeycomb ya da Datadog için bir <code>TracerProvider</code> bağla; akış üç dil boyunca tek bir şelale olarak belirir, her hop’un zamanlamasıyla ve retry/hata işaretleriyle. Bağlama, hiçbir şey değişmez — tamamen opt-in.</p>\n<h2 id=\"mekanizma-ve-bir-hayalet\">Mekanizma ve bir hayalet</h2>\n<p>Net olmaya değer bir ayrıntı var, çünkü tasarımın hem zekice hem de sınırlı olduğu yer orası.</p>\n<p>OpenTelemetry’de bir span’i <em>belirli bir trace’in içinde</em> başlatmak için ona bir <strong>uzak ebeveyn</strong> (remote parent) verirsin — istediğin trace id’yi taşıyan bir span context. Ama bir span context yalnızca <em>hem</em> bir trace id <em>hem de</em> bir span id’si varsa geçerlidir. Trace id’miz var (<code>trace_id</code>’den); span id’miz <strong>yok</strong>, çünkü bilinçle bir tane taşımadık. Bu yüzden <code>trace_id</code>’yi hash’leyerek deterministik, sıfır-olmayan bir span id sentezliyoruz. Ebeveyn geçerli, tüketici span’i doğru trace’e iniyor — ama ebeveyni hiç var olmamış bir span’i gösteriyor. Bir <strong>hayalet</strong>.</p>\n<p>Yukarıdaki tasarımın dürüst sınırı bu ve açıkça söylemeye değer. Hop’lar-arası span’lerin hepsi <strong>tek bir trace</strong>’i paylaşır — bir mesajın hayatının her adımını bir arada, zamanlanmış, hatalar işaretlenmiş görebilirsin. Tek başına bunun <strong>vermediği</strong> şey, hop’lar <em>arasında</em> birebir ebeveyn-çocuk bağı: tüketicinin span’i üreticinin span’inin çocuğu olarak bağlanmaz, çünkü üreticinin gerçek span id’si tel boyunca hiç taşınmadı. Tek bir process içinde hiyerarşi doğrudur; kuyruğun ötesinde tek trace altında düzdür.</p>\n<p>Gerçek hop’lar-arası ebeveyn-çocuğu geri almak, bir span id taşımak demek, o da bir <code>traceparent</code> demek — ve envelope donmuş olduğundan, o <code>traceparent</code> <strong>bant dışında</strong>, her broker bağlamasının <code>trace_id</code> için zaten kullandığı yuvanın yanında bir transport header olarak gitmek zorunda: SQS, Pulsar ve Kafka’da bir <code>bq-trace-id</code> alanı; RabbitMQ, Azure Service Bus ve ActiveMQ Artemis’te native correlation-id yuvası; hiç per-message metadata taşımayan Redis’te ise envelope gövdesinin kendisinden başka bir şey yok. Bu gerçek bir özellik, ama farklı ölçekte bir iş: her SDK’daki her transport bağlamasına dokunur. Yani bilinçli bir <strong>ikinci faz</strong> olarak bırakıldı — ve o faz bu yazıdan iki gün sonra kapandı: ADR-0028 ile <code>traceparent</code> transport-header taşıması 21 Haziran 2026’da altı SDK çekirdeğinin hepsine v0.2 olarak indi. Teslim edilen bir <code>traceparent</code> artık tüketici span’ini üreticinin span’inin gerçek çocuğuna yükseltiyor; taşınamadığı yerde — kalan boşluk PHP’nin Kafka, Pulsar ve STOMP üreticileri — yukarıda anlatılan v0.1 <code>trace_id</code> korelasyonuna temiz biçimde düşüyor. Sıralamanın kendisi zaten meseleydi: yukarıdaki birinci-faz tasarımı korelasyonu, hop başına zamanlamayı ve hata/retry görünürlüğünü — yani %90’ı — sıfır tel değişikliğiyle ve dil başına birkaç yüz satırla teslim eder ve v0.2’nin üzerine oturduğu zemin olarak kalır. %90’ı önce, son %10’u daha büyük bir işin ardına bırakarak göndermek doğru sıraydı — tıpkı <a href=\"/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari/\">pahalı ölçekleme adımlarından önce ucuzları yaptığın gibi</a>.</p>\n<h2 id=\"tek-semantik-altı-paketleme-deyimi\">Tek semantik, altı paketleme deyimi</h2>\n<p><strong>Çekirdeğin bağımlılıksız kalması</strong> kısıtının bir sonucu var: OpenTelemetry kodu, OTel API’sini import etmek zorunda olduğu her yerde çekirdekte yaşayamaz. Bu yüzden altı dilin beşinde çekirdeğin <em>yanında</em> yaşar, yalnızca opt-in ettiğinde erişilir — ve “çekirdeğin yanında, opsiyonel” her ekosistemde farklı yazılır. (.NET istisna; nedenine birazdan geliyoruz.) Altısında da semantik aynı; paketleme her dilin karakterini gösterdiği yer:</p>\n<ul>\n<li><strong>Go</strong> — ayrı bir modül (<code>babelqueue-go/otel</code>, kendi <code>go.mod</code>’u), tıpkı transport alt-modülleri gibi. Çekirdek modül OTel bağımlılığını hiç görmez.</li>\n<li><strong>Python</strong> — bir <code>[otel]</code> extra’sı. <code>pip install babelqueue[otel]</code>, <code>opentelemetry-api</code>’yi getirir; modül onu import eder, yani yalnızca sen istediğinde import edilebilir. Buradaki bir TraceID byte değil, 128-bit bir <code>int</code>’tir — aynı değer, farklı biçim.</li>\n<li><strong>Node</strong> — bir subpath export, <code>@babelqueue/core/otel</code>, <code>@opentelemetry/api</code> ile <em>opsiyonel</em> bir peer dependency olarak. Kritik nokta: izleme kodu paket kökünden <strong>yeniden export edilmez</strong>: edilseydi, <code>@babelqueue/core</code>’u import etmek OTel import’unu erkenden yükler ve peer’i kurmayan herkes için kırılırdı. Subpath, ana girişi gerçekten bağımlılıksız tutar.</li>\n<li><strong>Java</strong> — <code>opentelemetry-api</code> üzerinde <code>optional</code> bir Maven bağımlılığı. Optional bağımlılıklar transitive değildir, yani izleme sınıflarına hiç dokunmayan bir tüketici OTel’i classpath’ine hiç çekmez.</li>\n<li><strong>.NET</strong> — ilginç olanı. .NET’te idiomatik izleme primitifi <code>System.Diagnostics.ActivitySource</code>’tur, <strong>base class library</strong> içinde yaşar — ve OpenTelemetry .NET’in üzerine kurulduğu <em>tam olarak</em> odur. Yani .NET modülünün <strong>hiçbir bağımlılığı yok</strong> — çekirdeğin yanında değil, <em>içinde</em> durabilmesinin sebebi de bu (<code>BabelQueue.Core / Telemetry</code>): <code>Activity</code> nesneleri yayınlar, tüketicinin OTel boru hattı onları <code>AddSource(&quot;BabelQueue&quot;)</code> çağırarak toplar. Çekirdek, bağımlılığı izole ederek değil, bağımlılığa sahip olmayarak sıfır-dep kalır.</li>\n<li><strong>PHP</strong> — bir Composer <code>suggest</code> artı bir dev gereksinimi, mevcut opsiyonel yardımcıları aynalayarak. İzleme namespace’i orada; <code>open-telemetry/api</code> yalnızca kullanırsan gerekir.</li>\n</ul>\n<p>Altı deyim, hepsinde tutulan tek kural: <strong>gözlemlenebilirliği kullanmak tüketicinin yaptığı bir seçimdir, asla çekirdeğin kestiği bir vergi değil.</strong></p>\n<h2 id=\"gerçekte-ne-elde-ediyorsun\">Gerçekte ne elde ediyorsun?</h2>\n<p>Mekanizmayı bir kenara bırak, kazanç tarif etmesi küçük, sahip olması büyük. Mevcut izleme backend’inde, eskiden üç log samanlığında bir iğne olan bir mesaj tek bir şelaleye dönüşür: onu hangi servis üretti, broker’da ne kadar bekledi, her tüketici ne kadar sürdü, retry oldu mu, DLQ’landı mı — zaten ortalıkta dolaştırdıkları bir UUID dışında hiçbir konuda anlaşmasını gerektirmediğin diller ve broker’lar boyunca.</p>\n<p>Ve bunun tel üzerindeki maliyeti <strong>sıfır</strong>. İzleme var olmadan önce giden envelope ile onunla birlikte giden envelope byte-byte aynı. Trace baştan beri <code>trace_id</code>’nin içinde saklıydı; bütün iş, bir UUID ile bir TraceID’nin aynı on altı byte olduğunu fark etmekti.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://babelqueue.com/docs/spec/1.x/observability\">BabelQueue observability spec’i</a> bu tasarımı bir standart olarak yazıya döker; onu uygulayan SDK’lar — PHP, Go, Python, Node, Java ve .NET boyunca — <a href=\"https://github.com/BabelQueue\">BabelQueue ekosisteminde</a> yaşar.</p>",
      "content_text": "Bir mesaj bir PHP servisi tarafından üretiliyor, bir kuyruğa düşüyor, bir Go worker’ı onu tüketiyor, o da bir devam mesajı yayınlıyor ve onu bir Python servisi işliyor. Dört hop, üç dil, aralarda bir ya da daha fazla broker. O akış yavaşladığında ya da on binde bir mesaj öldüğünde soru hep aynı: **bu mesaj gerçekte nereye gitti ve her adımda ne oldu?**\n\nÇoğu polyglot kuyruk sistemi için dürüst cevap: kimse bilmiyor. Log’larda bir korelasyon kimliğin var — üç dilde de loglamayı hatırladıysan — ve farklı host’lardaki üç log akışında `grep` çekecek sabrın var. Sahip olmadığın şey bir resim. \"PHP’de 2ms’de üretildi → Redis’te 40ms bekledi → Go’da 210ms’de işlendi, iki kez retry oldu, DLQ’landı\"yı **tek bir bağlı şey** olarak göremiyorsun.\n\nİşte o resim bir **dağıtık trace**’tir ve onu çizmenin standart yolu OpenTelemetry’dir. Bu yazı, onu tel formatı **donmuş** ve çekirdekleri **sıfır bağımlılık** taşıyan bir mesaj standardına eklemekle ilgili — bu iki kısıt, bir arada, en bariz yaklaşımı yasadışı kılıp daha ilginç olanı zorunlu hâle getiriyor.\n\n## Tek kuralı baştan söyleyeyim\n\nHer şeyden önce şunu netleştireyim: **envelope’a bir alan ekleme.**\n\nBir mesaj veriyolu boyunca dağıtık izleme istediğinde ilk içgüdü, bir W3C `traceparent` taşımaktır — bir trace id’yi, bir span id’yi ve flag’leri kodlayan o standart 55 karakterlik string. HTTP tam olarak bunu bir header’da yapar. Bu içgüdü HTTP için doğru, burada yanlış; çünkü envelope donmuş bir kontrattır. Her dildeki her SDK, byte-bazında aynı şekli yayınlar: `job`, `trace_id`, `data`, `meta`, `attempts`. Bir `traceparent` alanı eklemek — opsiyonel olsa bile — o şekli değiştirir, bu da bir versiyon artışı, bu da altı dil implementasyonu ve her broker bağlaması boyunca koordine edilen bir tel değişikliği demektir. *Opsiyonel gözlemlenebilirlik* olması gereken bir özellik için bu saçma bir bedel.\n\nYani kural, kısıtın kendisi: izlemeyi **tele dokunmadan** çöz. Aşağıdaki her şey bunu ciddiye almaktan çıkıyor.\n\n## Kimsenin kullanmadığı içgörü\n\nEnvelope’un sana bedavaya verdiği şey şu. `trace_id` alanı bir korelasyon kimliği — üretim anında basılan ve [her hop boyunca değişmeden taşınan](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari) bir UUID. Zaten tel üzerinde, zaten taşınıyor, zaten tüm akışı birbirine bağlayan o tek değer.\n\nŞimdi OpenTelemetry’nin bir trace’i birbirine bağlamak için kullandığı şeye bak: bir **TraceID**. Spesifikasyonda bir TraceID tam olarak **16 byte**’tır.\n\nBir UUID tam olarak 16 byte’tır.\n\nBütün hüner bu. UUID bir `trace_id`, bir OTel TraceID’ye birebir eşlenir — tireleri at, 32 hex karakteri 16 byte’lık kimlik olarak oku, bitti. (UUID *olmayan* bir `trace_id` — diyelim BabelQueue dışı bir üreticiden geldi — SHA-256 ile deterministik olarak 16 byte’a hash’lenir.) Dolayısıyla aynı `trace_id`’yi paylaşan her hop, **aynı** OTel TraceID’yi türetir — bir anlaşma protokolü ve yeni bir alan olmadan. Zaten taşıdığın korelasyon kimliği *dağıtık trace’in ta kendisidir.*\n\nTasarım da kendini yazıyor:\n\n- **Tüketici** tarafında handler’ı sar. Çalışmadan önce `process <urn>` adında bir span başlat, ama o span’i mesajın `trace_id`’sinden türetilen trace’in içine zorla. Onu messaging convention’larıyla etiketle — `messaging.system`, `messaging.destination.name`, `messaging.message.id` ve `trace_id`’nin kendisine eşitlenen `messaging.message.conversation_id` — handler’ı çalıştır ve herhangi bir exception’ı span’in hata durumu olarak kaydet. Runtime’ın retry / dead-letter davranışı el değmeden kalır; span sadece onu gözlemler.\n- **Üretici** tarafında aynası. Bir `publish <urn>` span’i aç, *onun* trace id’sini al, tekrar bir UUID’ye biçimlendir ve envelope’u kurarken mesajın `trace_id`’sine bas. Aşağı akıştaki tüketici, TraceID’sini aynı `trace_id`’den türeterek aynı trace’e iner.\n\nJaeger, Tempo, Honeycomb ya da Datadog için bir `TracerProvider` bağla; akış üç dil boyunca tek bir şelale olarak belirir, her hop’un zamanlamasıyla ve retry/hata işaretleriyle. Bağlama, hiçbir şey değişmez — tamamen opt-in.\n\n## Mekanizma ve bir hayalet\n\nNet olmaya değer bir ayrıntı var, çünkü tasarımın hem zekice hem de sınırlı olduğu yer orası.\n\nOpenTelemetry’de bir span’i *belirli bir trace’in içinde* başlatmak için ona bir **uzak ebeveyn** (remote parent) verirsin — istediğin trace id’yi taşıyan bir span context. Ama bir span context yalnızca *hem* bir trace id *hem de* bir span id’si varsa geçerlidir. Trace id’miz var (`trace_id`’den); span id’miz **yok**, çünkü bilinçle bir tane taşımadık. Bu yüzden `trace_id`’yi hash’leyerek deterministik, sıfır-olmayan bir span id sentezliyoruz. Ebeveyn geçerli, tüketici span’i doğru trace’e iniyor — ama ebeveyni hiç var olmamış bir span’i gösteriyor. Bir **hayalet**.\n\nYukarıdaki tasarımın dürüst sınırı bu ve açıkça söylemeye değer. Hop’lar-arası span’lerin hepsi **tek bir trace**’i paylaşır — bir mesajın hayatının her adımını bir arada, zamanlanmış, hatalar işaretlenmiş görebilirsin. Tek başına bunun **vermediği** şey, hop’lar *arasında* birebir ebeveyn-çocuk bağı: tüketicinin span’i üreticinin span’inin çocuğu olarak bağlanmaz, çünkü üreticinin gerçek span id’si tel boyunca hiç taşınmadı. Tek bir process içinde hiyerarşi doğrudur; kuyruğun ötesinde tek trace altında düzdür.\n\nGerçek hop’lar-arası ebeveyn-çocuğu geri almak, bir span id taşımak demek, o da bir `traceparent` demek — ve envelope donmuş olduğundan, o `traceparent` **bant dışında**, her broker bağlamasının `trace_id` için zaten kullandığı yuvanın yanında bir transport header olarak gitmek zorunda: SQS, Pulsar ve Kafka’da bir `bq-trace-id` alanı; RabbitMQ, Azure Service Bus ve ActiveMQ Artemis’te native correlation-id yuvası; hiç per-message metadata taşımayan Redis’te ise envelope gövdesinin kendisinden başka bir şey yok. Bu gerçek bir özellik, ama farklı ölçekte bir iş: her SDK’daki her transport bağlamasına dokunur. Yani bilinçli bir **ikinci faz** olarak bırakıldı — ve o faz bu yazıdan iki gün sonra kapandı: ADR-0028 ile `traceparent` transport-header taşıması 21 Haziran 2026’da altı SDK çekirdeğinin hepsine v0.2 olarak indi. Teslim edilen bir `traceparent` artık tüketici span’ini üreticinin span’inin gerçek çocuğuna yükseltiyor; taşınamadığı yerde — kalan boşluk PHP’nin Kafka, Pulsar ve STOMP üreticileri — yukarıda anlatılan v0.1 `trace_id` korelasyonuna temiz biçimde düşüyor. Sıralamanın kendisi zaten meseleydi: yukarıdaki birinci-faz tasarımı korelasyonu, hop başına zamanlamayı ve hata/retry görünürlüğünü — yani %90’ı — sıfır tel değişikliğiyle ve dil başına birkaç yüz satırla teslim eder ve v0.2’nin üzerine oturduğu zemin olarak kalır. %90’ı önce, son %10’u daha büyük bir işin ardına bırakarak göndermek doğru sıraydı — tıpkı [pahalı ölçekleme adımlarından önce ucuzları yaptığın gibi](/tr/systems/veri-yogunluklu-sistemler-kirilma-noktalari).\n\n## Tek semantik, altı paketleme deyimi\n\n**Çekirdeğin bağımlılıksız kalması** kısıtının bir sonucu var: OpenTelemetry kodu, OTel API’sini import etmek zorunda olduğu her yerde çekirdekte yaşayamaz. Bu yüzden altı dilin beşinde çekirdeğin *yanında* yaşar, yalnızca opt-in ettiğinde erişilir — ve \"çekirdeğin yanında, opsiyonel\" her ekosistemde farklı yazılır. (.NET istisna; nedenine birazdan geliyoruz.) Altısında da semantik aynı; paketleme her dilin karakterini gösterdiği yer:\n\n- **Go** — ayrı bir modül (`babelqueue-go/otel`, kendi `go.mod`’u), tıpkı transport alt-modülleri gibi. Çekirdek modül OTel bağımlılığını hiç görmez.\n- **Python** — bir `[otel]` extra’sı. `pip install babelqueue[otel]`, `opentelemetry-api`’yi getirir; modül onu import eder, yani yalnızca sen istediğinde import edilebilir. Buradaki bir TraceID byte değil, 128-bit bir `int`’tir — aynı değer, farklı biçim.\n- **Node** — bir subpath export, `@babelqueue/core/otel`, `@opentelemetry/api` ile *opsiyonel* bir peer dependency olarak. Kritik nokta: izleme kodu paket kökünden **yeniden export edilmez**: edilseydi, `@babelqueue/core`’u import etmek OTel import’unu erkenden yükler ve peer’i kurmayan herkes için kırılırdı. Subpath, ana girişi gerçekten bağımlılıksız tutar.\n- **Java** — `opentelemetry-api` üzerinde `optional` bir Maven bağımlılığı. Optional bağımlılıklar transitive değildir, yani izleme sınıflarına hiç dokunmayan bir tüketici OTel’i classpath’ine hiç çekmez.\n- **.NET** — ilginç olanı. .NET’te idiomatik izleme primitifi `System.Diagnostics.ActivitySource`’tur, **base class library** içinde yaşar — ve OpenTelemetry .NET’in üzerine kurulduğu *tam olarak* odur. Yani .NET modülünün **hiçbir bağımlılığı yok** — çekirdeğin yanında değil, *içinde* durabilmesinin sebebi de bu (`BabelQueue.Core / Telemetry`): `Activity` nesneleri yayınlar, tüketicinin OTel boru hattı onları `AddSource(\"BabelQueue\")` çağırarak toplar. Çekirdek, bağımlılığı izole ederek değil, bağımlılığa sahip olmayarak sıfır-dep kalır.\n- **PHP** — bir Composer `suggest` artı bir dev gereksinimi, mevcut opsiyonel yardımcıları aynalayarak. İzleme namespace’i orada; `open-telemetry/api` yalnızca kullanırsan gerekir.\n\nAltı deyim, hepsinde tutulan tek kural: **gözlemlenebilirliği kullanmak tüketicinin yaptığı bir seçimdir, asla çekirdeğin kestiği bir vergi değil.**\n\n## Gerçekte ne elde ediyorsun?\n\nMekanizmayı bir kenara bırak, kazanç tarif etmesi küçük, sahip olması büyük. Mevcut izleme backend’inde, eskiden üç log samanlığında bir iğne olan bir mesaj tek bir şelaleye dönüşür: onu hangi servis üretti, broker’da ne kadar bekledi, her tüketici ne kadar sürdü, retry oldu mu, DLQ’landı mı — zaten ortalıkta dolaştırdıkları bir UUID dışında hiçbir konuda anlaşmasını gerektirmediğin diller ve broker’lar boyunca.\n\nVe bunun tel üzerindeki maliyeti **sıfır**. İzleme var olmadan önce giden envelope ile onunla birlikte giden envelope byte-byte aynı. Trace baştan beri `trace_id`’nin içinde saklıydı; bütün iş, bir UUID ile bir TraceID’nin aynı on altı byte olduğunu fark etmekti.\n\n---\n\n*İlgili:* [BabelQueue observability spec’i](https://babelqueue.com/docs/spec/1.x/observability) bu tasarımı bir standart olarak yazıya döker; onu uygulayan SDK’lar — PHP, Go, Python, Node, Java ve .NET boyunca — [BabelQueue ekosisteminde](https://github.com/BabelQueue) yaşar.",
      "date_published": "2026-06-19T00:00:00.000Z",
      "tags": [
        "opentelemetry",
        "observability",
        "distributed-tracing",
        "messaging",
        "architecture",
        "System"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/semayi-kuyrugun-kenarinda-dogrulamak/",
      "url": "https://sade.dev/tr/notes/semayi-kuyrugun-kenarinda-dogrulamak/",
      "title": "Şemayı Kuyruğun Kenarında Doğrulamak",
      "summary": "Broker byte taşır, payload'ın içine asla bakmaz; tek bir servisin içindeki tip güvenliği veri telden geçtiği an buharlaşır. İki kenarda da doğrula, ama hangisinin kazandırdığını bil: üretici tarafı, yayınlamadan önce, hatayı senkron olarak gerçekten hatası olan serviste ortaya çıkarır. Tüketici tarafı ise kontrol etmediğin bir üreticiden gelen zehirli mesajı dead-letter kuyruğuna yönlendiren güvenlik ağıdır.",
      "content_html": "<p>Bir worker production’da <code>KeyError</code> fırlatmaya başladı. Sebep kendi kodu değildi — upstream bir servis, olayın payload’ından bir alanı düşüren bir değişiklik shiplemişti ve kuyruk bozuk mesajı tek kelime etmeden taşımıştı. Hata, kötü mesaj değildi. Hata, onu sınırda kontrol eden hiçbir şeyin olmamasıydı.</p>\n<h2 id=\"kuyruk-byte-taşır-tip-değil\">Kuyruk byte taşır, tip değil</h2>\n<p>Bir request body tipli bir handler’a çarpar; yanlış şekildeyse framework onu kapıda reddeder. Kuyruk sana bunun hiçbirini vermez. Payload opak JSON’dur — broker byte taşır ve içine asla bakmaz. Üretici ne koyduysa, tüketici onu alır, geçerli olsun olmasın. Tek bir servisin içinde sahip olduğun tip güvenliği, veri telden geçtiği an buharlaşır.</p>\n<p>Yani bir mesajın şekli hakkında bir garanti istiyorsan, onu kendin eklemelisin — <strong>kenarda</strong>, verinin girdiği ve çıktığı yerde.</p>\n<h2 id=\"verinin-girdiği-ve-çıktığı-yerde-doğrula\">Verinin girdiği ve çıktığı yerde doğrula</h2>\n<p>İki kenar var ve farklı hataları yakalar:</p>\n<ul>\n<li><strong>Üretici tarafında, yayınlamadan önce — birincil olan.</strong> Payload’ı üretirken doğrula. Geçersiz veri kuyruğa hiç girmez, hata <em>senkron</em> olarak gerçekten hatası olan serviste ortaya çıkar ve bir kez yakalanır — mesaj her tüketiciye yayılmadan önce. Başarısız olmanın ucuz yeri burası.</li>\n<li><strong>Tüketici tarafında, alırken — güvenlik ağı.</strong> Tüketirken yine doğrula. Bu, üretici tarafının yakalayamayacağını yakalar: kontrol etmediğin üreticilerden gelen mesajlar — başka bir takım, eski bir deploy edilmiş sürüm, elle hazırlanmış bir replay. Burada doğrulamayı geçemeyen mesaj zehirli bir mesajdır; handler’ı döngüde çökertmek yerine onu bir dead-letter kuyruğuna yönlendir.</li>\n</ul>\n<p>Bilmeye değer bir incelik: çoğu kuyruk runtime’ında “anında reddet” kancası yoktur, yani tüketici tarafı bir ret genelde dead-letter’a gitmeden önce <em>retry</em> eder — ve geçersiz veri retry’de geçerli olmaz. Bu boşa iştir; üretici tarafının değerli olmasının sebebi tam olarak budur. Tüketici tarafı emniyet kemeridir, direksiyon değil.</p>\n<h2 id=\"şema-envelopela-değil-mesajla-yaşar\">Şema envelope’la değil, mesajla yaşar</h2>\n<p>Doğruladığın şey, <em>mesaj türü başına</em> bir şemadır — olayın kimliğiyle anahtarlanır, taşıma envelope’una cıvatalanmaz. Onu bağımsız sürümlenen bir registry’de tut; <a href=\"/tr/journal/sema-evrimi-sozlesmeyi-kirmadan/\">tüketicileri kırmadan dikkatle evrimleştirdiğin</a> aynı şema, runtime’da zorladığın şema olur. Tek sözleşme, hem değişim-anında (bu düzenleme birini kırıyor mu?) hem runtime’da (bu mesaj ona uyuyor mu?) kontrol edilir.</p>\n<p>Bu, <a href=\"/tr/notes/idempotency-ayni-mesaj-iki-kez/\">bir mükerrer teslimatı no-op yapmanın</a> yapısal kuzeni: ikisi de ne geldiğini kontrol etmediğini kabul eder ve garantiyi göndericiye güvenmek yerine kendi sınırına koyar.</p>\n<h2 id=\"ne-zaman-buna-değmez\">Ne zaman buna değmez?</h2>\n<p>Tek bir servisin iç kuyruğu — tek üretici, tek tüketici, birlikte deploy — buna ihtiyaç duymaz. Tip sistemi zaten iki ucu da kapsar; kenar doğrulaması törendir. <em>İkinci</em>, bağımsız deploy edilen bir üretici ya da tüketici var olduğu an değmeye başlar — farklı bir takım, farklı bir dil, farklı bir sürüm temposu. O an aynı zamanda tipsiz payload’ın sessizce en kırılgan sözleşmen hâline geldiği andır.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://babelqueue.com/docs/spec/1.x/schema-validation\">BabelQueue schema-validation spec’i</a> URN-başına şemayı ve nerede zorlandığını tanımlar; <a href=\"https://github.com/BabelQueue/babelqueue-registry\">babelqueue-registry</a> o şemaları tutar — kontrolü çalıştıran ise onun <code>bqschema</code> aracıdır.</p>",
      "content_text": "Bir worker production'da `KeyError` fırlatmaya başladı. Sebep kendi kodu değildi — upstream bir servis, olayın payload'ından bir alanı düşüren bir değişiklik shiplemişti ve kuyruk bozuk mesajı tek kelime etmeden taşımıştı. Hata, kötü mesaj değildi. Hata, onu sınırda kontrol eden hiçbir şeyin olmamasıydı.\n\n## Kuyruk byte taşır, tip değil\n\nBir request body tipli bir handler'a çarpar; yanlış şekildeyse framework onu kapıda reddeder. Kuyruk sana bunun hiçbirini vermez. Payload opak JSON'dur — broker byte taşır ve içine asla bakmaz. Üretici ne koyduysa, tüketici onu alır, geçerli olsun olmasın. Tek bir servisin içinde sahip olduğun tip güvenliği, veri telden geçtiği an buharlaşır.\n\nYani bir mesajın şekli hakkında bir garanti istiyorsan, onu kendin eklemelisin — **kenarda**, verinin girdiği ve çıktığı yerde.\n\n## Verinin girdiği ve çıktığı yerde doğrula\n\nİki kenar var ve farklı hataları yakalar:\n\n- **Üretici tarafında, yayınlamadan önce — birincil olan.** Payload'ı üretirken doğrula. Geçersiz veri kuyruğa hiç girmez, hata *senkron* olarak gerçekten hatası olan serviste ortaya çıkar ve bir kez yakalanır — mesaj her tüketiciye yayılmadan önce. Başarısız olmanın ucuz yeri burası.\n- **Tüketici tarafında, alırken — güvenlik ağı.** Tüketirken yine doğrula. Bu, üretici tarafının yakalayamayacağını yakalar: kontrol etmediğin üreticilerden gelen mesajlar — başka bir takım, eski bir deploy edilmiş sürüm, elle hazırlanmış bir replay. Burada doğrulamayı geçemeyen mesaj zehirli bir mesajdır; handler'ı döngüde çökertmek yerine onu bir dead-letter kuyruğuna yönlendir.\n\nBilmeye değer bir incelik: çoğu kuyruk runtime'ında \"anında reddet\" kancası yoktur, yani tüketici tarafı bir ret genelde dead-letter'a gitmeden önce *retry* eder — ve geçersiz veri retry'de geçerli olmaz. Bu boşa iştir; üretici tarafının değerli olmasının sebebi tam olarak budur. Tüketici tarafı emniyet kemeridir, direksiyon değil.\n\n## Şema envelope'la değil, mesajla yaşar\n\nDoğruladığın şey, *mesaj türü başına* bir şemadır — olayın kimliğiyle anahtarlanır, taşıma envelope'una cıvatalanmaz. Onu bağımsız sürümlenen bir registry'de tut; [tüketicileri kırmadan dikkatle evrimleştirdiğin](/tr/journal/sema-evrimi-sozlesmeyi-kirmadan) aynı şema, runtime'da zorladığın şema olur. Tek sözleşme, hem değişim-anında (bu düzenleme birini kırıyor mu?) hem runtime'da (bu mesaj ona uyuyor mu?) kontrol edilir.\n\nBu, [bir mükerrer teslimatı no-op yapmanın](/tr/notes/idempotency-ayni-mesaj-iki-kez) yapısal kuzeni: ikisi de ne geldiğini kontrol etmediğini kabul eder ve garantiyi göndericiye güvenmek yerine kendi sınırına koyar.\n\n## Ne zaman buna değmez?\n\nTek bir servisin iç kuyruğu — tek üretici, tek tüketici, birlikte deploy — buna ihtiyaç duymaz. Tip sistemi zaten iki ucu da kapsar; kenar doğrulaması törendir. *İkinci*, bağımsız deploy edilen bir üretici ya da tüketici var olduğu an değmeye başlar — farklı bir takım, farklı bir dil, farklı bir sürüm temposu. O an aynı zamanda tipsiz payload'ın sessizce en kırılgan sözleşmen hâline geldiği andır.\n\n---\n\n*İlgili:* [BabelQueue schema-validation spec'i](https://babelqueue.com/docs/spec/1.x/schema-validation) URN-başına şemayı ve nerede zorlandığını tanımlar; [babelqueue-registry](https://github.com/BabelQueue/babelqueue-registry) o şemaları tutar — kontrolü çalıştıran ise onun `bqschema` aracıdır.",
      "date_published": "2026-06-18T00:00:00.000Z",
      "tags": [
        "queue",
        "schema",
        "reliability",
        "architecture",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/mimari-kararlar-curur/",
      "url": "https://sade.dev/tr/journal/mimari-kararlar-curur/",
      "title": "Mimari Kararlar Çürür Çünkü Onları Kimse Çalıştırmaz",
      "summary": "Bir mimari karar, onu her commit'te kontrol eden bir şey yoksa kural değil dilektir. Kararın iki parçası var: bir aracın tutamayacağı için nesre ait olan gerekçe ve import grafiği üzerinde deterministik olarak değerlendirilip build'i kırabilen kısıt. Gerekçeyi insanlar için yazmaya devam edin; sınırı, onu zorlamaktan asla yorulmayan bir makineye devredin.",
      "content_html": "<p>Yeni bir mühendis bana domain katmanının neden database paketini import ettiğini sordu — “bir ADR (mimari karar kaydı) import etmemesi gerektiğini söylemiyor mu?” Söylüyordu. İki yıldır söylüyordu. Ve o iki yılın çoğunda, bir yerlerde, kural sessizce çiğnenmişti: şurada bir helper, burada bir “sadece bu seferlik” import, her biri kendi pull request’inde makul. Karar wiki’de hâlâ doğruydu. Kodda aylar önce doğru olmaktan çıkmıştı ve kimse fark etmedi, çünkü onu izleyen hiçbir şey yoktu.</p>\n<p>Yazıp da zorlayamadığın bir karar bir kural değildir. İyi formatlanmış bir dilektir.</p>\n<h2 id=\"diyagram-ile-kod-küçük-ve-makul-adımlarla-ayrışır\">Diyagram ile kod, küçük ve makul adımlarla ayrışır</h2>\n<p>Kimse mimariyi çiğnemeye karar vermez. Her seferinde savunulabilir bir commit’le aşınır: async yolu yazmak daha yavaş olduğu için eklenen senkron bir çağrı, ihtiyacın olan fonksiyon öbür tarafta olduğu için bir sınırı geçen bir import. Her diff tek başına iyi görünür. Hasar <strong>birikimli ve yapısaldır</strong> — tek bir değişiklik ölçeğinde görünmez, ancak geri çekilip katmanlamanın yok olduğunu bulunca belli olur.</p>\n<p>Bu tam da bir insan review’ının çalıştığı ölçek ve tam da kaçırdığı ölçek. <span data-scheduled=\"/tr/journal/code-review-kulturu/\">Review en iyi bir değişikliğin mimarisinde işler</span>, ama bir reviewer tek bir PR görür, sınırı nihayet eriten bininci import’u değil. Bir insandan her commit’te tüm bağımlılık grafiğini kafasında tutmasını isteyemezsin. Wiki sayfası da tutamaz; o nesirdir ve nesir bir build’i patlatmaz.</p>\n<h2 id=\"kısıtı-çalıştırılabilir-yap\">Kısıtı çalıştırılabilir yap</h2>\n<p>Çözüm daha iyi yazılmış bir ADR değil. Kararın <em>zorlanabilir kısmını</em> nesirden çıkarıp çalışan bir şeye taşımak.</p>\n<p>“Domain altyapıya bağımlı olmamalı” özünde bir görüş değildir — import grafiği üzerinde bir kısıttır: <code>domain/</code> altındaki dosyalar <code>db/</code>’yi import edemez. Bu, bir aracın her commit’te deterministik olarak değerlendirebileceği ve çiğnendiğinde build’i kırabileceği bir kural — çiğneyen import <code>main</code>’e ulaşmadan, iki yıl sonra değil. Yakalama maliyeti, “bir şey nihayet kırılınca yapılan bir arkeoloji seansı”ndan “onu sokan PR’daki kırmızı bir kontrol”e iner.</p>\n<p>Bu, <a href=\"/tr/journal/sema-evrimi-sozlesmeyi-kirmadan/\">bir şemayı değiştirmek yerine sürümlemenin</a> aynı hamlesi: herkesin <em>üzerinde anlaştığı</em> bir kural, mekanik bir şey çizgiyi tutana dek hiçbir şey etmez — çünkü insan disiplini, her katkıcının her commit’ine ölçeklenmez.</p>\n<h2 id=\"dokümanda-ne-kalır-ne-kod-olur\">Dokümanda ne kalır, ne kod olur</h2>\n<p>Bu ADR’yi öldürmez — onu ikiye böler. Bir mimari kararın iki parçası vardır: <strong>gerekçe</strong> (hexagonal katmanlamayı niye seçtik, neyi feda ettik, ne zaman yeniden gözden geçiririz) ve <strong>kısıt</strong> (domain hiçbir şey import etmez; API, DB’yle yalnızca bir repository üzerinden konuşur). Gerekçe nesre aittir — bir aracın tutamayacağı, bir insanın ihtiyaç duyduğu bağlam. Kısıt ise koda aittir, kontrol edilebileceği yere.</p>\n<p><em>Neden</em>’i yazmaya devam et. <em>Ne</em>’yi zorlamak için nesre güvenmeyi bırak.</p>\n<h2 id=\"bu-ne-zaman-önemsizleşir\">Bu ne zaman önemsizleşir?</h2>\n<p>Tüm sistemin iki katman ve tek bir masanın etrafına sığan bir takımsa, sınırlar herkesin kafasında yaşar ve onlar için bir CI kapısı törendir. Üçüncü bir katkıcı katıldığı ya da kod tabanı tek bir kişinin tutabileceğini aştığı an, zorlanmayan sınır <em>zaten</em> aşınıyordur — sadece yeni mühendis domain’in neden database’i import ettiğini sorana dek görmezsin.</p>\n<hr/>\n<p>Bir mimari, karar verdiğin şey değildir; kodunun şu an yaptığı şeydir. Büyüyen bir kod tabanıyla temasta hayatta kalan tek kararlar, bir şeyin her commit’te kontrol ettikleridir. Gerekçeyi insanlar için yaz — ve sınırı, onu zorlamaktan asla yorulmayan bir makineye devret.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://github.com/muhammetsafak/archlint\">archlint</a>, tam da bunun için yazdığım araç — katman kurallarını okuyup bir ihlalde build’i kıran, deterministik bir import-sınır linter’ı (Go, TypeScript ve Python), paketlenmiş bir CI Action’ıyla.</p>",
      "content_text": "Yeni bir mühendis bana domain katmanının neden database paketini import ettiğini sordu — \"bir ADR (mimari karar kaydı) import etmemesi gerektiğini söylemiyor mu?\" Söylüyordu. İki yıldır söylüyordu. Ve o iki yılın çoğunda, bir yerlerde, kural sessizce çiğnenmişti: şurada bir helper, burada bir \"sadece bu seferlik\" import, her biri kendi pull request'inde makul. Karar wiki'de hâlâ doğruydu. Kodda aylar önce doğru olmaktan çıkmıştı ve kimse fark etmedi, çünkü onu izleyen hiçbir şey yoktu.\n\nYazıp da zorlayamadığın bir karar bir kural değildir. İyi formatlanmış bir dilektir.\n\n## Diyagram ile kod, küçük ve makul adımlarla ayrışır\n\nKimse mimariyi çiğnemeye karar vermez. Her seferinde savunulabilir bir commit'le aşınır: async yolu yazmak daha yavaş olduğu için eklenen senkron bir çağrı, ihtiyacın olan fonksiyon öbür tarafta olduğu için bir sınırı geçen bir import. Her diff tek başına iyi görünür. Hasar **birikimli ve yapısaldır** — tek bir değişiklik ölçeğinde görünmez, ancak geri çekilip katmanlamanın yok olduğunu bulunca belli olur.\n\nBu tam da bir insan review'ının çalıştığı ölçek ve tam da kaçırdığı ölçek. Review en iyi bir değişikliğin mimarisinde işler, ama bir reviewer tek bir PR görür, sınırı nihayet eriten bininci import'u değil. Bir insandan her commit'te tüm bağımlılık grafiğini kafasında tutmasını isteyemezsin. Wiki sayfası da tutamaz; o nesirdir ve nesir bir build'i patlatmaz.\n\n## Kısıtı çalıştırılabilir yap\n\nÇözüm daha iyi yazılmış bir ADR değil. Kararın *zorlanabilir kısmını* nesirden çıkarıp çalışan bir şeye taşımak.\n\n\"Domain altyapıya bağımlı olmamalı\" özünde bir görüş değildir — import grafiği üzerinde bir kısıttır: `domain/` altındaki dosyalar `db/`'yi import edemez. Bu, bir aracın her commit'te deterministik olarak değerlendirebileceği ve çiğnendiğinde build'i kırabileceği bir kural — çiğneyen import `main`'e ulaşmadan, iki yıl sonra değil. Yakalama maliyeti, \"bir şey nihayet kırılınca yapılan bir arkeoloji seansı\"ndan \"onu sokan PR'daki kırmızı bir kontrol\"e iner.\n\nBu, [bir şemayı değiştirmek yerine sürümlemenin](/tr/journal/sema-evrimi-sozlesmeyi-kirmadan) aynı hamlesi: herkesin *üzerinde anlaştığı* bir kural, mekanik bir şey çizgiyi tutana dek hiçbir şey etmez — çünkü insan disiplini, her katkıcının her commit'ine ölçeklenmez.\n\n## Dokümanda ne kalır, ne kod olur\n\nBu ADR'yi öldürmez — onu ikiye böler. Bir mimari kararın iki parçası vardır: **gerekçe** (hexagonal katmanlamayı niye seçtik, neyi feda ettik, ne zaman yeniden gözden geçiririz) ve **kısıt** (domain hiçbir şey import etmez; API, DB'yle yalnızca bir repository üzerinden konuşur). Gerekçe nesre aittir — bir aracın tutamayacağı, bir insanın ihtiyaç duyduğu bağlam. Kısıt ise koda aittir, kontrol edilebileceği yere.\n\n*Neden*'i yazmaya devam et. *Ne*'yi zorlamak için nesre güvenmeyi bırak.\n\n## Bu ne zaman önemsizleşir?\n\nTüm sistemin iki katman ve tek bir masanın etrafına sığan bir takımsa, sınırlar herkesin kafasında yaşar ve onlar için bir CI kapısı törendir. Üçüncü bir katkıcı katıldığı ya da kod tabanı tek bir kişinin tutabileceğini aştığı an, zorlanmayan sınır *zaten* aşınıyordur — sadece yeni mühendis domain'in neden database'i import ettiğini sorana dek görmezsin.\n\n---\n\nBir mimari, karar verdiğin şey değildir; kodunun şu an yaptığı şeydir. Büyüyen bir kod tabanıyla temasta hayatta kalan tek kararlar, bir şeyin her commit'te kontrol ettikleridir. Gerekçeyi insanlar için yaz — ve sınırı, onu zorlamaktan asla yorulmayan bir makineye devret.\n\n---\n\n*İlgili:* [archlint](https://github.com/muhammetsafak/archlint), tam da bunun için yazdığım araç — katman kurallarını okuyup bir ihlalde build'i kıran, deterministik bir import-sınır linter'ı (Go, TypeScript ve Python), paketlenmiş bir CI Action'ıyla.",
      "date_published": "2026-06-18T00:00:00.000Z",
      "tags": [
        "architecture",
        "governance",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/notes/idempotency-ayni-mesaj-iki-kez/",
      "url": "https://sade.dev/tr/notes/idempotency-ayni-mesaj-iki-kez/",
      "title": "Idempotency: Aynı Mesaj İki Kez Geldiğinde",
      "summary": "At-least-once yok edilecek bir arıza değil, etrafında tasarım yapılacak bir garantidir. Anahtarı gönderici basar ve yalnızca payload'dan türetilmez; işi yapmakla anahtarı kaydetmek arasındaki boşluk da şekli belirler: etki senin kontrolündeyse anahtarı aynı transaction'da yaz, değilse seen-set'le yetin. Eşzamanlı mükerrerler için kontrol değil, unique constraint gerekir.",
      "content_html": "<p>Bir müşteri tek sipariş için iki kez ücretlendirildi. Hiçbir şey bilerek iki kez çalışmadı: ödeme worker’ı kartı çekti, sonra mesajı ack’leyemeden çöktü — broker da sözünü tutup mesajı yeniden teslim etti.</p>\n<p>Hata, mükerrer teslimat değildi. Mükerrer teslimat için inşa edilmemiş handler’dı.</p>\n<h2 id=\"at-least-once-bir-söz-bir-arıza-değil\">At-least-once bir söz, bir arıza değil</h2>\n<p>Neredeyse her queue — ve yeniden denenen her HTTP çağrısı — <strong>at-least-once</strong>’tır. Bir handler aynı mesajı birden çok kez görebilir: bir çökme, bir release, bir redelivery ya da timeout’a düşüp yeniden deneyen bir istemci sonrası. Ağ üzerinde exactly-once, broker’ın ucuza veremeyeceği bir koordinasyon olmadan pratik değil; bu yüzden çok az broker onu vaat eder — vaat edenler de (Google Cloud Pub/Sub gibi) dar koşullara sıkıştırır: tek bölge, yalnızca pull aboneliği.</p>\n<p>Bu, problemi yeniden çerçeveler. Redelivery, yok edilecek bir başarısızlık değil; etrafında tasarım yapılacak bir garanti. (<a href=\"/tr/notes/senkron-mu-asenkron-mu/\">Queue</a>’nun başarısız bir job’u kaybetmek yerine yeniden denerken yaptığı pazarlığın aynısı.)</p>\n<h2 id=\"idempotent-demek-iki-kez--bir-kez-demek\">Idempotent demek “iki kez = bir kez” demek</h2>\n<p>Bir operasyon, defalarca uygulandığında etkisi bir kez uygulanmışla aynıysa idempotent’tir — <code>f(f(x)) = f(x)</code>.</p>\n<ul>\n<li>“Sipariş durumunu <code>paid</code> yap” idempotent’tir.</li>\n<li>“Bakiyeyi 10 artır” <strong>değildir</strong> — iki kez +20 eder.</li>\n<li>“Karşılama e-postasını gönder” <strong>değildir</strong> — iki kez iki e-posta eder.</li>\n</ul>\n<p>Bütün iş, ikinci tür operasyonu birinciye çevirmek.</p>\n<h2 id=\"idempotency-anahtarı-göndericiden-gelir\">Idempotency anahtarı göndericiden gelir</h2>\n<p><em>Bu operasyon</em> için kararlı bir kimliğe ihtiyacın var ve bunu <strong>gönderici</strong> belirlemeli, alıcı değil:</p>\n<ul>\n<li>Bir queue mesajı için bu, <strong>üreticinin ürettiği mesaj-başına id</strong>’dir — mesaj başına bir id, trace/correlation id’den ayrı (o, birçok mesajı kapsar).</li>\n<li>Bir HTTP yazımı için bu, <strong>istemcinin ürettiği <code>Idempotency-Key</code> başlığı</strong>’dır — Stripe modeli.</li>\n</ul>\n<p>İki kural anahtarı dürüst tutar:</p>\n<ul>\n<li><strong>Yalnızca payload’dan türetme.</strong> Meşru biçimde birbirinin aynı iki istek — aynı müşterinin aynı ürünü iki kez alması — çakışır ve ikincisi sessizce düşürülür.</li>\n<li><strong>Anahtarı alıcıya ürettirme.</strong> Alıcı, bir retry’ı yepyeni bir çağrıdan ayıramaz; “bu, daha önce denediğim aynı operasyon” bilgisi yalnızca göndericide vardır.</li>\n</ul>\n<h2 id=\"dedupe-zaten-yaptığını-hatırla\">Dedupe: zaten yaptığını hatırla</h2>\n<p>Handler, işten önce tek bir kontrol yapar: <em>bu anahtar işlendi mi?</em> Evetse atla ve ack’le. Hayırsa işi yap, sonra anahtarı kaydet. Tüm desen bu — ağırlık, kaydın <em>nerede</em> durduğunda.</p>\n<h2 id=\"i̇ş-ile-kayıt-arasındaki-çökme\">İş ile kayıt arasındaki çökme</h2>\n<p>Zor kısım, işi yapmakla anahtarı kaydetmek arasındaki boşluk. İki şekil var ve doğrusu yan etkiye bağlı:</p>\n<ul>\n<li><strong>Seen-set — başarıdan sonra kaydet.</strong> Handler döndükten sonra anahtarı Redis’e ya da bir tabloya yaz. Ucuz ve geniş çapta uygulanabilir. Pencere: yan etkiden <em>sonra</em> ama kayıttan <em>önce</em> çökersen, bir redelivery yeniden işler. Yan etkinin kendisi idempotent’se sorun değil — bir <code>UPSERT</code>, bir “set to paid”.</li>\n<li><strong>Transactional — işle birlikte kaydet.</strong> Idempotency kaydını, iş değişikliğiyle <strong>aynı veritabanı transaction’ında</strong> yaz. Pencere yok: anahtar ve etki birlikte commit olur ya da birlikte geri alınır. Bedeli: etki, senin kontrol ettiğin bir DB yazımı olmalı ve anahtar deposu aynı veritabanı olmalı — ayrı bir Redis değil. Kendi kontrolündeki bir etki için mükerrer işlemeyi gerçekten bitiren budur.</li>\n</ul>\n<p>Yani yan etki senin yerine seçer: sahip olduğun bir satır → transactional; transaction’ına dahil edemeyeceğin üçüncü-taraf bir çağrı (e-posta, bir çekim) → seen-set ve kendi anahtarını onun <code>Idempotency-Key</code>’i olarak ileterek downstream’i de idempotent yap.</p>\n<h2 id=\"eşzamanlı-mükerrerlere-kontrol-değil-constraint-lazım\">Eşzamanlı mükerrerlere kontrol değil, constraint lazım</h2>\n<p>Aynı anahtarın, ikisi de kaydetmeden yarışan iki teslimatı bir “önce kontrol et sonra yaz” dizisinden sıyrılır — o dizi atomik değil. <strong>Anahtar kolonundaki bir unique constraint</strong> atomiktir: ikinci insert, yakalayıp “zaten işlendi” diye ele aldığın bir conflict’e dönüşür. Bu, <a href=\"/tr/systems/ardisik-numara-uretiminde-race-condition-ve-gap/\">sıralı numara üretiminde bir race’in açtığı gap’lerin</a> aynı şekli — benzersizlik gerçekte uygulamada değil, veritabanında zorlanır.</p>\n<h2 id=\"buna-ne-zaman-hiç-ihtiyacın-yok\">Buna ne zaman hiç ihtiyacın yok?</h2>\n<p>Handler zaten idempotent’se — kararlı bir id ile anahtarlanmış saf bir <code>UPSERT</code>, bir “alanı şu değere ayarla” — hiçbir şeye ihtiyacın olmayabilir. Anahtar + dedupe’a, operasyonun <strong>idempotent olmayan bir yan etkisi</strong> (para, e-posta, dış bir <code>POST</code>) ya da <strong>birikimi</strong> (increment, append) olduğunda uzan. Karmaşıklığı yalnızca ikinci çalıştırmanın gerçekten zarar verdiği yere harca.</p>\n<hr/>\n<p>At-least-once, düzelteceğin kısım değil; kabul ettiğin kısım. Handler’ı, ikinci teslimat bir no-op olacak şekilde kur; o zaman redelivery bir olay olmaktan çıkar ve olduğu şeye döner — broker’ın sözünü tutması.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://babelqueue.com/docs/spec/1.x/idempotency\">BabelQueue idempotency spec’i</a> anahtarın nereden geldiğini ve dedupe’un nasıl davrandığını sabitler; <a href=\"https://github.com/BabelQueue/babelqueue-examples/tree/main/idempotency-payments\"><code>idempotency-payments</code> örneği</a> ise tam da bu çift-çekim vakasının çalıştırılabilir hâlidir.</p>",
      "content_text": "Bir müşteri tek sipariş için iki kez ücretlendirildi. Hiçbir şey bilerek iki kez çalışmadı: ödeme worker'ı kartı çekti, sonra mesajı ack'leyemeden çöktü — broker da sözünü tutup mesajı yeniden teslim etti.\n\nHata, mükerrer teslimat değildi. Mükerrer teslimat için inşa edilmemiş handler'dı.\n\n## At-least-once bir söz, bir arıza değil\n\nNeredeyse her queue — ve yeniden denenen her HTTP çağrısı — **at-least-once**'tır. Bir handler aynı mesajı birden çok kez görebilir: bir çökme, bir release, bir redelivery ya da timeout'a düşüp yeniden deneyen bir istemci sonrası. Ağ üzerinde exactly-once, broker'ın ucuza veremeyeceği bir koordinasyon olmadan pratik değil; bu yüzden çok az broker onu vaat eder — vaat edenler de (Google Cloud Pub/Sub gibi) dar koşullara sıkıştırır: tek bölge, yalnızca pull aboneliği.\n\nBu, problemi yeniden çerçeveler. Redelivery, yok edilecek bir başarısızlık değil; etrafında tasarım yapılacak bir garanti. ([Queue](/tr/notes/senkron-mu-asenkron-mu)'nun başarısız bir job'u kaybetmek yerine yeniden denerken yaptığı pazarlığın aynısı.)\n\n## Idempotent demek \"iki kez = bir kez\" demek\n\nBir operasyon, defalarca uygulandığında etkisi bir kez uygulanmışla aynıysa idempotent'tir — `f(f(x)) = f(x)`.\n\n- \"Sipariş durumunu `paid` yap\" idempotent'tir.\n- \"Bakiyeyi 10 artır\" **değildir** — iki kez +20 eder.\n- \"Karşılama e-postasını gönder\" **değildir** — iki kez iki e-posta eder.\n\nBütün iş, ikinci tür operasyonu birinciye çevirmek.\n\n## Idempotency anahtarı göndericiden gelir\n\n*Bu operasyon* için kararlı bir kimliğe ihtiyacın var ve bunu **gönderici** belirlemeli, alıcı değil:\n\n- Bir queue mesajı için bu, **üreticinin ürettiği mesaj-başına id**'dir — mesaj başına bir id, trace/correlation id'den ayrı (o, birçok mesajı kapsar).\n- Bir HTTP yazımı için bu, **istemcinin ürettiği `Idempotency-Key` başlığı**'dır — Stripe modeli.\n\nİki kural anahtarı dürüst tutar:\n\n- **Yalnızca payload'dan türetme.** Meşru biçimde birbirinin aynı iki istek — aynı müşterinin aynı ürünü iki kez alması — çakışır ve ikincisi sessizce düşürülür.\n- **Anahtarı alıcıya ürettirme.** Alıcı, bir retry'ı yepyeni bir çağrıdan ayıramaz; \"bu, daha önce denediğim aynı operasyon\" bilgisi yalnızca göndericide vardır.\n\n## Dedupe: zaten yaptığını hatırla\n\nHandler, işten önce tek bir kontrol yapar: *bu anahtar işlendi mi?* Evetse atla ve ack'le. Hayırsa işi yap, sonra anahtarı kaydet. Tüm desen bu — ağırlık, kaydın *nerede* durduğunda.\n\n## İş ile kayıt arasındaki çökme\n\nZor kısım, işi yapmakla anahtarı kaydetmek arasındaki boşluk. İki şekil var ve doğrusu yan etkiye bağlı:\n\n- **Seen-set — başarıdan sonra kaydet.** Handler döndükten sonra anahtarı Redis'e ya da bir tabloya yaz. Ucuz ve geniş çapta uygulanabilir. Pencere: yan etkiden *sonra* ama kayıttan *önce* çökersen, bir redelivery yeniden işler. Yan etkinin kendisi idempotent'se sorun değil — bir `UPSERT`, bir \"set to paid\".\n- **Transactional — işle birlikte kaydet.** Idempotency kaydını, iş değişikliğiyle **aynı veritabanı transaction'ında** yaz. Pencere yok: anahtar ve etki birlikte commit olur ya da birlikte geri alınır. Bedeli: etki, senin kontrol ettiğin bir DB yazımı olmalı ve anahtar deposu aynı veritabanı olmalı — ayrı bir Redis değil. Kendi kontrolündeki bir etki için mükerrer işlemeyi gerçekten bitiren budur.\n\nYani yan etki senin yerine seçer: sahip olduğun bir satır → transactional; transaction'ına dahil edemeyeceğin üçüncü-taraf bir çağrı (e-posta, bir çekim) → seen-set ve kendi anahtarını onun `Idempotency-Key`'i olarak ileterek downstream'i de idempotent yap.\n\n## Eşzamanlı mükerrerlere kontrol değil, constraint lazım\n\nAynı anahtarın, ikisi de kaydetmeden yarışan iki teslimatı bir \"önce kontrol et sonra yaz\" dizisinden sıyrılır — o dizi atomik değil. **Anahtar kolonundaki bir unique constraint** atomiktir: ikinci insert, yakalayıp \"zaten işlendi\" diye ele aldığın bir conflict'e dönüşür. Bu, [sıralı numara üretiminde bir race'in açtığı gap'lerin](/tr/systems/ardisik-numara-uretiminde-race-condition-ve-gap) aynı şekli — benzersizlik gerçekte uygulamada değil, veritabanında zorlanır.\n\n## Buna ne zaman hiç ihtiyacın yok?\n\nHandler zaten idempotent'se — kararlı bir id ile anahtarlanmış saf bir `UPSERT`, bir \"alanı şu değere ayarla\" — hiçbir şeye ihtiyacın olmayabilir. Anahtar + dedupe'a, operasyonun **idempotent olmayan bir yan etkisi** (para, e-posta, dış bir `POST`) ya da **birikimi** (increment, append) olduğunda uzan. Karmaşıklığı yalnızca ikinci çalıştırmanın gerçekten zarar verdiği yere harca.\n\n---\n\nAt-least-once, düzelteceğin kısım değil; kabul ettiğin kısım. Handler'ı, ikinci teslimat bir no-op olacak şekilde kur; o zaman redelivery bir olay olmaktan çıkar ve olduğu şeye döner — broker'ın sözünü tutması.\n\n---\n\n*İlgili:* [BabelQueue idempotency spec'i](https://babelqueue.com/docs/spec/1.x/idempotency) anahtarın nereden geldiğini ve dedupe'un nasıl davrandığını sabitler; [`idempotency-payments` örneği](https://github.com/BabelQueue/babelqueue-examples/tree/main/idempotency-payments) ise tam da bu çift-çekim vakasının çalıştırılabilir hâlidir.",
      "date_published": "2026-06-17T00:00:00.000Z",
      "tags": [
        "queue",
        "reliability",
        "architecture",
        "async",
        "Note"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/ai-darbogazi-kaldirmadi/",
      "url": "https://sade.dev/tr/journal/ai-darbogazi-kaldirmadi/",
      "title": "AI Darboğazı Kaldırmadı, Yerini Değiştirdi",
      "summary": "Kod üretimini üç kat hızlandırmak throughput'u üçe katlamaz; kısıtı bir istasyon aşağı taşır ve önünde bir work-in-progress yığını büyütür. Review kuyruğu, kararsız test süiti ve entegrasyon hataları o yığından geliyor. Kalıcı yatırım, cazip olmayan aşağı taraf: deterministik doğrulama, idempotent entegrasyon ve bariz hatanın commit sınırında yakalanması.",
      "content_html": "<p>İzlediğim bir ekip, ajanlara yaslanınca kabaca iki kat kod üretmeye başladı. Altı hafta sonra <em>daha yavaş</em> teslim ediyorlardı. Pull-request kuyruğu üçe katlanmış, CI yeşilden çok kırmızıydı ve iki olayın kökü kimsenin gerçekten okumadığı değişikliklere çıkıyordu. Çıktı arttı; throughput düştü.</p>\n<p>Bunda gizem yok. Hattın tek bir istasyonunu hızlandırıp buna zafer dediler.</p>\n<h2 id=\"tek-istasyonu-hızlandırıp-buna-throughput-diyemezsin\">Tek istasyonu hızlandırıp buna throughput diyemezsin</h2>\n<p>Bir teslimat hattı bir istasyon dizisidir: yaz → review → entegre et → doğrula → release. Throughput’u <strong>en yavaş</strong> istasyon belirler, en hızlısı değil. Yazmayı 3× hızlandırırsın ama review ve doğrulama yerinde kalırsa throughput’u üçe katlamış olmazsın — kısıtı yalnızca bir istasyon aşağı taşımış ve onun önünde bir work-in-progress yığını büyütmüş olursun.</p>\n<p>O yığın bedavaya gelmez. Merge conflict’e yaşlanan okunmamış PR’lar, yarı entegre branch’ler, kimsenin yeşil tutamayacağı kadar hızlı büyüyen bir test süiti. Kod ucuzlayınca darboğaz yok olmadı. Yer değiştirdi — ve görülmesi zorlaştı, çünkü artık “kod yazmak” gibi değil “beklemek” gibi görünüyor.</p>\n<h2 id=\"doranın-bunun-için-zaten-bir-kelimesi-var-büyüteç\">DORA’nın bunun için zaten bir kelimesi var: büyüteç</h2>\n<p><a href=\"https://dora.dev/research/2024/dora-report/\">2024</a> ve <a href=\"https://dora.dev/dora-report-2025/\">2025</a> DORA (DevOps Research and Assessment) raporlarının verisi aynı noktaya öbür taraftan iniyor: AI bir <strong>büyüteç, kısayol değil.</strong> Güçlü teslimat pratiklerine sahip ekipler ondan daha çok verim alıyor; sahip olmayanlar disfonksiyonlarını daha hızlı sevk ediyor. Her iki rapor da AI benimsemesinin <em>istikrar düşüşüyle ilişkilendiğini</em> gördü — çünkü gerçek kısıt her zaman süreçti, klavyede yazmak değil.</p>\n<p>Büyüteç, ne verirsen onu yükseltir. Temiz bir sinyal verirsen daha yüksek ve net olur; gürültü verirsen yalnızca daha yüksek gürültü. Hangisi olduğuna AI karar vermiyor. Aşağı tarafın veriyor.</p>\n<h2 id=\"darboğaz-gerçekte-nereye-gitti\">Darboğaz gerçekte nereye gitti?</h2>\n<p>Hep pahalı olan ama şimdi boğulan kısımlara taşındı:</p>\n<ul>\n<li><strong>Review.</strong> Üretim, insanların gerçek dikkat verebileceğinden hızlı PR çıkarıyor — böylece review bir lastik damgaya bozuluyor; okunmamış değişikliklerin production’a ulaşma yolu tam da budur.</li>\n<li><strong>Doğrulama.</strong> Test yazmak artık neredeyse bedava; süitler tam da bu yüzden, güveni inşa etmekten hızlı aşındıran <a href=\"/tr/journal/kararsiz-testler-ve-determinizm/\">kararsız, zamana bağımlı testlerle</a> doluyor.</li>\n<li><strong>Entegrasyon.</strong> Daha çok elin daha hızlı yazdığı daha çok kod; daha çok coupling ve <a href=\"/tr/notes/idempotency-ayni-mesaj-iki-kez/\">aynı mesajın ya da isteğin iki kez gelip</a> durumu bozmasının daha çok yolu demek.</li>\n</ul>\n<p>Bunların hiçbiri bir klavye problemi değil. Hepsi, hızlı yazmanın faturası.</p>\n<h2 id=\"kalıcı-yatırım-sıkıcı-olan-aşağı-taraf\">Kalıcı yatırım, sıkıcı olan aşağı taraf</h2>\n<p>Yani bu çağdaki mühendislik üstünlüğü “AI yazdı” değil. <strong>Yazılana güvenmenin maliyetini</strong>, yeni darboğaz boğulmayacak kadar düşük tutmak. O iş cazip değil ve AI’ın geri verdiği zamanı harcayacağım yer orası:</p>\n<ul>\n<li>Doğrulamayı <strong>deterministik</strong> yap; ki yeşil bir kontrol bir anlam taşısın ve review ona yaslanabilsin.</li>\n<li>Entegrasyonu <strong>idempotent ve dayanıklı</strong> yap; ki daha hızlı, daha dağınık değişiklik redelivery’yi bir çift çekime çevirmesin.</li>\n<li>Review’ı <span data-scheduled=\"/tr/journal/code-review-kulturu/\">virgül değil mimari hakkında</span> yap — mekanik kısmı araçlara bırak ki ölçekleyemediğin insan dikkati gerçekten önemli kararlara gitsin.</li>\n<li>Bariz hatayı, üç istasyon sonra CI’da yüz kat pahalıya değil, <strong>commit sınırında</strong> yakala.</li>\n</ul>\n<p>Bu, <a href=\"/tr/journal/vibe-coding-ile-production/\">“vibe coding ilk sürümün maliyetini düşürür, yanılmanın maliyetini değil”</a> dersinin aynısı — tek bir değişiklik yerine tüm hattın seviyesinde okunmuş hâli.</p>\n<h2 id=\"bu-ne-zaman-geçerli-değil\">Bu ne zaman geçerli değil?</h2>\n<p>Gerçek kısıtın <em>hâlâ</em> üretimse — sahici bir greenfield, tek kişilik bir prototip, henüz koruyacak bir aşağı tarafı olmayan bir spike — o zaman hızlı git ve var olmayan bir sistem için süreç kurma. Kayma ancak dürüst tutulacak bir şey olduğunda ısırır: bir ekip, bir review kuyruğu, başkalarının bağımlı olduğu bir production.</p>\n<p>Hangi dünyada olduğunu bilmek, asıl muhakeme. İki kat kod üreten çoğu ekip artık birincisinde değil.</p>\n<hr/>\n<p>AI mühendisliği ucuzlatmadı; <em>bir kısmını</em> ucuzlattı. Disiplin, kazandığın zamanı tam da darboğaza dönüşen kısma harcamak — zaten yığılmakta olandan daha fazlasını üretmeye geri dökmek değil.</p>",
      "content_text": "İzlediğim bir ekip, ajanlara yaslanınca kabaca iki kat kod üretmeye başladı. Altı hafta sonra *daha yavaş* teslim ediyorlardı. Pull-request kuyruğu üçe katlanmış, CI yeşilden çok kırmızıydı ve iki olayın kökü kimsenin gerçekten okumadığı değişikliklere çıkıyordu. Çıktı arttı; throughput düştü.\n\nBunda gizem yok. Hattın tek bir istasyonunu hızlandırıp buna zafer dediler.\n\n## Tek istasyonu hızlandırıp buna throughput diyemezsin\n\nBir teslimat hattı bir istasyon dizisidir: yaz → review → entegre et → doğrula → release. Throughput'u **en yavaş** istasyon belirler, en hızlısı değil. Yazmayı 3× hızlandırırsın ama review ve doğrulama yerinde kalırsa throughput'u üçe katlamış olmazsın — kısıtı yalnızca bir istasyon aşağı taşımış ve onun önünde bir work-in-progress yığını büyütmüş olursun.\n\nO yığın bedavaya gelmez. Merge conflict'e yaşlanan okunmamış PR'lar, yarı entegre branch'ler, kimsenin yeşil tutamayacağı kadar hızlı büyüyen bir test süiti. Kod ucuzlayınca darboğaz yok olmadı. Yer değiştirdi — ve görülmesi zorlaştı, çünkü artık \"kod yazmak\" gibi değil \"beklemek\" gibi görünüyor.\n\n## DORA'nın bunun için zaten bir kelimesi var: büyüteç\n\n[2024](https://dora.dev/research/2024/dora-report/) ve [2025](https://dora.dev/dora-report-2025/) DORA (DevOps Research and Assessment) raporlarının verisi aynı noktaya öbür taraftan iniyor: AI bir **büyüteç, kısayol değil.** Güçlü teslimat pratiklerine sahip ekipler ondan daha çok verim alıyor; sahip olmayanlar disfonksiyonlarını daha hızlı sevk ediyor. Her iki rapor da AI benimsemesinin *istikrar düşüşüyle ilişkilendiğini* gördü — çünkü gerçek kısıt her zaman süreçti, klavyede yazmak değil.\n\nBüyüteç, ne verirsen onu yükseltir. Temiz bir sinyal verirsen daha yüksek ve net olur; gürültü verirsen yalnızca daha yüksek gürültü. Hangisi olduğuna AI karar vermiyor. Aşağı tarafın veriyor.\n\n## Darboğaz gerçekte nereye gitti?\n\nHep pahalı olan ama şimdi boğulan kısımlara taşındı:\n\n- **Review.** Üretim, insanların gerçek dikkat verebileceğinden hızlı PR çıkarıyor — böylece review bir lastik damgaya bozuluyor; okunmamış değişikliklerin production'a ulaşma yolu tam da budur.\n- **Doğrulama.** Test yazmak artık neredeyse bedava; süitler tam da bu yüzden, güveni inşa etmekten hızlı aşındıran [kararsız, zamana bağımlı testlerle](/tr/journal/kararsiz-testler-ve-determinizm) doluyor.\n- **Entegrasyon.** Daha çok elin daha hızlı yazdığı daha çok kod; daha çok coupling ve [aynı mesajın ya da isteğin iki kez gelip](/tr/notes/idempotency-ayni-mesaj-iki-kez) durumu bozmasının daha çok yolu demek.\n\nBunların hiçbiri bir klavye problemi değil. Hepsi, hızlı yazmanın faturası.\n\n## Kalıcı yatırım, sıkıcı olan aşağı taraf\n\nYani bu çağdaki mühendislik üstünlüğü \"AI yazdı\" değil. **Yazılana güvenmenin maliyetini**, yeni darboğaz boğulmayacak kadar düşük tutmak. O iş cazip değil ve AI'ın geri verdiği zamanı harcayacağım yer orası:\n\n- Doğrulamayı **deterministik** yap; ki yeşil bir kontrol bir anlam taşısın ve review ona yaslanabilsin.\n- Entegrasyonu **idempotent ve dayanıklı** yap; ki daha hızlı, daha dağınık değişiklik redelivery'yi bir çift çekime çevirmesin.\n- Review'ı virgül değil mimari hakkında yap — mekanik kısmı araçlara bırak ki ölçekleyemediğin insan dikkati gerçekten önemli kararlara gitsin.\n- Bariz hatayı, üç istasyon sonra CI'da yüz kat pahalıya değil, **commit sınırında** yakala.\n\nBu, [\"vibe coding ilk sürümün maliyetini düşürür, yanılmanın maliyetini değil\"](/tr/journal/vibe-coding-ile-production) dersinin aynısı — tek bir değişiklik yerine tüm hattın seviyesinde okunmuş hâli.\n\n## Bu ne zaman geçerli değil?\n\nGerçek kısıtın *hâlâ* üretimse — sahici bir greenfield, tek kişilik bir prototip, henüz koruyacak bir aşağı tarafı olmayan bir spike — o zaman hızlı git ve var olmayan bir sistem için süreç kurma. Kayma ancak dürüst tutulacak bir şey olduğunda ısırır: bir ekip, bir review kuyruğu, başkalarının bağımlı olduğu bir production.\n\nHangi dünyada olduğunu bilmek, asıl muhakeme. İki kat kod üreten çoğu ekip artık birincisinde değil.\n\n---\n\nAI mühendisliği ucuzlatmadı; *bir kısmını* ucuzlattı. Disiplin, kazandığın zamanı tam da darboğaza dönüşen kısma harcamak — zaten yığılmakta olandan daha fazlasını üretmeye geri dökmek değil.",
      "date_published": "2026-06-17T00:00:00.000Z",
      "tags": [
        "ai-workflow",
        "productivity",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/kararsiz-testler-ve-determinizm/",
      "url": "https://sade.dev/tr/journal/kararsiz-testler-ve-determinizm/",
      "title": "Kararsız Testler: Retry Çözüm Değil",
      "summary": "Retry, quarantine ve bir ticket kararsız bir testi deterministik yapmaz; CI zaten kırıldıktan sonra belirtiyi gizler. Kararsızlığın çoğu az sayıda bilinen anti-pattern'den gelir — sabit bir sleep, seed'siz rastgelelik, duvar saatine dayalı bir assertion, yapısal bir selector — ve her biri test yazıldığı anda diff'te statik olarak görülebilir. En ucuz yakalama yeri o satırdır.",
      "content_html": "<p>İncelememe bir pull request düştü: diff’in yarısı testti ve onları bir model yazmıştı. Yerelde geçiyorlardı. CI’da biri bir kez patladı, yeniden çalıştırınca geçti, iki gün sonra yük altında yine patladı. Koda kimse dokunmamıştı.</p>\n<p>O test yanlış değildi. <strong>Kararsızdı (flaky)</strong> — ve kararsızlık, kendine has bir bozukluk türü.</p>\n<h2 id=\"ai-kararsızlığı-icat-etmedi--seri-üretime-geçirdi\">AI kararsızlığı icat etmedi — seri üretime geçirdi</h2>\n<p>Eskiden test yazmanın bir maliyeti vardı; bu yüzden daha az yazar, her birini daha çok düşünürdün. Model bunları saniyeler içinde, yalnızca kodun o anki görünüşünden yazıyor. Eşzamanlılığı, kaynak limitlerini, bir async çağrının gerçek zamanlamasını göremez — o yüzden <em>doğru görünen</em> ama sessizce deterministik olmayan kalıplara uzanır:</p>\n<ul>\n<li>Async işi “beklemek” için sabit bir sleep: <code>time.Sleep(2 * time.Second)</code>, <code>await page.waitForTimeout(2000)</code>, <code>cy.wait(2000)</code>.</li>\n<li>Her çalıştırmada farklı değer üreten, seed’siz bir rastgele kaynak: <code>Math.random()</code>, <code>rand.Intn(100)</code>, sabit seed’i olmayan bir <code>faker</code>.</li>\n<li>DOM yapısına çakılı, kırılgan bir selector — <code>nth-child</code>, otomatik üretilmiş bir class adı — markup azıcık kayınca kırılır.</li>\n<li>Her şeye bir mock; böylece test bir sonucu değil bir etkileşimi doğrular — <span data-scheduled=\"/tr/journal/sade-test-kulturu/\">hiçbir şey yapmayan testlerle coverage kovalamak</span> yazısında anlattığım o boş yeşilin aynısı.</li>\n</ul>\n<p>Tek tek bakınca her biri makul. Ölçekte bir kararsızlık fabrikası.</p>\n<h2 id=\"süit-büyüdükçe-daha-az-güvenilir-oluyor\">Süit büyüdükçe <em>daha az</em> güvenilir oluyor</h2>\n<p>%0,5 ihtimalle boşuna patlayan tek bir test görünmez. Aynısından 800 tane koy, temiz bir çalıştırmada <em>herhangi</em> bir testin patlama ihtimali 1 − 0,995^800 ≈ %98. Patlamalar birbirinden bağımsız da değil: CI, paylaşımlı ve throttle’lı donanımda koşar; laptop’unda 50 ms’de biten async test, yük altında timeout’a düşer.</p>\n<p>Model test eklemeye devam ettikçe bu sayı tırmanır ve süitin yanlış-kırmızı verme ihtimali kesinliğe doğru yürür. Önemsediğin geçiş, önemsemediğin gürültüde boğulur.</p>\n<h2 id=\"retry-bir-koşu-bandı-tedavi-değil\">Retry bir koşu bandı, tedavi değil</h2>\n<p>Refleks, gürültüyü yutmak: job’u otomatik retry’la, testi flaky işaretle, quarantine’e al, bir ticket aç. Bunların hepsi <strong>reaktif</strong> — test CI’ı zaten bozduktan, çoğu zaman birden çok kez bozduktan <em>sonra</em> devreye girer.</p>\n<ul>\n<li>Retry gerçek CI dakikası yakar ve feedback loop’u uzatır; yeni hiçbir şey öğrenmemek için iki kez ödersin.</li>\n<li>Quarantine, sessizce büyüyen bir devre dışı test yığını üretir. O yığın borçtur ve faiziyle büyür — her atlanan test, artık kimsenin koşmadığı bir kontrol.</li>\n<li>Ticket, backlog gürültüsüne yaşlanır ve sessizce önemini yitirir.</li>\n</ul>\n<p>Hiçbiri testi deterministik yapmaz. Belirtiyi gizler ve bunun bedelini sana keser.</p>\n<h2 id=\"patlamayı-değil-anti-patterni-yakala\">Patlamayı değil, anti-pattern’i yakala</h2>\n<p>Kararsız bir test nadiren ince bir sebepten patlar. Az sayıda, iyi bilinen anti-pattern yüzünden patlar — ve bunlar <strong>test daha çalışmadan, diff’te görünür.</strong> Sabit bir sleep, seed’siz bir RNG, duvar-saatine dayalı bir assertion, yapısal bir selector: her biri, test yazıldığı anda statik ve deterministik olarak tespit edilebilir.</p>\n<p>Ben bu kontrolü commit sınırında çalıştırıyorum — değişen test dosyalarının eklenen satırları üzerinde statik bir tarama, bilinen anti-pattern’leri işaretliyor. Aynı diff, aynı sonuç; model yok, network yok. Akıllı bir race’i yakalamaz, zaten amacı bu değil. Amaç daha dar ama çok değerli: <em>bariz</em> kararsızlığı CI’a hiç ulaşmadan durdurmak — ki orada yakalamak yüz kat pahalı ve bir mühendisin öğleden sonrasına mal oluyor.</p>\n<h2 id=\"testi-tespit-edilecek-bir-şey-kalmayacak-şekilde-tasarla\">Testi, tespit edilecek bir şey kalmayacak şekilde tasarla</h2>\n<p>Tespit, emniyet ağı. Asıl çözüm, testi en baştan deterministik yazmak:</p>\n<ul>\n<li>Sabit beklemeleri <strong>koşula dayalı</strong> beklemeyle değiştir — beklediğin duruma ulaşana kadar yokla ya da çerçevenin <code>Eventually</code>/<code>waitFor</code>’ını kullan. Saati değil, bir olguyu bekle.</li>\n<li>Her rastgele kaynağı <strong>seed</strong>’le; test ortasında <code>now()</code> ve <code>Math.random()</code> okumak yerine deterministik bir saat ve ID enjekte et.</li>\n<li>DOM pozisyonuna değil, <strong>role veya test id</strong>’ye göre seç.</li>\n<li>Logic’i değil, <strong>sınırı</strong> mock’la — ve en az bir gerçek integration yolunu koru, yoksa süit var olmayan bir dünyayı doğrular.</li>\n</ul>\n<h2 id=\"bu-kalıp-ne-zaman-doğru-cevap-olmaktan-çıkar\">Bu kalıp ne zaman doğru cevap olmaktan çıkar?</h2>\n<p>Statik bir kapı isabet-önceliklidir: bariz anti-pattern’leri yakalar, kendi kodunun içine gömülü bir race condition’ı değil. Kurallarını muhafazakâr tut — gürültülü bir kapı yok sayılır, yok sayılan bir kapı ise hiç olmamasından kötüdür. Akıllı kararsızlığı kaçıracaktır; tahmin etmesine izin verme.</p>\n<p>Ve bunların hiçbiri daha zor olan okumanın yerine geçmez: <em>test edilen sistem</em> deterministik olmadığı için kararsız olan bir test, sana test hakkında bir şey söylemiyor. Sana sistem hakkında bir şey söylüyor.</p>\n<hr/>\n<p>Güvenmek için yeniden çalıştırman gereken yeşil bir CI, yeşil değildir. Kararsız bir testi öldürmenin en ucuz yeri, yazıldığı satır — sana bir build’e mal olmadan önce.</p>\n<hr/>\n<p><em>İlgili:</em> burada anlatılan commit-sınırı kontrolünü <a href=\"https://commitbrief.com\">CommitBrief</a>’te shipledim — <a href=\"https://github.com/CommitBrief/commitbrief\">flaky-test dedektörü</a> değişen test dosyalarındaki sabit sleep’leri ve seed’siz rastgeleliği, modelden önce, deterministik olarak statik yakalar.</p>",
      "content_text": "İncelememe bir pull request düştü: diff'in yarısı testti ve onları bir model yazmıştı. Yerelde geçiyorlardı. CI'da biri bir kez patladı, yeniden çalıştırınca geçti, iki gün sonra yük altında yine patladı. Koda kimse dokunmamıştı.\n\nO test yanlış değildi. **Kararsızdı (flaky)** — ve kararsızlık, kendine has bir bozukluk türü.\n\n## AI kararsızlığı icat etmedi — seri üretime geçirdi\n\nEskiden test yazmanın bir maliyeti vardı; bu yüzden daha az yazar, her birini daha çok düşünürdün. Model bunları saniyeler içinde, yalnızca kodun o anki görünüşünden yazıyor. Eşzamanlılığı, kaynak limitlerini, bir async çağrının gerçek zamanlamasını göremez — o yüzden *doğru görünen* ama sessizce deterministik olmayan kalıplara uzanır:\n\n- Async işi \"beklemek\" için sabit bir sleep: `time.Sleep(2 * time.Second)`, `await page.waitForTimeout(2000)`, `cy.wait(2000)`.\n- Her çalıştırmada farklı değer üreten, seed'siz bir rastgele kaynak: `Math.random()`, `rand.Intn(100)`, sabit seed'i olmayan bir `faker`.\n- DOM yapısına çakılı, kırılgan bir selector — `nth-child`, otomatik üretilmiş bir class adı — markup azıcık kayınca kırılır.\n- Her şeye bir mock; böylece test bir sonucu değil bir etkileşimi doğrular — hiçbir şey yapmayan testlerle coverage kovalamak yazısında anlattığım o boş yeşilin aynısı.\n\nTek tek bakınca her biri makul. Ölçekte bir kararsızlık fabrikası.\n\n## Süit büyüdükçe *daha az* güvenilir oluyor\n\n%0,5 ihtimalle boşuna patlayan tek bir test görünmez. Aynısından 800 tane koy, temiz bir çalıştırmada *herhangi* bir testin patlama ihtimali 1 − 0,995^800 ≈ %98. Patlamalar birbirinden bağımsız da değil: CI, paylaşımlı ve throttle'lı donanımda koşar; laptop'unda 50 ms'de biten async test, yük altında timeout'a düşer.\n\nModel test eklemeye devam ettikçe bu sayı tırmanır ve süitin yanlış-kırmızı verme ihtimali kesinliğe doğru yürür. Önemsediğin geçiş, önemsemediğin gürültüde boğulur.\n\n## Retry bir koşu bandı, tedavi değil\n\nRefleks, gürültüyü yutmak: job'u otomatik retry'la, testi flaky işaretle, quarantine'e al, bir ticket aç. Bunların hepsi **reaktif** — test CI'ı zaten bozduktan, çoğu zaman birden çok kez bozduktan *sonra* devreye girer.\n\n- Retry gerçek CI dakikası yakar ve feedback loop'u uzatır; yeni hiçbir şey öğrenmemek için iki kez ödersin.\n- Quarantine, sessizce büyüyen bir devre dışı test yığını üretir. O yığın borçtur ve faiziyle büyür — her atlanan test, artık kimsenin koşmadığı bir kontrol.\n- Ticket, backlog gürültüsüne yaşlanır ve sessizce önemini yitirir.\n\nHiçbiri testi deterministik yapmaz. Belirtiyi gizler ve bunun bedelini sana keser.\n\n## Patlamayı değil, anti-pattern'i yakala\n\nKararsız bir test nadiren ince bir sebepten patlar. Az sayıda, iyi bilinen anti-pattern yüzünden patlar — ve bunlar **test daha çalışmadan, diff'te görünür.** Sabit bir sleep, seed'siz bir RNG, duvar-saatine dayalı bir assertion, yapısal bir selector: her biri, test yazıldığı anda statik ve deterministik olarak tespit edilebilir.\n\nBen bu kontrolü commit sınırında çalıştırıyorum — değişen test dosyalarının eklenen satırları üzerinde statik bir tarama, bilinen anti-pattern'leri işaretliyor. Aynı diff, aynı sonuç; model yok, network yok. Akıllı bir race'i yakalamaz, zaten amacı bu değil. Amaç daha dar ama çok değerli: *bariz* kararsızlığı CI'a hiç ulaşmadan durdurmak — ki orada yakalamak yüz kat pahalı ve bir mühendisin öğleden sonrasına mal oluyor.\n\n## Testi, tespit edilecek bir şey kalmayacak şekilde tasarla\n\nTespit, emniyet ağı. Asıl çözüm, testi en baştan deterministik yazmak:\n\n- Sabit beklemeleri **koşula dayalı** beklemeyle değiştir — beklediğin duruma ulaşana kadar yokla ya da çerçevenin `Eventually`/`waitFor`'ını kullan. Saati değil, bir olguyu bekle.\n- Her rastgele kaynağı **seed**'le; test ortasında `now()` ve `Math.random()` okumak yerine deterministik bir saat ve ID enjekte et.\n- DOM pozisyonuna değil, **role veya test id**'ye göre seç.\n- Logic'i değil, **sınırı** mock'la — ve en az bir gerçek integration yolunu koru, yoksa süit var olmayan bir dünyayı doğrular.\n\n## Bu kalıp ne zaman doğru cevap olmaktan çıkar?\n\nStatik bir kapı isabet-önceliklidir: bariz anti-pattern'leri yakalar, kendi kodunun içine gömülü bir race condition'ı değil. Kurallarını muhafazakâr tut — gürültülü bir kapı yok sayılır, yok sayılan bir kapı ise hiç olmamasından kötüdür. Akıllı kararsızlığı kaçıracaktır; tahmin etmesine izin verme.\n\nVe bunların hiçbiri daha zor olan okumanın yerine geçmez: *test edilen sistem* deterministik olmadığı için kararsız olan bir test, sana test hakkında bir şey söylemiyor. Sana sistem hakkında bir şey söylüyor.\n\n---\n\nGüvenmek için yeniden çalıştırman gereken yeşil bir CI, yeşil değildir. Kararsız bir testi öldürmenin en ucuz yeri, yazıldığı satır — sana bir build'e mal olmadan önce.\n\n---\n\n*İlgili:* burada anlatılan commit-sınırı kontrolünü [CommitBrief](https://commitbrief.com)'te shipledim — [flaky-test dedektörü](https://github.com/CommitBrief/commitbrief) değişen test dosyalarındaki sabit sleep'leri ve seed'siz rastgeleliği, modelden önce, deterministik olarak statik yakalar.",
      "date_published": "2026-06-17T00:00:00.000Z",
      "tags": [
        "testing",
        "ci",
        "ai",
        "quality",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/sema-evrimi-sozlesmeyi-kirmadan/",
      "url": "https://sade.dev/tr/journal/sema-evrimi-sozlesmeyi-kirmadan/",
      "title": "Şema Evrimi: Sözleşmeyi Kırmadan Değiştirmek",
      "summary": "Servisler arası bir sözleşmeyi atomik değiştiremezsin; tek güvenli değişiklik eskiyle yeninin bir arada var olabildiğidir: gevşetmek güvenli, sıkılaştırmak değil. Opsiyonel alan eklemek ya da bir required kısıtını kaldırmak eski datayı geçerli bırakır; zorunlu alan eklemek, yeniden adlandırmak, tip değiştirmek ya da additionalProperties'i kapatmak bırakmaz. Kırıcıysa yeni bir kimlik üret ve önce tüketicileri yükselt.",
      "content_html": "<p>Biri bir olaya zorunlu bir <code>customer_id</code> alanı ekledi ve üreticiyi deploy etti. Tüketicilerin yarısı henüz bu değişikliği shiplememişti. Yirmi dakika boyunca, eski tüketicilerin bir yıldır sorunsuz işlediği her mesaj birden validation’dan kalmaya başladı — ta ki rollout tamamlanana kadar. Hiçbir kod “yanlış” değildi. Bir sözleşme, ona bağlı olanların altından çekilip değiştirildi.</p>\n<p>Bir mesajın <code>data</code> şekli bir API’dir. Response body yerine queue’da JSON olması onu daha az sözleşme yapmaz — <em>daha zor</em> bir sözleşme yapar, çünkü iki tarafı aynı anda değiştiremezsin.</p>\n<h2 id=\"dağıtık-bir-sözleşmeyi-atomik-değiştiremezsin\">Dağıtık bir sözleşmeyi atomik değiştiremezsin</h2>\n<p>HTTP API’sinin kontrol ettiğin tek bir sunucusu vardır. Yayınlanan bir mesajın ise <strong>bağımsız deploy edilen, kendi takvimlerinde olan birçok tüketicisi</strong> vardır — ve at-least-once teslimatla, eski şekille üretilmiş yoldaki mesajlar, sen yenisini yayarken hâlâ geliyordur. “Herkesin yeni şemada olduğu” bir an yoktur. Yani tek güvenli değişiklikler, <strong>eski ve yeninin bir arada var olabildiği</strong> değişikliklerdir.</p>\n<p>Bu tek kısıt sana tüm kural setini verir.</p>\n<h2 id=\"ne-güvenli-ne-değil\">Ne güvenli, ne değil?</h2>\n<p>Bir değişiklik, eski şema altında geçerli olan data yeni şema altında da geçerliyse backward-compatible’dır — böylece erken yükselten bir tüketici, henüz yükseltmemiş bir üreticinin yaydığı mesajları hâlâ kabul eder.</p>\n<ul>\n<li><strong>Opsiyonel bir alan ekle</strong> — güvenli. Eski data onsuz olur, olur.</li>\n<li><strong>Bir <code>required</code> kısıtını kaldır</strong> (alanı opsiyonel yap) — güvenli. Eski data daha gevşek kuralı zaten karşılar.</li>\n<li><strong>Bir <code>enum</code>’ı genişlet, bir <code>minimum</code>’u gevşet</strong> — güvenli.</li>\n</ul>\n<p>Ve ısıranlar:</p>\n<ul>\n<li><strong>Zorunlu bir alan ekle</strong> ya da <strong>opsiyonel bir alanı zorunlu yap</strong> — kırıcı. Eski data onu içermez.</li>\n<li><strong>Bir alanı kaldır, yeniden adlandır ya da tipini değiştir</strong> — kırıcı. Yeniden adlandırma, bir kaldırma artı bir eklemedir.</li>\n<li><strong>Kuralları sıkılaştır</strong> — bir <code>enum</code> değerini düşür, bir <code>minimum</code>’u yükselt, <code>additionalProperties</code>’i kapat — kırıcı.</li>\n</ul>\n<p>Asimetri asıl mesele: <strong>gevşetmek güvenli, sıkılaştırmak değil.</strong> Bir tüketici, beklediğinden daha hoşgörülü bir data’yı, artık talep ettiği bir şeyi eksik bir data’ya göre çok daha kolay tolere eder.</p>\n<h2 id=\"kırıcıysa-kimliği-sürümle--değiştirme\">Kırıcıysa, kimliği sürümle — değiştirme</h2>\n<p>Kırıcı bir değişiklikte içgüdü “şemayı güncelleyiver” demek. Deme. Mevcut bir kimliğin arkasındaki şekli değiştirmek, yukarıdaki rollout’u kıran şeyin ta kendisi. Bunun yerine <strong>yeni bir kimlik üret</strong> — yeni bir mesaj URN’i (<code>urn:babel:orders:created.v2</code>), yeni bir topic, yeni bir olay adı — ve ikisini paralel koştur:</p>\n<ol>\n<li>Üreticiler <code>v1</code> yaymaya devam eder; sen <code>v2</code>’yi yanında yayınlarsın.</li>\n<li>Tüketiciler kendi takvimlerinde <code>v2</code>’ye göç eder.</li>\n<li><code>v1</code>’in tüketicisi kalmayınca onu emekliye ayır.</li>\n</ol>\n<p>Her adımın altındaki kural: <strong>önce tüketiciler yükseltir.</strong> Hiçbir deploy edilmiş tüketicinin anlamadığı bir sürümü asla yayma. (Bu, <a href=\"/tr/notes/idempotency-ayni-mesaj-iki-kez/\">bir mükerrer teslimat no-op olacak şekilde handler tasarlamanın</a> aynı şekli — sırayı kontrol etmediğin için, değişikliği her sırada uygulanabilir kılarsın.)</p>\n<h2 id=\"kuralı-mekanik-hâle-getir\">Kuralı mekanik hâle getir</h2>\n<p>Bunların hiçbiri, cuma akşamı 5’te bir code review’da yeniden türetmek isteyeceğin bir yargı değil. Yukarıdaki uyumluluk kuralları deterministik — eski ve yeni şema verildiğinde, bir araç sana “additive, ship et” ya da “kırıcı, yeni sürüm üret” diyebilir, hiçbir görüş katmadan. O yüzden ben bunu kapıya koyuyorum: sınırda iki şemayı diff’leyip kırıcı değişiklikte build’i kıran bir kontrol — aşağı tarafa ulaşmadan önce. Orada yakalamak bir PR yorumuna mal olur; production’da yakalamak yukarıdakine benzer bir rollout penceresine.</p>\n<p>Bu, <a href=\"/tr/journal/ai-darbogazi-kaldirmadi/\">“darboğaz aşağı kaydı”</a> tezinin sıkıcı-altyapı ucu: şema ucuza değiştirilir ama <em>yanlış</em> değiştirmek pahalıdır, o yüzden bir parça araçla o ikinci maliyeti masadan kaldırırsın.</p>\n<h2 id=\"bu-ne-zaman-önemsizleşir\">Bu ne zaman önemsizleşir?</h2>\n<p>Bir mesajın tam olarak tek bir üreticisi ve birlikte deploy olan tek bir tüketicisi varsa — tek bir servisin iç queue’su — sözleşme aslında dağıtık değildir; iki tarafı aynı anda değiştirebilirsin. O zaman bu tören fazlalıktır. Kurallar, <em>ikinci</em>, bağımsız deploy edilen bir tüketici var olduğu an karşılığını vermeye başlar. İlk ayını aşan çoğu olay o noktaya ulaşır.</p>\n<hr/>\n<p>Bir şema sahip olduğun bir struct değildir; başka servislerin etrafında plan yaptığı bir sözdür. Onu, geri alamayacağın herhangi bir sözü nasıl evrimleştirirsen öyle evrimleştir — additive olarak ya da yeni bir ad altında. Asla eskisini sessizce yeniden tanımlayarak değil.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://babelqueue.com/docs/spec/1.x/schema-validation\">BabelQueue schema-validation spec’i</a> bu uyumluluk kurallarını yazıya döker; <a href=\"https://github.com/BabelQueue/babelqueue-registry\">babelqueue-registry</a> URN-başına şemaların yaşadığı yerdir — onun <code>bqschema</code> aracı (ve paketlenmiş Action’ı) kırıcı değişiklikte build’i kıran sınır kontrolüdür.</p>",
      "content_text": "Biri bir olaya zorunlu bir `customer_id` alanı ekledi ve üreticiyi deploy etti. Tüketicilerin yarısı henüz bu değişikliği shiplememişti. Yirmi dakika boyunca, eski tüketicilerin bir yıldır sorunsuz işlediği her mesaj birden validation'dan kalmaya başladı — ta ki rollout tamamlanana kadar. Hiçbir kod \"yanlış\" değildi. Bir sözleşme, ona bağlı olanların altından çekilip değiştirildi.\n\nBir mesajın `data` şekli bir API'dir. Response body yerine queue'da JSON olması onu daha az sözleşme yapmaz — *daha zor* bir sözleşme yapar, çünkü iki tarafı aynı anda değiştiremezsin.\n\n## Dağıtık bir sözleşmeyi atomik değiştiremezsin\n\nHTTP API'sinin kontrol ettiğin tek bir sunucusu vardır. Yayınlanan bir mesajın ise **bağımsız deploy edilen, kendi takvimlerinde olan birçok tüketicisi** vardır — ve at-least-once teslimatla, eski şekille üretilmiş yoldaki mesajlar, sen yenisini yayarken hâlâ geliyordur. \"Herkesin yeni şemada olduğu\" bir an yoktur. Yani tek güvenli değişiklikler, **eski ve yeninin bir arada var olabildiği** değişikliklerdir.\n\nBu tek kısıt sana tüm kural setini verir.\n\n## Ne güvenli, ne değil?\n\nBir değişiklik, eski şema altında geçerli olan data yeni şema altında da geçerliyse backward-compatible'dır — böylece erken yükselten bir tüketici, henüz yükseltmemiş bir üreticinin yaydığı mesajları hâlâ kabul eder.\n\n- **Opsiyonel bir alan ekle** — güvenli. Eski data onsuz olur, olur.\n- **Bir `required` kısıtını kaldır** (alanı opsiyonel yap) — güvenli. Eski data daha gevşek kuralı zaten karşılar.\n- **Bir `enum`'ı genişlet, bir `minimum`'u gevşet** — güvenli.\n\nVe ısıranlar:\n\n- **Zorunlu bir alan ekle** ya da **opsiyonel bir alanı zorunlu yap** — kırıcı. Eski data onu içermez.\n- **Bir alanı kaldır, yeniden adlandır ya da tipini değiştir** — kırıcı. Yeniden adlandırma, bir kaldırma artı bir eklemedir.\n- **Kuralları sıkılaştır** — bir `enum` değerini düşür, bir `minimum`'u yükselt, `additionalProperties`'i kapat — kırıcı.\n\nAsimetri asıl mesele: **gevşetmek güvenli, sıkılaştırmak değil.** Bir tüketici, beklediğinden daha hoşgörülü bir data'yı, artık talep ettiği bir şeyi eksik bir data'ya göre çok daha kolay tolere eder.\n\n## Kırıcıysa, kimliği sürümle — değiştirme\n\nKırıcı bir değişiklikte içgüdü \"şemayı güncelleyiver\" demek. Deme. Mevcut bir kimliğin arkasındaki şekli değiştirmek, yukarıdaki rollout'u kıran şeyin ta kendisi. Bunun yerine **yeni bir kimlik üret** — yeni bir mesaj URN'i (`urn:babel:orders:created.v2`), yeni bir topic, yeni bir olay adı — ve ikisini paralel koştur:\n\n1. Üreticiler `v1` yaymaya devam eder; sen `v2`'yi yanında yayınlarsın.\n2. Tüketiciler kendi takvimlerinde `v2`'ye göç eder.\n3. `v1`'in tüketicisi kalmayınca onu emekliye ayır.\n\nHer adımın altındaki kural: **önce tüketiciler yükseltir.** Hiçbir deploy edilmiş tüketicinin anlamadığı bir sürümü asla yayma. (Bu, [bir mükerrer teslimat no-op olacak şekilde handler tasarlamanın](/tr/notes/idempotency-ayni-mesaj-iki-kez) aynı şekli — sırayı kontrol etmediğin için, değişikliği her sırada uygulanabilir kılarsın.)\n\n## Kuralı mekanik hâle getir\n\nBunların hiçbiri, cuma akşamı 5'te bir code review'da yeniden türetmek isteyeceğin bir yargı değil. Yukarıdaki uyumluluk kuralları deterministik — eski ve yeni şema verildiğinde, bir araç sana \"additive, ship et\" ya da \"kırıcı, yeni sürüm üret\" diyebilir, hiçbir görüş katmadan. O yüzden ben bunu kapıya koyuyorum: sınırda iki şemayı diff'leyip kırıcı değişiklikte build'i kıran bir kontrol — aşağı tarafa ulaşmadan önce. Orada yakalamak bir PR yorumuna mal olur; production'da yakalamak yukarıdakine benzer bir rollout penceresine.\n\nBu, [\"darboğaz aşağı kaydı\"](/tr/journal/ai-darbogazi-kaldirmadi) tezinin sıkıcı-altyapı ucu: şema ucuza değiştirilir ama *yanlış* değiştirmek pahalıdır, o yüzden bir parça araçla o ikinci maliyeti masadan kaldırırsın.\n\n## Bu ne zaman önemsizleşir?\n\nBir mesajın tam olarak tek bir üreticisi ve birlikte deploy olan tek bir tüketicisi varsa — tek bir servisin iç queue'su — sözleşme aslında dağıtık değildir; iki tarafı aynı anda değiştirebilirsin. O zaman bu tören fazlalıktır. Kurallar, *ikinci*, bağımsız deploy edilen bir tüketici var olduğu an karşılığını vermeye başlar. İlk ayını aşan çoğu olay o noktaya ulaşır.\n\n---\n\nBir şema sahip olduğun bir struct değildir; başka servislerin etrafında plan yaptığı bir sözdür. Onu, geri alamayacağın herhangi bir sözü nasıl evrimleştirirsen öyle evrimleştir — additive olarak ya da yeni bir ad altında. Asla eskisini sessizce yeniden tanımlayarak değil.\n\n---\n\n*İlgili:* [BabelQueue schema-validation spec'i](https://babelqueue.com/docs/spec/1.x/schema-validation) bu uyumluluk kurallarını yazıya döker; [babelqueue-registry](https://github.com/BabelQueue/babelqueue-registry) URN-başına şemaların yaşadığı yerdir — onun `bqschema` aracı (ve paketlenmiş Action'ı) kırıcı değişiklikte build'i kıran sınır kontrolüdür.",
      "date_published": "2026-06-17T00:00:00.000Z",
      "tags": [
        "architecture",
        "schema",
        "queue",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/katmanli-mimari-mi-clean-architecture-mi/",
      "url": "https://sade.dev/tr/journal/katmanli-mimari-mi-clean-architecture-mi/",
      "title": "Katmanlı Mimari mi, Clean Architecture mı?",
      "summary": "Clean Architecture ile katmanlı mimari doğru ve yanlış cevaplar değil; farklı ağırlıklar ve seçimi domain yapıyor. Çoğu web uygulaması özünde CRUD'dur; orada clean code uğruna Eloquent'e karşı savaşmak kazanılandan pahalıya gelir. Dolaylılık, iş kurallarının framework'ten uzun yaşadığı yerde kendini öder — ve bu ikili bir seçim değil: karmaşık tek bir modül onu taşırken kod tabanının geri kalanı düz kalabilir.",
      "content_html": "<p>Bir Laravel projesini ilk açtığımda, tek bir “kullanıcı oluştur” işleminin yedi dosyaya yayıldığını gördüm: Controller, Request DTO, UseCase, Domain Entity, Repository Interface, Eloquent Repository ve iki yönlü bir Mapper. İçindeki tek iş kuralı, e-postanın benzersiz olmasıydı.</p>\n<p>Bu, Clean Architecture’ın yanlış uygulanmış hâliydi — ama suç Clean Architecture’da değil. Suç, hangi projenin hangi disipline ihtiyacı olduğunu hiç sormamakta.</p>\n<h2 id=\"i̇ki-yaklaşım-kısaca\">İki yaklaşım, kısaca</h2>\n<p><strong>Katmanlı (layered) mimari</strong> tanıdık olandır: Controller → Service → Repository → veritabanı. Bağımlılıklar yukarıdan aşağı akar, en altta framework durur. Laravel’in kendisi de zaten budur — Eloquent bir active record, controller’lar HTTP’ye bağlı, her şey framework’e gömülü.</p>\n<p><strong>Clean Architecture</strong> (ve akrabaları hexagonal, onion) bağımlılık yönünü tersine çevirir: merkezde framework’ten habersiz domain durur, bağımlılıklar <strong>içeri</strong> doğru bakar. Veritabanı, HTTP, framework — hepsi en dıştaki halkada, birer detay. Domain, Laravel’in var olduğunu bilmez.</p>\n<p>Fark felsefi değil, çok somut: katmanlı mimaride domain Eloquent’e bağımlıdır; Clean’de Eloquent domain’e bağımlıdır.</p>\n<h2 id=\"clean-architecture-neyi-satın-alır\">Clean Architecture neyi satın alır?</h2>\n<p>Bedava değil, ama gerçek şeyler verir:</p>\n<ul>\n<li><strong>Framework’ten bağımsız test.</strong> Domain kuralları, veritabanı olmadan, saf birim testlerle koşulur. Hızlı; kırılgan değil.</li>\n<li><strong>Framework’ün ömründen uzun domain.</strong> İş kuralları, Laravel sürümlerinden bağımsız yaşar. On yıllık bir domain, üç major framework upgrade’i görebilir.</li>\n<li><strong>Birden çok teslim mekanizması.</strong> Aynı domain’i HTTP API, CLI komutu ve queue worker üzerinden çağırmak doğallaşır.</li>\n</ul>\n<p>Karşılığında ödediğiniz de somut: her katman geçişinde mapping, daha çok dosya, daha çok dolaylılık. Tek satırlık bir kural için yedi dosya — yazının başındaki manzara.</p>\n<h2 id=\"çoğu-laravel-projesi-için-katmanlı-yeter\">Çoğu Laravel projesi için katmanlı yeter</h2>\n<p>Açık konuşayım: çoğu web uygulaması özünde CRUD’dur. Veriyi doğrula, kaydet, oku, göster. Böyle bir uygulamada Eloquent’e karşı “clean code” uğruna savaşmak, kazanılan değerden pahalıdır.</p>\n<p>Laravel’i bir detay değil, bilinçle seçilmiş bir temel olarak kabul edin. Controller + Form Request + Eloquent model + ince bir service sınıfı — bu, projelerin büyük çoğunluğu için doğru ağırlıktır. Framework’ü kucaklamak bir ödün değil, bir karardır; tıpkı <a href=\"/tr/journal/neden-boring-architecture/\">boring architecture</a> gibi.</p>\n<p>Yedi dosyalık bir CRUD ise <a href=\"/tr/journal/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> mimari ölçekteki hâlidir: bugün olmayan bir karmaşıklık için bugünden vergi ödemek.</p>\n<h2 id=\"clean-architecture-ne-zaman-karşılığını-verir\">Clean Architecture ne zaman karşılığını verir?</h2>\n<p>Disiplin, domain karmaşıklığı yüksek olduğunda gerçek getiri sağlar. Şu sinyaller varsa ciddiye alın:</p>\n<ul>\n<li>İş kuralları “kaydet/oku”nun çok ötesinde: çok adımlı hesaplamalar, durum makineleri, sektörel düzenlemeler.</li>\n<li>Bu kuralların framework’ten uzun yaşaması bekleniyor.</li>\n<li>Aynı domain birden fazla arayüzden besleniyor — API, CLI, mesaj kuyruğu, panel.</li>\n<li>Kural değişiklikleri sık ve riskli; veritabanına dokunmadan test edebilmek gerçek bir hız kazandırıyor.</li>\n</ul>\n<p>Sigorta primlendirme, muhasebe, lojistik optimizasyonu — bu alanlarda Clean Architecture’ın dolaylılığı kendini öder. Bir blog ya da standart bir yönetim panelinde ödemez.</p>\n<h2 id=\"hep-ya-da-hiç-değil\">Hep ya da hiç değil</h2>\n<p>En sık atlanan nokta: bu bir ikili seçim değil. Aynı kod tabanında ikisi bir arada yaşayabilir.</p>\n<p>Domain’in gerçekten karmaşık olduğu modülü — diyelim fiyatlandırma — Clean tarzı izole edin: saf domain, interface’le ayrılmış repository, framework’ten bağımsız testler. Geri kalan CRUD modüllerini düz bırakın: controller, Eloquent, ince service. <a href=\"/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum/\">Modüler monolitin</a> sağladığı sınırlar bunu doğal kılar — her modül kendi ağırlığını taşır.</p>\n<p>Mimari, projeye tek tip dağıtılan bir kural değil; karmaşıklığın yoğunlaştığı yere göre ayarlanan bir bütçedir.</p>\n<hr/>\n<p>Clean Architecture bir rozet değil, bir araç. Domain’iniz onun çözdüğü problemi yaşıyorsa paha biçilmez; yaşamıyorsa, tek satırlık bir kuralı yedi dosyaya bölen bir törenden ibaret.</p>\n<p>Mimariyi domain seçer — moda değil.</p>",
      "content_text": "Bir Laravel projesini ilk açtığımda, tek bir \"kullanıcı oluştur\" işleminin yedi dosyaya yayıldığını gördüm: Controller, Request DTO, UseCase, Domain Entity, Repository Interface, Eloquent Repository ve iki yönlü bir Mapper. İçindeki tek iş kuralı, e-postanın benzersiz olmasıydı.\n\nBu, Clean Architecture'ın yanlış uygulanmış hâliydi — ama suç Clean Architecture'da değil. Suç, hangi projenin hangi disipline ihtiyacı olduğunu hiç sormamakta.\n\n## İki yaklaşım, kısaca\n\n**Katmanlı (layered) mimari** tanıdık olandır: Controller → Service → Repository → veritabanı. Bağımlılıklar yukarıdan aşağı akar, en altta framework durur. Laravel'in kendisi de zaten budur — Eloquent bir active record, controller'lar HTTP'ye bağlı, her şey framework'e gömülü.\n\n**Clean Architecture** (ve akrabaları hexagonal, onion) bağımlılık yönünü tersine çevirir: merkezde framework'ten habersiz domain durur, bağımlılıklar **içeri** doğru bakar. Veritabanı, HTTP, framework — hepsi en dıştaki halkada, birer detay. Domain, Laravel'in var olduğunu bilmez.\n\nFark felsefi değil, çok somut: katmanlı mimaride domain Eloquent'e bağımlıdır; Clean'de Eloquent domain'e bağımlıdır.\n\n## Clean Architecture neyi satın alır?\n\nBedava değil, ama gerçek şeyler verir:\n\n- **Framework'ten bağımsız test.** Domain kuralları, veritabanı olmadan, saf birim testlerle koşulur. Hızlı; kırılgan değil.\n- **Framework'ün ömründen uzun domain.** İş kuralları, Laravel sürümlerinden bağımsız yaşar. On yıllık bir domain, üç major framework upgrade'i görebilir.\n- **Birden çok teslim mekanizması.** Aynı domain'i HTTP API, CLI komutu ve queue worker üzerinden çağırmak doğallaşır.\n\nKarşılığında ödediğiniz de somut: her katman geçişinde mapping, daha çok dosya, daha çok dolaylılık. Tek satırlık bir kural için yedi dosya — yazının başındaki manzara.\n\n## Çoğu Laravel projesi için katmanlı yeter\n\nAçık konuşayım: çoğu web uygulaması özünde CRUD'dur. Veriyi doğrula, kaydet, oku, göster. Böyle bir uygulamada Eloquent'e karşı \"clean code\" uğruna savaşmak, kazanılan değerden pahalıdır.\n\nLaravel'i bir detay değil, bilinçle seçilmiş bir temel olarak kabul edin. Controller + Form Request + Eloquent model + ince bir service sınıfı — bu, projelerin büyük çoğunluğu için doğru ağırlıktır. Framework'ü kucaklamak bir ödün değil, bir karardır; tıpkı [boring architecture](/tr/journal/neden-boring-architecture) gibi.\n\nYedi dosyalık bir CRUD ise [spekülatif genelliğin](/tr/journal/ileride-lazim-olur-kodunun-faturasi) mimari ölçekteki hâlidir: bugün olmayan bir karmaşıklık için bugünden vergi ödemek.\n\n## Clean Architecture ne zaman karşılığını verir?\n\nDisiplin, domain karmaşıklığı yüksek olduğunda gerçek getiri sağlar. Şu sinyaller varsa ciddiye alın:\n\n- İş kuralları \"kaydet/oku\"nun çok ötesinde: çok adımlı hesaplamalar, durum makineleri, sektörel düzenlemeler.\n- Bu kuralların framework'ten uzun yaşaması bekleniyor.\n- Aynı domain birden fazla arayüzden besleniyor — API, CLI, mesaj kuyruğu, panel.\n- Kural değişiklikleri sık ve riskli; veritabanına dokunmadan test edebilmek gerçek bir hız kazandırıyor.\n\nSigorta primlendirme, muhasebe, lojistik optimizasyonu — bu alanlarda Clean Architecture'ın dolaylılığı kendini öder. Bir blog ya da standart bir yönetim panelinde ödemez.\n\n## Hep ya da hiç değil\n\nEn sık atlanan nokta: bu bir ikili seçim değil. Aynı kod tabanında ikisi bir arada yaşayabilir.\n\nDomain'in gerçekten karmaşık olduğu modülü — diyelim fiyatlandırma — Clean tarzı izole edin: saf domain, interface'le ayrılmış repository, framework'ten bağımsız testler. Geri kalan CRUD modüllerini düz bırakın: controller, Eloquent, ince service. [Modüler monolitin](/tr/journal/projelere-neden-moduler-monolit-ile-basliyorum) sağladığı sınırlar bunu doğal kılar — her modül kendi ağırlığını taşır.\n\nMimari, projeye tek tip dağıtılan bir kural değil; karmaşıklığın yoğunlaştığı yere göre ayarlanan bir bütçedir.\n\n---\n\nClean Architecture bir rozet değil, bir araç. Domain'iniz onun çözdüğü problemi yaşıyorsa paha biçilmez; yaşamıyorsa, tek satırlık bir kuralı yedi dosyaya bölen bir törenden ibaret.\n\nMimariyi domain seçer — moda değil.",
      "date_published": "2026-06-13T00:00:00.000Z",
      "tags": [
        "architecture",
        "clean-architecture",
        "simplicity",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    },
    {
      "id": "https://sade.dev/tr/journal/vibe-coding-ile-production/",
      "url": "https://sade.dev/tr/journal/vibe-coding-ile-production/",
      "title": "Vibe Coding ile Production'a Çıkılır mı?",
      "summary": "Vibe coding ilk sürümün maliyetini düşürdü; veri modeli, güvenlik sınırı ve failure mode hakkında yanılmanın maliyetini değil. O maliyet ertelenir, silinmez — ve ertelenen maliyet faiz işletir; fatura build anında değil, production'da gelir. Çıktının kalitesini belirleyen yazma hızı değil, sorulan sorunun ve yapılan review'un kalitesidir.",
      "content_html": "<p>“Yerelde çalışıyordu” cümlesinin yeni bir akrabası var: “demoda çalışıyordu.” Bir hafta önce tarif edilip dakikalar içinde ayağa kalkan, ilk kullanıcılarını bile almış bir ürün — sonra ilk gerçek yük geldiğinde yavaşlıyor, ilk meraklı kullanıcı başkasının faturasını görüyor, ilk şema değişikliği migration’da patlıyor. Üreten kişi şaşırıyor: kod çalışıyordu.</p>\n<p>Çalışıyordu. Ama vibe coding’in düşürdüğü maliyet <strong>üretim</strong>di; yanlış olmanın maliyeti değil. O maliyet ertelenir, silinmez — ve ertelenen maliyet faiz işletir.</p>\n<h2 id=\"vibe-codingin-ucuzlattığı-şey-gerçek\">Vibe coding’in ucuzlattığı şey gerçek</h2>\n<p>Strawman kurmayalım. Bir ilk sürümü çıkarmak hiç bu kadar ucuz olmamıştı: bir CRUD ekranı, bir REST uç noktası, bir form doğrulaması — tarif et, çalışsın. Prototip, throwaway, iç araç, öğrenme projesi için bu mükemmel bir hızlandırıcı.</p>\n<p>Ucuzlatmadığı tek şey var: veri modeli, güvenlik sınırı ve failure mode hakkında <strong>yanlış</strong> olmanın bedeli. Bu üç şey production’da yanlışsa, fatura build anında değil, çok sonra — genelde gece 2’de — gelir.</p>\n<h2 id=\"fatura-buildde-değil-productionda-görünür\">Fatura build’de değil, production’da görünür</h2>\n<p>Vibe coding ile çıkan sistemlerde tekrar tekrar gördüğüm dört kalıp. Hiçbiri “kod derlenmiyor” tipinde değil; hepsi <strong>intent ile gerçeklik arasındaki boşlukta</strong> yaşıyor.</p>\n<p><strong>1. Göç edemeyen veri modeli.</strong> Dolu bir tabloya <code>NOT NULL</code> kolon, <code>DEFAULT</code>’suz. Boş tabloda sorunsuz, production tablosunda fail. Daha derini: ilişkileri yanlış kurulmuş bir şema altı ay sonra basit bir özelliği imkânsız kılar. Veri modelini sonradan düzeltmek bir refactor değil, bir <span data-scheduled=\"/tr/journal/buyuk-veri-sendromu/\">veri göçü riskidir</span> — ve en pahalı anda, gerçek veriyle yapılır.</p>\n<p><strong>2. Düşman girdiyi hesaba katmayan güvenlik.</strong> Kimlik kontrolünden geçen ama sahiplik kontrolünden geçmeyen object ID’ler (IDOR), query’ye ulaşan kullanıcı girdisi, koda gömülü secret. Agent’ın ürettiği “standart” çözüm, girdinin düşman olabileceğini varsaymaz; çünkü ona sorulan soru “bu çalışsın” idi, “bu saldırıya dayansın” değil.</p>\n<p><strong>3. Ölçeklenmeyi görmeyen erişim deseni.</strong> Döngü içinde sorgu (N+1), <code>SELECT *</code>, pagination’sız liste, eş zamanlı olması gereken yerde senkron çağrı. On satırlık tabloda görünmez; yüz binlik tabloda p95’i uçurur.</p>\n<p><strong>4. Gözlemlenemeyen ve geri alınamayan sistem.</strong> Log yok, metrik yok, rollback planı yok. Kırıldığında <em>neden</em> kırıldığını göremezsiniz — ki vibe-coded sistemde en çok ihtiyaç duyacağınız şey budur.</p>\n<p>Ortak payda şu: araç en güvenli göründüğü yerde en tehlikeli. Yaygın kalıpları sağlam yazar; problemin kendine özgü boyutunu görmeden “standart” bir çözüm verir. <strong>Standart çözüm, sormadığınız problemin cevabıdır.</strong></p>\n<h2 id=\"çıktının-kalitesini-belirleyen-yazma-hızı-değil-denetim\">Çıktının kalitesini belirleyen: yazma hızı değil, denetim</h2>\n<p>Aynı agent’ı üç farklı elde düşünün. Fark, kimin daha hızlı yazdırdığı değil; <strong>sorulan sorunun ve yapılan review’un kalitesi.</strong></p>\n<ul>\n<li><strong>Hiç/çok az bilgi.</strong> Soruyu çerçeveleyemez, çıktıyı eleştirel okuyamaz. “Standart çözüm” yukarıdaki dört kalıpla birlikte production’a olduğu gibi çıkar. “Çalışıyor” ile “doğru çalışıyor” arasındaki farkı henüz hissedemez.</li>\n<li><strong>Az-orta bilgi.</strong> Bazı soruları doğru kurar, çıktının bir kısmını yakalar. Tavanı sistemin sınırlarında: yük altında nerede kırılır, bu soyutlama doğru mu, bu veri modeli hangi değişikliği kaldırır. Sonuç: iyi parçalar, kırılgan bütün.</li>\n<li><strong>Senior + sistem mimarisi.</strong> Agent bir ikame değil, bir <strong>çarpan</strong>. Plan yaptır → onayla → adım adım yazdır → bir junior’ın kodu gibi review et → savunamayacağın satırı merge etme. Bu disiplinin somut hâli ayrı bir yazıda: <a href=\"/tr/journal/ai-assisted-engineering-workflow/\">AI-Assisted Engineering Workflow’um</a>. Bu elde hız artar, kalite düşmez — çünkü muhakeme insanda kalır.</li>\n</ul>\n<p>Bu bir bütçe ifadesidir, ideoloji değil: senior, ertelenen maliyete karşı <strong>en ucuz sigortadır.</strong> Doğru veri modelini baştan kurmak bir öğleden sonra; yanlışını canlı sistemde düzeltmek bir çeyrek. “Para kazanınca düzeltiriz” bu yüzden absürt — para kazandıran ürün, aynı zamanda elinizi bağlayan, göçü en riskli üründür. Senior sonradan değil, önceden ucuzdur.</p>\n<h2 id=\"ne-zaman-vibe-coding-doğru-cevap\">Ne zaman vibe coding doğru cevap?</h2>\n<p>Her öneri gibi bunun da bir geçerlilik sınırı var — ve sınırı söylemeden öneri yapmam.</p>\n<p>Vibe coding <strong>doğru</strong> araçtır: prototip, throwaway demo, hobi, gerçek verisi/kullanıcısı olmayan iç araç, öğrenme. Buralarda yanlış olmanın maliyeti düşük olduğu için ertelemek rasyoneldir.</p>\n<p>Denklem şu üç şeyden biri masaya gelince çevrilir: <strong>gerçek kullanıcı, gerçek veri, gerçek para</strong> (ya da gerçek uptime taahhüdü). O eşikten sonra ertelenen maliyet faizini ödetmeye başlar ve direksiyonu tutan birine ihtiyaç doğar.</p>\n<hr/>\n<p>Yapay zekâ agent’larıyla üretim mümkün, hatta gerçekten hızlı. Ama agent, kodu kimin yazdığını değiştirdi; kimin sorumlu olduğunu değil. <strong>Vibe coding üretimi ucuzlattı, yanlış olmanın maliyetini değil</strong> — ve o maliyet, production’a kimin elinde çıktığına bakar.</p>",
      "content_text": "\"Yerelde çalışıyordu\" cümlesinin yeni bir akrabası var: \"demoda çalışıyordu.\" Bir hafta önce tarif edilip dakikalar içinde ayağa kalkan, ilk kullanıcılarını bile almış bir ürün — sonra ilk gerçek yük geldiğinde yavaşlıyor, ilk meraklı kullanıcı başkasının faturasını görüyor, ilk şema değişikliği migration'da patlıyor. Üreten kişi şaşırıyor: kod çalışıyordu.\n\nÇalışıyordu. Ama vibe coding'in düşürdüğü maliyet **üretim**di; yanlış olmanın maliyeti değil. O maliyet ertelenir, silinmez — ve ertelenen maliyet faiz işletir.\n\n## Vibe coding'in ucuzlattığı şey gerçek\n\nStrawman kurmayalım. Bir ilk sürümü çıkarmak hiç bu kadar ucuz olmamıştı: bir CRUD ekranı, bir REST uç noktası, bir form doğrulaması — tarif et, çalışsın. Prototip, throwaway, iç araç, öğrenme projesi için bu mükemmel bir hızlandırıcı.\n\nUcuzlatmadığı tek şey var: veri modeli, güvenlik sınırı ve failure mode hakkında **yanlış** olmanın bedeli. Bu üç şey production'da yanlışsa, fatura build anında değil, çok sonra — genelde gece 2'de — gelir.\n\n## Fatura build'de değil, production'da görünür\n\nVibe coding ile çıkan sistemlerde tekrar tekrar gördüğüm dört kalıp. Hiçbiri \"kod derlenmiyor\" tipinde değil; hepsi **intent ile gerçeklik arasındaki boşlukta** yaşıyor.\n\n**1. Göç edemeyen veri modeli.** Dolu bir tabloya `NOT NULL` kolon, `DEFAULT`'suz. Boş tabloda sorunsuz, production tablosunda fail. Daha derini: ilişkileri yanlış kurulmuş bir şema altı ay sonra basit bir özelliği imkânsız kılar. Veri modelini sonradan düzeltmek bir refactor değil, bir veri göçü riskidir — ve en pahalı anda, gerçek veriyle yapılır.\n\n**2. Düşman girdiyi hesaba katmayan güvenlik.** Kimlik kontrolünden geçen ama sahiplik kontrolünden geçmeyen object ID'ler (IDOR), query'ye ulaşan kullanıcı girdisi, koda gömülü secret. Agent'ın ürettiği \"standart\" çözüm, girdinin düşman olabileceğini varsaymaz; çünkü ona sorulan soru \"bu çalışsın\" idi, \"bu saldırıya dayansın\" değil.\n\n**3. Ölçeklenmeyi görmeyen erişim deseni.** Döngü içinde sorgu (N+1), `SELECT *`, pagination'sız liste, eş zamanlı olması gereken yerde senkron çağrı. On satırlık tabloda görünmez; yüz binlik tabloda p95'i uçurur.\n\n**4. Gözlemlenemeyen ve geri alınamayan sistem.** Log yok, metrik yok, rollback planı yok. Kırıldığında *neden* kırıldığını göremezsiniz — ki vibe-coded sistemde en çok ihtiyaç duyacağınız şey budur.\n\nOrtak payda şu: araç en güvenli göründüğü yerde en tehlikeli. Yaygın kalıpları sağlam yazar; problemin kendine özgü boyutunu görmeden \"standart\" bir çözüm verir. **Standart çözüm, sormadığınız problemin cevabıdır.**\n\n## Çıktının kalitesini belirleyen: yazma hızı değil, denetim\n\nAynı agent'ı üç farklı elde düşünün. Fark, kimin daha hızlı yazdırdığı değil; **sorulan sorunun ve yapılan review'un kalitesi.**\n\n- **Hiç/çok az bilgi.** Soruyu çerçeveleyemez, çıktıyı eleştirel okuyamaz. \"Standart çözüm\" yukarıdaki dört kalıpla birlikte production'a olduğu gibi çıkar. \"Çalışıyor\" ile \"doğru çalışıyor\" arasındaki farkı henüz hissedemez.\n- **Az-orta bilgi.** Bazı soruları doğru kurar, çıktının bir kısmını yakalar. Tavanı sistemin sınırlarında: yük altında nerede kırılır, bu soyutlama doğru mu, bu veri modeli hangi değişikliği kaldırır. Sonuç: iyi parçalar, kırılgan bütün.\n- **Senior + sistem mimarisi.** Agent bir ikame değil, bir **çarpan**. Plan yaptır → onayla → adım adım yazdır → bir junior'ın kodu gibi review et → savunamayacağın satırı merge etme. Bu disiplinin somut hâli ayrı bir yazıda: [AI-Assisted Engineering Workflow'um](/tr/journal/ai-assisted-engineering-workflow). Bu elde hız artar, kalite düşmez — çünkü muhakeme insanda kalır.\n\nBu bir bütçe ifadesidir, ideoloji değil: senior, ertelenen maliyete karşı **en ucuz sigortadır.** Doğru veri modelini baştan kurmak bir öğleden sonra; yanlışını canlı sistemde düzeltmek bir çeyrek. \"Para kazanınca düzeltiriz\" bu yüzden absürt — para kazandıran ürün, aynı zamanda elinizi bağlayan, göçü en riskli üründür. Senior sonradan değil, önceden ucuzdur.\n\n## Ne zaman vibe coding doğru cevap?\n\nHer öneri gibi bunun da bir geçerlilik sınırı var — ve sınırı söylemeden öneri yapmam.\n\nVibe coding **doğru** araçtır: prototip, throwaway demo, hobi, gerçek verisi/kullanıcısı olmayan iç araç, öğrenme. Buralarda yanlış olmanın maliyeti düşük olduğu için ertelemek rasyoneldir.\n\nDenklem şu üç şeyden biri masaya gelince çevrilir: **gerçek kullanıcı, gerçek veri, gerçek para** (ya da gerçek uptime taahhüdü). O eşikten sonra ertelenen maliyet faizini ödetmeye başlar ve direksiyonu tutan birine ihtiyaç doğar.\n\n---\n\nYapay zekâ agent'larıyla üretim mümkün, hatta gerçekten hızlı. Ama agent, kodu kimin yazdığını değiştirdi; kimin sorumlu olduğunu değil. **Vibe coding üretimi ucuzlattı, yanlış olmanın maliyetini değil** — ve o maliyet, production'a kimin elinde çıktığına bakar.",
      "date_published": "2026-06-13T00:00:00.000Z",
      "tags": [
        "ai-workflow",
        "vibe-coding",
        "production",
        "architecture",
        "opinion",
        "Journal"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://www.muhammetsafak.com.tr"
        }
      ]
    }
  ]
}