arifabds

App in Handy · Vaka çalışması

Bir problemle onu çözen app arasındakiboşluk için bir güven platformu.

Deploy edildi: filo gerçek bir sunucuda koşuyor, ödemeler ertelendi

Üç depo, on üç backend servisi ve bilinçli olarak sıkıcı tek bir kural: bir servis kendi verisinin sahibidir ve dış dünyayla bir event log üzerinden konuşur. Artık gerçek bir sunucuda koşuyor; onu oraya taşıyan hat, hangi build’in servis ettiğini varsaymak yerine doğruluyor. Aşağısı bunun nasıl kurulduğu, her kararın neden o yöne gittiği ve neyin hâlâ açık olduğu.

13

servis

26

event topic

1.524

backend testi

146

use case

106

migration

4

dil

Genel bakış

Ürün, altmış saniyede

Mimari ancak ürün anlaşıldıktan sonra anlam kazanıyor; bu yüzden önce o geliyor ve kısa kalıyor.

Öncül

Bir uygulamanın iyi olup olmadığını sormak yanlış soru. Aynı araç birinin problemi için mükemmel, bir başkasınınki için işe yaramaz. Platform bu yüzden app’leri tek başlarına puanlamıyor: belirli bir problemi cevaplayan belirli bir app’i, yani Match’i puanlıyor ve skoru taşıyan o eşleşme.

HandyScore

Her oy birkaç bağlamsal ekseni 1’den 10’a puanlıyor. Deterministik bir motor bunları normalize ediyor, yeni görüşlere eskileri atmadan daha fazla ağırlık veriyor, ince örneklemleri nötr bir prior’a doğru çekiyor ve eksenleri kendi kırılımıyla birlikte gelen tek bir sayıya harmanlıyor. Formül kararlar bölümünde; hiçbir yeri kara kutu değil.

“Me Too”

Problemler ayrıca ve çok daha basit biçimde oylanıyor: “bu problemi gerçekten kaç kişi yaşıyor” sorusunu cevaplayan net bir olumlu/olumsuz sinyal. Talep ile kalite farklı sorular olduğu için farklı araçlarla ölçülüyor.

Üç depo

Ayrı depolar, ayrı deploy döngüleri, ayrı commit geçmişleri, bilinçli olarak. Tanıtım sitesi ürünü bozabilecek konumda olmamalı; ikisi de filoyu bozabilecek konumda olmamalı.

DepoStack
platform13 servis, bir edge gateway, 9 paylaşılan kütüphaneJava 21 · Spring Boot 3.5 · PostgreSQL · Kafka
web-uiÜrün arayüzüNext.js 16 · React 19 · RSC-first
landingKamuya açık tanıtım yüzeyiNext.js 16 · next-intl

Bir isteğin yolculuğu

Bu aynı zamanda sayfanın geri kalanının haritası: her adım aşağıda bir yerde açılıyor.

  • 01

    Edge

    Gateway kimlik token’ını bir kez doğruluyor, kısa ömürlü bir internal token mint ediyor, rate limit’i uyguluyor ve bir correlation-id damgalıyor.

  • 02

    Tek sahip servis

    İstek tam olarak bir servise ulaşıyor; o servis içte hexagonal: infrastructure application’ı, application da saf bir domain’i çağırıyor.

  • 03

    Tek lokal transaction

    Durum değişikliği ve giden event birlikte yazılıyor. Ya ikisi de commit oluyor ya hiçbiri.

  • 04

    Relay

    Zamanlanmış bir relay yayınlanmamış satırları SKIP LOCKED ile kapıyor, sabit bir event id damgalıyor ve aggregate id ile keyed olarak yayınlıyor.

  • 05

    Idempotent consumer’lar

    İlgilenen her servis event’i kendi read-model’ine uyguluyor ve dedup’ı, etkiyle aynı transaction içinde o event id üzerinden yapıyor.

Mimari

Tek edge, on üç sahip, tek log

Neyin sahibi olduğunu ve kiminle konuştuğunu görmek için bir servis seçin. Buradaki hiçbir ok bir niyetin çizimi değil; her biri depoda var olan bir topic.

api-gateway · 8080
Kafka / Redpanda · event log

Altta ne var

  • PostgreSQL 16
  • Kafka / Redpanda
  • Redis
  • pgvector
  • OpenTelemetry Collector
  • Tempo
  • Prometheus
  • Grafana
  • Alertmanager
ÜretirTüketirBir servis seçin
  • identity-service: Kullanıcıların tek sahibi ve kimlik sağlayıcıyla konuşan tek servis: webhook, ilk girişte otomatik provisioning ve diğer her servisin projekte ettiği kullanıcı snapshot’ı.
  • catalog-service: App’lerin ve kategori boyutunun sahibi. “Orphan killer” burada yaşıyor: bir match oluştuğunda eşleşen app öksüz olmaktan çıkıyor.
  • problem-service: Problemlerin ve talep skorunun sahibi. Yalnızca consumer: okuduğu her yabancı alan, bir başkasının event’lerinden beslenen yerel bir projeksiyon.
  • scoring-service: Deterministik HandyScore motoru. Bir oy skoru yeniden hesaplayıp yayınlıyor; sonucu üç ayrı bounded context cache’liyor.
  • social-service: Merkez: match’ler, mesaj thread’leri ve mesaj oyları; artı her okumada yeniden hesaplanan yazar rozetleri; böylece bir ban kalktığında maskeleme kendiliğinden geri alınıyor.
  • moderation-service: Raporlar, destek biletleri ve filo geneli yayın kapısı: her yeni app, problem ve match’in onay beklediği kuyruk.
  • search-service: Saf CQRS okuma tarafı. Hiçbir kaynak verinin sahibi değil: indeks tamamen event’lerden kuruluyor ve yeniden inşa etmek log’u replay etmek demek.
  • media-service: Stateless bir yaprak ve nesne depolama kimlik bilgilerinin tek sahibi. Tipi ve boyutu doğruluyor, URL döndürüyor, byte’ları asla uygulamadan akıtmıyor.
  • notification-service: E-posta broadcast rayı. Hiçbir adres saklamıyor; alıcılar gönderim anında identity’den çekiliyor ve geçici olarak kullanılıyor.
  • admin-service: Kesişen operatör yüzeyleri: servislere dağıtılan dashboard istatistikleri ve edge rate limiter’ının kara kutu testi.
  • ai-service: Yalnızca platformun kendi corpus’una demirlenmiş retrieval destekli sohbet, match insight kartları, bir eval harness’ı ve tam bir maliyet defteri.
  • seed-service: Kamuya açık veriyi topluyor, nötrleştiriyor ve sahibi olan servisin olağan write API’siyle yazıyor. Asla bir oy uydurmuyor.
  • billing-service: Abonelik yaşam döngüsü ve bedava-premium rayı. Ücretli yüzeyi varsayılan olarak kapalı çıkıyor.

Build’in zorladığı sekiz kural

Bunlar tavsiye değil. Her biri ya bir build’i düşüren bir şeyle denetleniyor ya da topolojiden ötürü imkânsız.

#Kural
01Bir servis kendi verisinin sahibidir. Başka hiçbir servis onun veritabanına dokunmaz: paylaşılan şema yok, servisler arası SQL yok, servisler arası foreign key yok.Database-per-service topolojisi ve yabancı persistence import’larına bakan bir ArchUnit kuralı
02Servisler arası demek bir HTTP çağrısı ya da bir Kafka event’i demek. Asla in-process bir çağrı değil.Paylaşılan business jar yok; kütüphaneler yalnız kernel’i ve event kontratlarını taşıyor
03Dağıtık transaction yok. Bir iş birimi tam olarak bir veritabanını kapsar.Transaction’lar yalnız tek bir servisin interactor’larında; servisler arası tutarlılık eventual
04Her servis içte hexagonal ve domain’inde hiç framework yok.Monolitten devralınan, servis başına bir ArchUnit kural seti
05Event’ler outbox sayesinde güvenilir, consumer’lar idempotent.Üretici başına outbox tablosu ve relay; consumer’ın transaction’ında event id ile dedup
06Kimlik doğrulaması bir kez, edge’de yapılır.Gateway JWT filtresi; servisler yalnız gateway’in mint ettiği internal token’a güvenir
07Contract-first: sync API OpenAPI, async API versiyonlu event şeması.Bir event record katalogda yoksa kök build’i düşüren bir contract testi
08Her şey gözlemlenebilir: trace, metrik ve correlation-id hem HTTP’den hem Kafka’dan geçer.Otomatik yapılandırılan bir observability starter’ı; unutulacak servis başına config yok

Buraya nasıl gelindi

Sıfırdan mikroservis yazmak ile çalışan bir monolitten servis oymak farklı işler. Bu ikincisiydi ve monolit yol boyunca byte-for-byte donuk kaldı: okunabilir, çalıştırılabilir, hiç değiştirilmemiş.

  1. Zemin

    Çıkarımdan önce altyapı

    Broker, veritabanı, cache ve observability yığını önce yerelde ayağa kalktı. Monolit dondurulmuş ilan edildi: oku, kopyala, asla değiştirme; böylece geri dönüş her zaman gerçek kaldı.

  2. Strangler

    Ön kapıyı gateway alıyor

    Her şey monolite proxy’lendi. O andan itibaren her çıkarım, önüne daha yüksek öncelikli bir route ekledi. Cut-over mekanizması buydu: her seferinde tek bir yol, her zaman geri alınabilir.

  3. Platform

    Paylaşılan raylar

    Correlation-id, bir-kez-doğrula artı internal token mint’i, Redis rate limit’i, outbox/idempotency/dead-letter kütüphanesi ve OpenTelemetry starter’ı. Hiçbir servis dahil olmayı unutamasın diye bilinçli olarak otomatik yapılandırılan kütüphaneler hâlinde yazıldı.

  4. Ayrıştırma

    Servisler arası her okuma bir projeksiyona dönüşüyor

    Asıl iş. Başka bir modülün tablolarına uzanan okumalar tek tek, event log’undan beslenen ve okuyanın sahip olduğu projeksiyonlara çevrildi. Filonun ilk senkron servisler-arası client’ı retry ve circuit breaker ile inşa edildi, sonra gereksiz hâle getirilip emekliye ayrıldı.

  5. Temizlik

    Kalan paylaşılan okumalar

    Geriye kalan yedi cross-context okuma tek tek kapatıldı; her biri okuyanın sahip olduğu bir projeksiyonla değişti. Dürüst kısmı: daha önceki bir rapor bunun zaten bittiğini iddia etmişti. Abartılıydı ve retrospektif bunu açıkça yazıyor.

  6. Ayrılma

    Fiziksel ayrım, sonra catch-all gidiyor

    Her stateful servis kendi veritabanına ve kendi migration zincirine taşındı. Ardından monolitin catch-all route’u kaldırıldı ve monolit istek yolundan tamamen çıktı. Binary referans olarak çalışır durumda bırakıldı.

  7. Sonrası

    Parite, pipeline, sertleştirme

    Yaklaşık elli use-case ve domain testi servis servis geri kazanıldı, CI matrisi ve release pipeline’ı kuruldu, filo greenfield olarak boot edilip observability canlı doğrulandı.

Ancak gerçekten yapınca ortaya çıkan üç şey

Bir monoliti bölmek in-process listener’ları sessizce öldürür

İki davranış in-process event listener’ları üzerinde koşuyordu: bir app eşleştiğinde “öksüz” listesinden çıkması ve mesajı oylanan bir yazarın itibarının değişmesi. O yayıncılarla dinleyiciler farklı süreçlere düştüğü an ikisi de çalışmayı bıraktı: hata yok, exception yok, log’da hiçbir şey yok. İkisi de bulunup Kafka consumer’ı olarak yeniden bağlandı. Hiçbir şeyin patlamaması, hiçbir şeyin bozulmadığının kanıtı değil.

Türetilmiş bir bayrak yeniden yayınlanmazsa aşağısı hiç öğrenmez

Bir app öksüz olmaktan çıktığında bayrak sahibi serviste değişiyor ama hiçbir event yayınlanmıyordu; dolayısıyla search’ün öksüz listesi kalıcı olarak bayattı. Düzeltme doğru yerde tek satırdı; onu bulmak ise hangi durum geçişlerinin event ürettiğini, hangilerinin sessizce üretmediğini düşünmeyi gerektirdi.

Kendi migrasyon raporunu denetlemek de migrasyonun parçası

Bir faz raporu son shared-database okuyucusunun emekliye ayrıldığını iddia ediyordu. Yedi tane daha vardı. Bu düzeltme retrospektiften silinmek yerine içine yazıldı; çünkü bir dokümandaki yanlış iddia, o dokümanda bir bug’dır.

Event omurgası

Bir durum değişikliği nasıl herkesin gerçeği hâline geliyor

Önceki bölümdeki her ok bu topic’lerden biri. Aşağıdaki zincir onları taşıyan şey ve her adım, bir önceki tasarımın veri kaybedebildiği belirli bir yol yüzünden var.

Zincir

  1. 01

    Interactor durumu değiştirir

    Bir use case kendi aggregate’ini değiştirir ve in-process bir domain event yayınlar. Kafka’dan haberi yoktur.

  2. 02

    Outbox appender, commit’ten önce

    Bir listener event’i aynı transaction içinde bir outbox tablosuna yazar. Rollback hiçbir şey yayınlamaz; commit event’i kaybedemez.

    Önceki tasarım commit’ten hemen sonra yayınlıyordu. O gönderim başarısız olursa event iz bırakmadan yok oluyordu: durum değişmişti ve aşağı akıştaki kimse bunu hiç öğrenmiyordu.

  3. 03

    Relay yayınlar

    Zamanlanmış bir relay bekleyen satırları SKIP LOCKED ile kapar (birden çok instance için güvenlidir), sabit bir event id damgalar ve aggregate id ile keyed olarak yayınlar.

    Aggregate id ile keylemek aggregate başına sırayı veren şey. Bilinçli tek bir istisna var ve aşağıdaki notlarda.

  4. 04

    Log

    Topic’ler onlara sahip olan bounded context’in adını taşır. Log, bir read-model’in yeniden inşa edilebilmesinin sebebi: replay bir hayal değil, birinci sınıf bir kurtarma yolu.

  5. 05

    Consumer’lar uygular

    İlgilenen her servis event’i kendi şemasına projekte eder. Deserialization korumalıdır; bozuk bir payload partition’ı zehirleyemez.

  6. 06

    Aynı transaction içinde dedup

    İşlenmiş-event işareti ile iş etkisi birlikte commit olur. At-least-once teslimi exactly-once etkiye çeviren şey budur.

  7. 07

    Sınırlı retry, sonra dead-letter

    Üç kez geri çekilmeli deneme, sonra kayıt topic’in dead-letter kuyruğuna gider. Bir deserialization hatası retry’ları tamamen atlar; hiçbir zaman parse edilmeyecek bir payload’ı yeniden denemek sadece daha yavaş bir başarısızlıktır.

  8. 08

    İstendiğinde replay

    Bir operatör ucu dead-letter’a düşmüş kayıtları header’larıyla birlikte, byte sadık biçimde yeniden oynatır; böylece orijinal event id hayatta kalır ve dedup, kısmen uygulanmış bir batch’i hâlâ korur.

Katalog

Bu kayıt bir doküman değil. Bir contract testi paylaşılan kütüphanedeki her event record’unu keşfediyor ve burada bir satırı yoksa kök build’i düşürüyor; yani async API kendi tarifinden uzaklaşamıyor.

  • scoring.app-score-updatedSnapshot

    App’in cache’lenmiş kalite skorunu tazeler.

    Üretici
    scoring
    Tüketiciler
    catalog, ai
    Key
    appId
  • scoring.problem-score-updatedSnapshot

    Problemin cache’lenmiş talep skorunu tazeler.

    Üretici
    scoring
    Tüketiciler
    problem, ai
    Key
    problemId
  • scoring.handy-score-updatedSnapshot

    Match skorunu cache’lendiği her yerde tazeler.

    Üretici
    scoring
    Tüketiciler
    social, problem, search, catalog, ai
    Key
    matchId
  • scoring.problem-vote-castSnapshot

    Çağıranın kendi “Me Too” kararını ön-doldurur.

    Üretici
    scoring
    Tüketiciler
    problem
    Key
    problemId:userId
  • scoring.problem-vote-clearedTombstone

    Geri çekilen oyu düşürür; ön-doldurma biter.

    Üretici
    scoring
    Tüketiciler
    problem
    Key
    problemId:userId
  • scoring.app-vote-castSnapshot

    Çağıranın kendi eksen bazlı app puanını ön-doldurur.

    Üretici
    scoring
    Tüketiciler
    catalog
    Key
    appId:userId
  • scoring.match-vote-castSnapshot

    Çağıranın kendi eksen bazlı match puanını ön-doldurur.

    Üretici
    scoring
    Tüketiciler
    social
    Key
    matchId:userId
  • catalog.category-upsertedSnapshot

    Tüketicilerin yerelde render ettiği kategori etiketini günceller.

    Üretici
    catalog
    Tüketiciler
    problem, social, search
    Key
    categoryId
  • catalog.app-upsertedSnapshot

    Yerel app projeksiyonunu günceller: etiket, arama metni, öksüz bayrağı.

    Üretici
    catalog
    Tüketiciler
    social, scoring, problem, search, ai
    Key
    appId
  • catalog.app-pending-reviewKapı

    App’i onay kuyruğuna ekler.

    Üretici
    catalog
    Tüketiciler
    moderation
    Key
    appId
  • catalog.app-deletedTombstone

    App’i her projeksiyondan siler.

    Üretici
    catalog
    Tüketiciler
    social, scoring, problem, search, ai
    Key
    appId
  • catalog.category-deletedTombstone

    Kategoriyi düşürür; etiketler “kategorisiz”e döner.

    Üretici
    catalog
    Tüketiciler
    problem, social, search
    Key
    categoryId
  • identity.user-upsertedSnapshot

    Yerel kullanıcı projeksiyonunu günceller: rozet, itibar, ban durumu.

    Üretici
    identity
    Tüketiciler
    social, moderation
    Key
    userId
  • identity.user-deletedTombstone

    Kullanıcıyı her projeksiyondan siler.

    Üretici
    identity
    Tüketiciler
    social, moderation
    Key
    userId
  • social.match-message-postedSnapshot

    Mesajı raporlanabilir kalsın diye snapshot’lar.

    Üretici
    social
    Tüketiciler
    moderation
    Key
    messageId
  • social.match-message-deletedTombstone

    Mesajı silinmiş işaretler ama satırı kanıt olarak tutar.

    Üretici
    social
    Tüketiciler
    moderation
    Key
    messageId
  • social.match-createdSnapshot

    Match projeksiyonunu günceller ve eşleşen app’i öksüzlükten çıkarır.

    Üretici
    social
    Tüketiciler
    problem, catalog, search, scoring, ai
    Key
    matchId
  • social.match-deletedTombstone

    Match’i her projeksiyondan siler.

    Üretici
    social
    Tüketiciler
    problem, search, scoring, catalog, ai
    Key
    matchId
  • social.match-pending-reviewKapı

    Match’i onay kuyruğuna ekler.

    Üretici
    social
    Tüketiciler
    moderation
    Key
    matchId
  • social.user-reputation-changedDelta

    Mesaj yazarına bir itibar deltası uygular.

    Üretici
    social
    Tüketiciler
    identity
    Key
    userId
  • problem.problem-upsertedSnapshot

    Problem projeksiyonunu günceller: aranabilir metin, durum, yazar.

    Üretici
    problem
    Tüketiciler
    search, scoring, social, catalog, ai
    Key
    problemId
  • problem.problem-deletedTombstone

    Problemi her projeksiyondan siler.

    Üretici
    problem
    Tüketiciler
    search, scoring, social, catalog, ai
    Key
    problemId
  • problem.problem-pending-reviewKapı

    Problemi onay kuyruğuna ekler.

    Üretici
    problem
    Tüketiciler
    moderation
    Key
    problemId
  • catalog.contribution-rewardedDelta

    Kabul edilen bir app için bir kez karma verir.

    Üretici
    catalog
    Tüketiciler
    identity
    Key
    userId
  • problem.contribution-rewardedDelta

    Kabul edilen bir problem için bir kez karma verir.

    Üretici
    problem
    Tüketiciler
    identity
    Key
    userId

Paragrafı hak eden dört incelik

Snapshot’lar affeder, delta’lar etmez

Neredeyse her topic güncel durumun tamamını taşıyor; yani consumer bir değer set ediyor ve yeniden teslim doğal olarak yakınsıyor. İki topic bunun yerine bir değişim taşıyor: itibar düzeltmesi ve katkı ödülü. Onlar için dedup bir konfor değil; at-least-once teslim ile çift sayım arasındaki tek şey o. Ödül yolu bir adım daha atıyor ve üreticiye ikinci, bağımsız bir koruma koyuyor: içeriğin üzerine bir kereye mahsus bir zaman damgası; böylece yayınla → geri çek → yeniden yayınla döngüsü tam bir kez ödüllendiriyor.

Aynı kelime, iki doğru davranış

Silinen bir app, match ya da problem her projeksiyondan kaldırılıyor; silinmiş bir şey listede görünmemeli. Silinen bir thread mesajı ise kaldırılmıyor: satırı bir silinmiş bayrağıyla kalıyor, çünkü sonradan silinmiş bir mesaj hâlâ raporlanabilir olmalı ve kanıt kaybolmamalı. İkisine de tombstone deniyor; ikisine aynı davranmak birini bozardı.

Consumer grubu bir doğruluk kararıdır

Bir servis match oluşumuna birbiriyle ilgisiz iki sebeple tepki veriyor: bir aggregate’i değiştiriyor ve bir projeksiyon tutuyor. İki listener’ı aynı consumer grubuna koyarsanız partition’ları aralarında bölüşürler: her tepki event’lerin yalnız bir kısmını görür ve hiçbiri bariz biçimde bozuk görünmez. Ayrı gruplar bunu ikisinin de her şeyi gördüğü temiz bir fan-out yapıyor.

Partition key her zaman aggregate id değildir

Oy event’leri oy satırıyla değil, problem-ve-oy-veren çiftiyle keyleniyor. Read-model’in kimliği o çift ve bir kişinin oy ver → değiştir → geri çek dizisi sıralı kalmak zorunda. Oy satırının id’si geri çekip yeniden oy verince değişiyor; onunla keylemek, bayat bir “oy verildi”nin onu kaldırması gereken “temizlendi”den sonra düşmesine izin verirdi.

Mühendislik kararları

Her parça neden böyle

Her kart kararı ve gerekçesini birkaç satırda söylüyor. Tam argüman, kabul edilen takas ve depodaki yeri için birini açın.

VeriBir servis kendi verisinin sahibidir: paylaşılan şema yok, servisler arası join yok, servisler arası foreign key yokYabancı veri, bir soft identifier artı okuyanın sahip olduğu ve event log’undan güncel tuttuğu bir projeksiyondur.Paylaşılan bir tablo, paylaşılan bir deploy demektir. İki servis aynı satırları okumaya başladığı an ikisi de şemasını tek başına değiştiremez ve mikroservis sınırı süse dönüşür.

Her stateful servisin kendi veritabanı ve kendi ilk sürümünden başlayan kendi migration zinciri var. Bir servis başkasının verisine ihtiyaç duyduğunda, identifier’ı ve kendi tanımladığı bir port’un arkasında yerel bir read-model tutuyor; yani sakladığı şekil, sahibinin elindeki şekil değil, kendi ihtiyacı olan şekil.

Kural disiplinle korunmuyor. Bir servis başkasının persistence tiplerini import ederse ArchUnit build’i düşürüyor; fiziksel ayrımdan sonra ise kazara bile uzanılacak paylaşılan bir şema kalmadı.

TakasServisler arası her okuma eventual consistent oluyor. “Bu app var mı” gibi yazma yolu kontrolleri kesin olmaktan çıkıp yakınsayan hâle geliyor; burada kabul edilebilir bir şey ve sonradan keşfedilmek yerine geçerli olduğu yerlere yazılmış durumda.

Depodaki karşılığı

  • 13 × <svc>_db
  • ArchUnit: SERVICES_DO_NOT_DEPEND_ON_EACH_OTHER
  • *LookupPort → EventFed*Adapter
VeriOkuma tarafının hiç kaynak verisi yokSearch’ün veritabanı var ama tek bir iş tablosu yok: tamamen event’lerden kurulan dört projeksiyon ve bir tam metin arama yardımcısı.Search her context boyunca okuyor; yani sizi tam olarak sınırların ötesine bir join atmaya kışkırtan sorgu bu. Onu saf bir projeksiyon yapmak kışkırtmayı ortadan kaldırıyor ve bir indeksi yeniden kurmayı migration değil replay hâline getiriyor.

Sorgular yerel projeksiyonlara düz SQL ile, Postgres tam metin araması ve bir GIN indeksi üzerinden koşuyor. Burada bilinçli olarak ORM entity’si yok; okuma tarafının koruyacağı bir domain olmadığı için bir object mapper saf ek yük olurdu.

İki kural sayfalamayı dürüst tutuyor. Sıralama bir enum ve enum’ın kendisi izin listesi: bilinmeyen bir değer bizim kodumuz çalışmadan reddediliyor, çünkü clause native bir sorgunun içine interpolate edilmek zorunda ve oraya bir istemci string’i asla ulaşmamalı. İkincisi, her sıralama satır id’siyle bitiyor; çünkü kesin bir toplam sıra olmadan sıralama kolonunda eşitlenen satırlar sayfalar arasında sessizce tekrarlanıyor ya da kayboluyor.

TakasAramak istediğiniz her şeyin önce bir event üzerinde yolculuk etmesi gerekiyor. Yeni bir alan gerektiğinde uzanılacak pratik bir join yok; ki amaç bu, ama bir arama özelliğinin bazen başka bir serviste başlaması anlamına da geliyor.

-- Every ORDER BY on the paged union ends with the id.
-- Without a strict total order, rows tying on the sort column
-- silently repeat or vanish between pages.
ORDER BY ts_rank(search_vector, query) DESC, created_at DESC, id

Depodaki karşılığı

  • search_db
  • app/problem/category/match_projection
  • SearchRepositoryAdapter
  • SearchSort
VeriÇok eksenli bir listeleme tek bir composite sorgudur, öncelik zinciri değilHer filtre aynı sorgu içinde null toleranslı bir predicate. Yeni bir tane eklemek satır eklemek, dal eklemek değil.Listeleme eskiden filtre eksenleri üzerinde bir if/else merdiveniydi: ilk dolu olan kazanıyor, geri kalanı sessizce düşüyordu. Ada ve öksüzlük durumuna göre filtreleyen bir istek yalnızca adı cevaplıyordu ve hiçbir çağıran bunu anlayamıyordu.

Tek bir criteria nesnesi ve tek bir native sorguya indirmek, artık ölü olan dokuz repository metodunu sildi ve ucun, dokümantasyonunun hep iddia ettiği gibi davranmasını sağladı.

JSON alanları alan çıkarımıyla değil bütün doküman containment’ı ile sorgulanıyor (çıkarım dizi üyeliğini zaten cevaplayamıyor) ve bu aynı zamanda tek bir GIN indeksinin bugünkü ve gelecekteki her JSON eksenine hizmet etmesi demek.

TakasTek büyük bir sorgu, beş küçük metottan bir bakışta daha zor okunuyor. Karşılığında iki filtrenin hangisinin kazanacağı konusunda anlaşamaması imkânsız hâle geliyor.

-- One query, every axis null-tolerant. A new filter is a predicate here,
-- never a new repository method and never a new branch.
WHERE (CAST(:categoryId AS uuid) IS NULL OR a.category_id = CAST(:categoryId AS uuid))
  AND (CAST(:platform   AS text) IS NULL OR a.metadata @> :platformJson::jsonb)
  AND (CAST(:name       AS text) IS NULL OR a.search_vector @@ plainto_tsquery(:name))

Depodaki karşılığı

  • AppSearchCriteria
  • AppSearchNativeQuery
  • GIN jsonb_path_ops (V6)
VeriFilo geneli bir mapping varsayılanı bir alanı sessizce temizlenemez yaptıNull’ları yok saymak “çağıranın göndermediğini boşaltma” için doğru; yokluğun kendisi yeni değer olan bir alan için yanlış.Bir problem üzerine bir kez ekran görüntüsü konduktan sonra bir daha kaldırılamıyordu. Domain null diyordu, mapper null’ı yok sayıyordu, eski URL her kayıtta hayatta kalıyordu ve hiçbir şey hata vermiyordu.

Düzeltme, geri kalan her şey için doğru olan global varsayılanı değiştirmek değil, o tek alan için bir property bazlı override. Yokluk da tek bir temsil kazandı: boş girdi komutun kendisinde null’a normalize ediliyor, böylece hiçbir okuyucu iki tür “resim yok” durumunu test etmek zorunda kalmıyor.

Hatırlanmaya değer kısım nasıl yakalandığı. Mock tabanlı testler her iki durumda da geçiyordu; kendisine söyleneni harfiyen yapan bir mapper üzerinde assert ediyorlardı. Satırın hiç değişmediğini gösteren şey, gerçek bir veritabanına karşı koşan bir integration testi oldu.

TakasGelecekteki her opsiyonel-ve-kaldırılabilir alan aynı açık override’a ve onu gerçekten temizleyen bir teste ihtiyaç duyuyor. Küçük ve tekrarlayan bir maliyet; alternatifi sessiz bir veri hatası sınıfı olduğu için kabul edildi.

// IGNORE is right for "don't blank what the caller didn't send" and
// silently wrong for a field whose absence IS the new value.
@Mapping(target = "screenshotUrl", source = "screenshotUrl",
         nullValuePropertyMappingStrategy = SET_TO_NULL)
void updateEntity(Problem domain, @MappingTarget ProblemEntity entity);

Depodaki karşılığı

  • MapStructGlobalConfig
  • problems.screenshot_url (V5)
  • ProblemPersistenceIT
MesajlaşmaDurum ve event aynı transaction’da commit olurGiden event, iş transaction’ının içinde bir outbox tablosuna yazılır. Yayınlamayı sonra bir relay yapar.Önceki tasarım commit’ten hemen sonra yayınlıyordu. O yayın başarısız olursa (broker düşmüş, ağ takılmış, süreç öldürülmüş), durum değişikliği hayatta kalıyor, event kalmıyordu. Aşağı akışta kimse öğrenmiyordu ve hiçbir şey kayıp yaşandığını kaydetmiyordu.

Commit’ten önce çalışan bir listener event’i aynı bağlantı üzerinde sahibi olan servisin outbox’ına ekliyor. Rollback hiçbir şey yayınlamıyor. Commit ise event’i tarif ettiği veri kadar kalıcı yapıyor.

Zamanlanmış bir relay bekleyen satırları SKIP LOCKED ile kapıyor (bu birden çok instance çalıştırmayı güvenli kılıyor), sonradan dedup için kullanılan sabit bir event id damgalıyor ve aggregate id ile keyed olarak yayınlıyor; böylece bir aggregate’in event’leri sıralı kalıyor.

TakasYayın artık asenkron ve biraz gecikmeli; relay’in polling aralığı gerçek bir gecikme ayarına dönüşüyor. Change-data-capture polling’i ortadan kaldırırdı; bu bir gözden kaçış değil, kayıtlı ve bilinçli bir erteleme.

// BEFORE_COMMIT: the event row joins the business transaction, so a
// rollback publishes nothing and a commit can never lose the event.
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void on(AppScoreUpdatedEvent event) {
    outbox.append(ScoringTopics.APP_SCORE_UPDATED, event.appId(), event);
}

Depodaki karşılığı

  • OutboxWriter
  • OutboxRelay
  • <svc>.outbox_events
  • ScoreOutboxAppender
MesajlaşmaAt-least-once teslim, exactly-once etkiDedup işareti iş etkisiyle aynı transaction’da yazılır. Öncesinde değil, sonrasında değil.Her broker yeniden teslim eder. İşaret ayrı commit olursa, etkinin uygulandığı ama işaretin yazılmadığı bir pencere doğar ve bir sonraki teslim etkiyi tekrar uygular.

Snapshot event’lerinde bu bir kolaylık: consumer bir değer set ediyor, dolayısıyla replay zaten yakınsıyor. Durum değil değişim taşıyan iki topic için (bir itibar düzeltmesi ve bir katkı ödülü) çift sayımı engelleyen tek şey bu.

Ödül yolu üreticide ikinci ve bağımsız bir koruma taşıyor: içeriğin üzerine bir kereye mahsus bir zaman damgası. Consumer dedup’ı bir yeniden teslimi durduruyor; üreticideki damga ise yayınla → geri çek → yeniden yayınla döngüsünün iki kez ödüllendirmesini durduruyor. Bir delta gerçekten ikisine birden ihtiyaç duyduğu için iki katman var.

TakasHer consumer event başına küçük bir yazma ödüyor ve defterin bir noktada budanması gerekiyor. Ortadan kaldırdığı hata sınıfının yanında ucuz.

-- The marker is written in the same transaction as the business effect,
-- which is what turns at-least-once delivery into exactly-once effect.
INSERT INTO processed_events (event_id, consumer)
VALUES (:eventId, :consumer)
ON CONFLICT (event_id, consumer) DO NOTHING;

Depodaki karşılığı

  • ProcessedEventStore
  • <svc>.processed_events
  • apps.contribution_rewarded_at
MesajlaşmaZehirli bir mesaj sistemi değil, tek bir kaydı durdururSınırlı retry, ardından dead-letter topic’i ve o kayıtları byte sadık biçimde yeniden oynatan bir operatör ucu.Bu olmadan bozuk tek bir payload kendi partition’ını sonsuza dek tıkar ve bütün bir projeksiyonu devre dışı bırakır. Naif sonsuz retry ile bunu gürültülü ve pahalı biçimde yapar.

Tek bir error handler tüm filoya hizmet ediyor: üç geri çekilmeli deneme, sonra dead-letter topic’i. Deserialization hataları retry’ları tamamen atlıyor; hiçbir zaman parse edilmeyecek bir payload’ı tekrar denemek sadece daha yavaş bir başarısızlık.

Kurtarma en az kapsama kadar önemli. Replay ucu header’ları koruyor; yani orijinal event id hayatta kalıyor ve yarısı uygulanmış bir batch’i yeniden oynatmak güvenli oluyor, çünkü dedup zaten inmiş olanı hâlâ tanıyor.

TakasRetry’lar retry topic’lerine yönlendirilmek yerine blocking. Düşük hacimli ve idempotent consumer’lar için bu daha basit ve yeterli; hacim aksini söylediği gün için yükseltme yolu yazılı.

Depodaki karşılığı

  • ConsumerErrorHandlingAutoConfiguration
  • DltReplayer
  • POST /actuator/dltreplay
MesajlaşmaDayanıklı senkron client’ı inşa et, sonra gereksiz kılFilonun ilk servisler-arası çağrısı düzgün biçimde sertleştirildi, sonra event-fed bir projeksiyonla değiştirildi ve son senkron servisler-arası client emekliye ayrıldı.Senkron bir çağrı erişilebilirliği kuplajlar: çağrılan düştüğünde çağıran da bozulur. Bu bazen doğru takastır, ama projekte edilebilecek bir okuma için kalıcı bir vergidir.

İlk çağrı dikkatli yoldan inşa edildi (tipli HTTP interface, transport timeout’ları, retry, circuit breaker, bulkhead, bir anti-corruption mapper ve zarif bir fallback) ve tam breaker döngüsü bağımlılığın gerçek bir kesintisi altında kanıtlandı.

Sonra sahibi aynı veriyi event olarak yayınlamaya başladı, iki tüketici onu yerelde projekte etti ve client silindi. Transport kütüphanesi ile internal uç kaldırılmak yerine korundu: bugün tüketicileri yok ve bir sonraki gerçek senkron ihtiyacın kullanacağı tam olarak onlar. Aynı yığın bugün moderation servisinin publish çağrılarını taşıyor.

TakasProjekte edilen okuma eventual consistent olduğu için bir yazma yolu varlık kontrolü gerçeklikle kısa süre anlaşmazlığa düşebiliyor. Bilerek kabul edildi ve etkilenen yollar belgelendi.

Depodaki karşılığı

  • libs/platform-http
  • PlatformHttpClientFactory
  • AppUpsertedEvent
  • social/scoring app_projection
GüvenlikKimlik doğrulaması bir kez, edge’de olurGateway dış token’ı doğrular, sonra onu aşağıdaki servislerin güvendiği kısa ömürlü bir internal token ile değiştirir.On üç servisin dış bir kimlik sağlayıcıyı bağımsız olarak doğrulaması, on üç yanlış yapılandırma noktası, on üç dışa bağımlılık ve “bu kullanıcı ne yapabilir” sorusuna on üç farklı cevap demektir.

Aşağıdaki servisler dış sağlayıcıyı hiç görmüyor. Paylaşılan bir starter’daki tek bir decoder bean’i hepsini bir kerede geçirdi; servis başına kod yok. Internal token iki dakika yaşıyor; sızmış bir tanesinin neredeyse hiçbir değeri kalmayacak kadar kısa.

İnce kısmı rol. Dış token’dan kopyalanmıyor: gateway onu identity servisinin kendi veritabanından, kısa süre cache’leyerek çözüyor. Yani ürünün içinde verilen bir yetki, kullanıcının bir sonraki girişinde değil, o pencere içinde tüm filoya yayılıyor.

TakasBedeli erişilebilirlik. identity cevap veremediğinde edge, dış token’ın iddia ettiği role güvenmek yerine isteği 503 ile reddediyor; önceki sürüm burada fail-open’dı ve bir dayanıklılık kampanyası bunu kapattı. Anonim trafik etkilenmiyor, otuz saniyelik önbellek bilinen kullanıcıları kısa kesintilerden geçiriyor; yani gerçekten reddedilen şey, yakın zamanda görülmemiş birinin kimlikli isteği.

platform:
  security:
    internal-jwt:
      issuer: api-gateway
      ttl-seconds: 120          # short enough that a stolen token is worthless
    role-cache:
      ttl-seconds: 30           # role comes from identity's DB, not from Clerk

Depodaki karşılığı

  • GatewaySecurityConfig
  • InternalIdentityMintGlobalFilter
  • GatewayRoleResolver
  • platform-security-starter
GüvenlikPremium’un tam olarak tek bir türetimi varTek bir metot karar veriyor ve bir yönetici her zaman premium. Senkronda tutulacak ikinci bir bayrak yok.Üç servise dağılmış entitlement mantığı, birbirinden ince farklarla ayrılan üç cevaba dönüşür; ve bug “bu ekran abone olmadığımı sanıyor” olarak yüzeye çıkar.

Yayılım yalnızca türetimle: yeni event yok, read-model yok, senkron çağrı yok. Identity servisi türetilmiş değeri açıyor, gateway internal token’ı mint ederken onu bir claim olarak damgalıyor ve her servis claim’i okuyor. Premium’a bağlı bir özellik eklemek bir boolean okumak.

Entitlement’ın kendisi kullanıcının aktif hak kaynaklarının birleşimi: ücretli abonelik, promosyon kodu, yönetici tarafından verilmiş hediye. Dolayısıyla biten bir abonelik, hâlâ geçerli bir promosyonu olan birini asla düşürmüyor; çünkü “bu kullanıcı hak sahibi mi” sorusu tek bir yerde soruluyor ve bütün sebeplerden birlikte cevaplanıyor.

TakasClaim ancak cache penceresi kadar taze. Bu bilinçli bir tercih: edge’de her istek için identity’ye gidip gelmek çok daha kötü bir anlaşma olurdu.

// The one and only derivation of "premium" in the fleet.
// An ADMIN is always premium: no second flag to keep in sync.
public boolean isPremium() {
    return accountType == AccountType.PREMIUM || role == Role.ADMIN;
}

Depodaki karşılığı

  • User.isPremium()
  • InternalPrincipalView
  • EntitlementPolicy.isEntitled
GüvenlikPara yüzeyi kapalı çıkarVarsayılanı kapalı olan tek bir bayrak, ücretli yüzeyin tamamını güvenlik katmanında ve bir kez daha use case’in içinde kapatıyor.Ödemeler kodun dışındaki bir sebeple ertelendi: sağlayıcı yalnızca kayıtlı bir şirketi kabul ediyor ve henüz bir şirket yok. Çalışan kodu silmek yanlış cevap olurdu; erişilebilir bırakmak daha da kötü olurdu.

Bayrak kapalıyken checkout, abonelik yaşam döngüsü, ödeme defteri, plan listesi, sağlayıcı webhook’u ve yönetici iade yüzeyi herkes için erişilemez: kullanıcı, yönetici, anonim fark etmeksizin. Tek bayrak üzerinde iki katman: güvenlik yapılandırması route’ları reddediyor, checkout use case’i de süreç içinde reddediyor; yani süreç içinden bile hiçbir şarj başlatılamıyor.

Tasarımın geri kalanı bir gün açılacağını varsayıyor. Hiçbir kart verisi saklanmıyor; hosted checkout formu tutuyor. Tutar her zaman sunucu tarafında bir plan tanımlayıcısından çözülüyor, istemciden asla kabul edilmiyor. Erişim yalnızca webhook ile doğrulanmış durumdan veriliyor; çünkü bir tarayıcı callback’i terk edilebilir ya da taklit edilebilir ve yetki değildir.

TakasÇalışmayan kod egzersiz görmez. Bu yüzden switch sırasının kendi testi var; en dıştaki kapı beklerken çürüyemesin diye.

Depodaki karşılığı

  • BillingSecurityConfig.PAID_SURFACE
  • StartCheckoutInteractor
  • BillingKillSwitchSecurityIT
GüvenlikSırrın kullanıldığı yere yazılan kontrol, yalnız o sırrı korurTek bir kayıt, fleet’in onsuz boot edemeyeceği her sırrı adlandırıyor; tek bir konfigürasyon kancası da prod açılışını, uygulama context’i yaratılmadan reddediyor.Kod tabanı bu deseni zaten biliyordu ve dört ayrı bean’in içine elle yazmıştı. Yine de 144 konfigürasyon anahtarının hiçbir koruması yoktu; çünkü sırrı tüketen bean’in içine konan bir kontrol tam olarak bir anahtarı korur. Hangi anahtarların korunduğu, o gün kimin hangi bean’i düzenlediğinin kaydına dönüşmüştü.

Böyle bir kuralın nereye konduğu, ne kadar uzağa eriştiğini belirliyor. On dört bootable modülün ortak tek kütüphanesi observability’ydi; güvenlik starter’ı ise gateway’e hiç eklenemiyor: servlet yığınını sürüklüyor ve reaktif bir gateway onu classpath’te bulunca başlamayı reddediyor. Yeni bir modül açmak, güvenlik kuralını başka bir şeyin adını taşıyan bir jar’ın içine saklamaktan iyiydi.

En keskin bulgu, çalışan bir committed default’tu. Kimlik sağlayıcısının anahtar ucu canlı bir geliştirme tenant’ına düşüyordu; yani prod’da set etmeyi unutmak hata vermezdi, sessizce o tenant’ın imzalarını kabul ederdi ve oraya kayıt olan herkes burada geçerli bir kullanıcı olurdu. Bir default kaçınılmazsa, yanlış bir şeye karşı çalışan değil, **çalışamayan** olanı seç: ayrılmış, geçersiz bir host.

TakasOn dört modül artık eksik bir anahtarda başlamayı reddediyor; bu daha kötü bir ilk deployment, daha iyi bir ikinci yıl demek. Hata gürültülü, anında ve anahtarın adını söylüyor.

// The secret is registered once, centrally, with the consequence of
// forgetting it. Not a check inside the bean that happens to use it:
// that version only ever guards the one key someone remembered.
GuardedSecret.fleetWide(
    "SENTRY_DSN",
    List.of("sentry.dsn", "SENTRY_DSN"),
    Set.of(),
    "production exceptions reach nobody — they go to container stdout"
        + " and are lost on the next restart, with no symptom anywhere"),

Depodaki karşılığı

  • libs/platform-config-guard
  • PlatformSecretRegistry
  • EnvironmentPostProcessor
  • security-gates.sh
GüvenlikGüncel olmak, taranmış olmak değildirBir bileşen envanteri (SBOM) periyodik üretilip taranıyor, imaj tabanları pinleniyor, ve her suppression tarayıcının kendi zorladığı bir son kullanma tarihi taşıyor.Bu faz hiçbir şey bulunmayacağı beklentisiyle planlanmıştı: her framework en güncel sürümündeydi. İlk tarama 46 yüksek/kritik advisory döndü; altısı kritik, üçü doğrudan istek yolunda — aralarında fleet’in tek giriş kapısı olan gateway’de maksimum önem derecesinde bir SpEL açığı. Bir sürüm seçildiği gün temizdir. Advisory sonra yayımlanır ve repoda hiçbir şey değişmez.

İki bağımlılık override’ı duruyor ve her biri kaldırma koşulunu birinin hafızasında değil bir yorumda taşıyor: tarama o property olmadan yeşile döndüğü gün sil. Suppression’lar da aynı standarda tabi — dosya bir gerekçe ve bir son kullanma tarihi zorunlu kılıyor, tarihi de tarayıcının kendisi uyguluyor; yani süre dolunca kayıt susmayı bırakıyor ve kapı kendiliğinden kırmızıya dönüyor.

Build’i çalıştıran girdiler de bağımlılıktır. Bir workflow action tag’i taşınabilir bir işaretçidir: taşınırsa bizim checkout’umuzda bizim token’ımızla farklı kod koşar ve bizim diff’imizde hiçbir şey değişmez — bu yüzden action’lar commit ile pinleniyor. Build wrapper indirdiği arşivi hash ile doğruluyor; çünkü transport güvenliği sunucuyu doğrular, artefaktı değil. İmaj tabanları da pinli, ki onlar olmadan taramanın hiçbir anlamı kalmıyor: tek bir makinede sessizce yedi builder ve beş run imajı birikmişti ve hangi build’in hangisini kullandığını hiçbir şey kaydetmiyordu.

Takasİki tarayıcı yerine bir tane. İkisi, uyumlu tutulacak iki zafiyet veritabanı, iki eşik ve iki ayrı suppression dosyası demekti — bu kod tabanının başka yerlerde bedelini ödemeye devam ettiği ayrışmanın tedarik zincirine ithal edilmiş hâli.

# An empty bill of materials scans perfectly clean, and a clean scan is
# indistinguishable from a good one by exit code alone. Anything that
# narrows the reactor lands here, so the floor is asserted first.
components=$(jq '.components | length' "$SBOM")
if [ "$components" -lt "$MIN_COMPONENTS" ]; then
  fail "SBOM has $components components (< $MIN_COMPONENTS) — scan not trustworthy"
fi

Depodaki karşılığı

  • dependency-scan.sh
  • CycloneDX SBOM → Trivy
  • .trivyignore.yaml (expired_at)
  • supply-chain.yml (weekly)
GüvenlikBunu kimin okuyabileceği yazılıydı; ne zaman ortadan kalkacağı değilKullanıcıya anahtarlı her sütun dört saklama sınıfından birine ayrılıyor, ve silme ilk kopya kadar ikinci kopyaya da ulaşıyor.Erişim kontrolü bir şeyi kimin görebileceğini cevaplar. Hiçbiri o şeyin ne zaman var olmayı bırakacağını cevaplamaz — ve bu boşluk bir izin hatasına benzemediği için bir izin incelemesinde hiç görünmez.

Hesap silme event’inin iki tüketicisi vardı ve ikisi de aynı küçük şeyi yapıyordu: projeksiyondaki görünen adı düşürmek. İsim kayboluyor, içerik kalıyordu; yani her asistan konuşması, tam prompt’u ve tam cevabıyla, silinmiş kullanıcının anahtarında süresiz duruyordu. Silme artık içeriğe ulaşıyor — ve önce ikinci kopyaya ulaşıyor: bir değerlendirme kaydı aynı materyali birebir gömüyor ve kendi kullanıcı sütunu yok, o yüzden aynı transaction içinde ve onu bağlayan satırdan **önce** gitmesi gerekiyor.

Silmek her sınıf için doğru cevap değil, ve hangisinin ne olduğunu söylemek işin ta kendisi. Özel sohbet silinir. Public katkı kalır ve okuma anında de-identify edilir; çünkü onu silmek başkalarının sayfalarını yeniden yazar. Bir oy kalır; çünkü zaten yayımlanmış bir sayının girdisidir. Ödeme kayıtları kalır; çünkü kanun öyle istiyor. Ayrıca bir log satırı, sahibi olmayan, saklama süresi olmayan ve bir silme talebini yerine getirme yolu bulunmayan ikinci bir kopyadır — oraya giden şey pseudonim tanımlayıcıdır. En kötü örnek de varsayılan olandı: bir yer tutucu e-posta gönderici alıcı başına bir satır yazıyordu, yani her broadcast kullanıcı dizinini log’a döküyordu.

TakasOkuma anında de-identify etmek, eskiden doğrudan bir isim join’i yapan yollara bir lookup ekliyor. Herkesin geçmişini olduğu gibi bırakan tek sürüm bu.

Depodaki karşılığı

  • identity.user-deleted
  • chat_turn_log
  • judge_evaluation.full_prompt
  • SECURITY_RUNBOOK §6
MimariKoşulmayan bir kontrol, olmayan bir kontroldürGüvenlik kampanyasının son fazı kaynağı değil **çalışanı** denetledi ve dört açık daha buldu — bunlardan biri fleet’in koddan beş faz geride olmasıydı.Dokuz faz repoyu doğrulamıştı. Hiçbiri koşan fleet’e bir şey sormamıştı. Bu ayrım, bir bedeli olana kadar kılı kırk yarmak gibi görünür.

İlk canlı ölçüm bir regresyon gibi göründü: anonim bir health isteği tam bileşen dökümü döndürdü — günler önce kapatılmış bir sızıntı. Regresyon değildi. İmajlar o düzeltme inmeden iki saat önce etiketlenmişti ve sonrasındaki beş fazın hiçbiri hiçbir container’da yoktu; üstelik kaynak ağacındaki her kontrol yeşildi. Bir imaj deposu hareketli bir etiket altında sessizce bayatlar, o yüzden artık her servis kendi build’ini damgalıyor ve bir kontrol, koştuğunu iddia ettiği koddan eski bir fleet’i reddediyor.

İki süit kırmızıydı ya da hiç yoktu ve kimse bilemezdi, çünkü onları hiçbir şey koşturmuyordu. Frontend başlık süitinin ne bir npm script’i ne bir workflow’u vardı. Derin uçtan uca tier ise yalnız bir release tag’inde tetikleniyor ve bu repoda hiç tag atılmadı; canlı bir fleet’e karşı ilk gerçek koşusu, kaynağı okuyarak bulunamayacak iki yeni açık buldu — çünkü ikisinde de kaynak doğru görünüyor. Bir catch-all handler, framework seviyesindeki “hatalı istek” istisnalarını anonim bir uçtan sunucu hatasına çeviriyordu; ve ortak hata sözleşmesini miras alamayan tek modül, internete bakan modül çıktı.

TakasÇalışan sistemi test etmek yavaştır, container ister ve her commit’te koşamaz. Bu yüzden kendi tetiği olan ayrı bir tier — ve yanına yazılan ders şu: kimsenin çekmediği bir tetik, olmayan bir testle aynı şeydir.

# Ask the running fleet what it is, rather than assuming it is the repo.
# The first time this was asked, the answer was five phases old: every
# check in the source tree was green and none of it was deployed.
built=$(curl -fsS "$svc/actuator/info" | jq -r '.build.time')
[ "$built" \> "$LAST_FIX" ] || fail "$svc image predates the fix ($built)"

Depodaki karşılığı

  • fleet-build-check.sh
  • /actuator/info build stamp
  • fleet-e2e.sh negative tier
  • ActuatorExposureIT
MimariOn üç servis, aynı iç yapıInfrastructure application’a, application saf bir domain’e bağımlı; asla tersi değil. Bir use case bir interactor ve bir komut.Dağıtık sistemler ününü servisler arasındaki boşluktan alır, ama kafa karışıklığının çoğu aslında onların içinde yaşar. On üç iç yapının aynı olması, bir servisi öğrenmenin hepsini öğretmesi demek.

Domain, içinde hiç framework olmayan düz Java; skor motorunun bir Spring context’i olarak değil aritmetik olarak test edilebilmesini sağlayan şey bu. Persistence modeli bilinçli olarak domain modelinden ayrı: bir ORM anotasyonu bir depolama meselesidir ve bir iş kuralını şekillendirmek onun işi değil.

Transaction sınırları yalnız interactor’larda, hatalar RFC 7807 problem dokümanları ve her servis kendi migration zincirinin sahibi. Katmanlanma review ile değil ArchUnit ile denetleniyor; böylece yorgun bir cuma gününde de geçerli kalıyor.

TakasPragmatik üç katmanlı bir servise göre özellik başına daha çok dosya. On üç serviste buna değiyor; tek serviste muhtemelen fazla olurdu.

Depodaki karşılığı

  • *Interactor per use case
  • ArchUnit rule sets
  • platform-kernel
  • RFC 7807
MimariHer şey görünmez doğarYeni app’ler, problemler ve match’ler incelemeyi bekleyerek başlıyor ve kamuya yalnız tek bir onay kuyruğundan çıkıyor.Her şeyin anında göründüğü bir güven platformunda güven yoktur. Ama mevcut bir durum yaşam döngüsünün yanına ikinci bir “yayınlandı” bayrağı eklemek iki görünürlük gerçeği yaratır ve o ikisi zamanla ayrışır.

Bu yüzden tek bir kapı var: “bu kamuya görünür mü” sorusunu mevcut durum yaşam döngüsü cevaplıyor. Değişen şey event’lerin ne zaman atıldığı. Public stream’ler artık oluşturmada yayınlamıyor (varlık henüz kamuya açık değil), onay anında yayınlıyor; yayından kaldırma ise silme tombstone’unu atıyor. Aşağı akıştaki tüketiciler hiçbir filtre eklemedi; sadece artık yalnızca kamunun görebileceği şeyler için event alıyorlar.

Bu kayma bütün bir hata sınıfını görünür kıldı. Görünürlüğü değiştiren her yazma yolunun event’i atması gerekiyor ve bir yönetici ucu atmıyordu; yani oradan reddedilen bir öge arama indeksinde ve asistanın corpus’unda canlı kalıyordu. Düzeltme, her çağıranın hatırlamasına güvenmek yerine görünürlük geçişinin kendisinde yayınlıyor.

TakasOnay insana bağlı bir darboğaz ve bunun için toplu işlemler yazılması gerekti, dürüst kısmi başarı raporlamasıyla; çünkü on ikisinin on biri başarılıyken “tamam” diyen bir batch, hiç rapor vermemekten kötüdür.

Depodaki karşılığı

  • AppStatus.isPubliclyVisible()
  • PendingItem
  • *-pending-review topics
  • BulkProgressStream
MimariAltyapıyı bilerek bozan bir test katmanıGerçek container’lar, gerçek broker, artı testin kesebildiği bir ağ proxy’si: sistem-under-test bir tarafta, testin kendi client’ı diğer tarafta.Bu sistemdeki her garanti (outbox dayanıklılığı, dedup, dead-letter) bir başarısızlık sırasında ne olacağına dair bir iddia. Unit testler her parçanın mutlu yolunu doğruluyor; zincirin tuttuğunu söyleyemiyorlar.

Bütün tasarım o asimetride. Uygulama broker’a ve veritabanına proxy üzerinden ulaşıyor; testin kendi consumer’ı doğrudan bağlanıyor. Böylece bir test Kafka’yı uygulamanın elinden alabiliyor, outbox’ın retry sayaçlarıyla birikmesini izleyebiliyor, broker’ı geri getirip boşaldığını assert edebiliyor ve bunların hepsi kendi doğrulama yolu hayattayken oluyor.

Bunu mümkün kılan bariz olmayan bir ayrıntı var. Bir Kafka istemcisi bir adrese bootstrap ediyor, sonra kendisine broker’ın advertised adresi veriliyor ve oraya doğrudan bağlanıyor; yani yalnız bootstrap adresini proxy’lemek hiçbir şeyi proxy’lemiyor. Bu yüzden broker, advertised listener’ı proxy’nin kendi host portu olacak şekilde başlatılıyor; hem bootstrap hem veri yolu oradan geçiyor. Bu olmadan proxy’yi kesmek hiçbir şeyi kesmiyor ve test yanlış sebeple geçiyor.

TakasBu testler kesinti içinde bilerek gerçek saniyeler harcıyor; bu yüzden katman etiketli ve varsayılan build’den hariç. Hızlı iç döngü hızlı kalıyor, chaos kendi profiliyle koşuyor.

// The system under test reaches Kafka through the proxy; the test's own
// consumer does not. Without that asymmetry, cutting the broker would
// blind the assertions as well as the application.
registry.add("spring.kafka.bootstrap-servers", ChaosIntegrationTest::proxiedBootstrapServers);
// …and the broker advertises Toxiproxy's host port, or only bootstrap
// would be proxied and cutBroker() would cut nothing.

Depodaki karşılığı

  • ChaosIntegrationTest
  • Toxiproxy 2.12
  • OutboxOutageChaosIT
  • mvn -Pchaos verify
Uygulamalı AIYalnız platformun kendi corpus’una demirlenmiş bir asistanServisin event log’undan projekte ettiği içerik üzerinde vektör araması; sağlayıcıdan bağımsız bir port’un arkasında, tek satırlık model anahtarıyla.Bir güven platformunda genel dünya bilgisinden cevap veren bir asistan, hiç asistan olmamasından kötüdür: platformun arkasında duramayacağı şeyler hakkında otoriter konuşur.

Corpus, app–problem–match üçgeni; diğer her servisin tükettiği aynı event’lerden AI servisine projekte ediliyor. Retrieval çok dilli embedding’ler üzerinde yaklaşık en yakın komşu araması ve indeks, filtreler aday kümesini daralttığında recall sessizce çökmeyecek şekilde ayarlandı.

Uygulama katmanı hangi sağlayıcının cevap verdiğini hiç bilmiyor. O sınıra iki ders gömülü. Bir modelden “JSON döndür” istemek bir kontrat değildir: çıktıyı bir kod bloğuna sarar ve parse başarısız olur; açılış süslü parantezini prefill etmek ilk testi geçer, sonra bir string değerinin içindeki kaçırılmamış tırnakta ölür ki bu prefill’in yapısal olarak engelleyemeyeceği bir sınıftır. Kalıcı çözüm bir şemaya karşı constrained decoding: geçersiz çıktıyı sadece caydırılmış değil, temsil edilemez yapıyor.

TakasYalnızca kendi içeriğimize demirlenmek, asistanın tahmin etmek yerine bilmediğini söylemesi demek. Burada doğru davranış bu ve yine de zaman zaman daha kötü bir demo.

Depodaki karşılığı

  • pgvector vector(1024) · BGE-M3
  • HNSW vector_cosine_ops
  • LlmPort
  • AnthropicChatClient
Uygulamalı AISayaç, defterin kendisidirHer model çağrısından önce dört harcama sınırı koşuyor ve kullanım, gerçekten faturalandırılmış olanın kaydından toplanıyor, asla ayrı bir sayaçtan değil.Defterin yanındaki ikinci bir sayaç, yanlış olabilecek ikinci bir şeydir ve ikisi zamanla ayrışır. Harcama konusunda yalan söyleyemeyecek tek sayı, harcama kayıtlarının kendisinden türetilendir.

Her harcayan use case’in en tepesinde sırayla dört katman koşuyor: kayan kısa pencere, haftalık tavan, global günlük sınır ve kümülatif sigorta. Okumalar fail-closed: cüzdan bir veritabanına kısa süre ulaşılamadı diye açılmıyor; bir pencere ancak her katmandan geçmiş bir istekle açılıyor; yani bir ret asla birinin saatini başlatmıyor.

Maliyet tam olarak tek bir yerde, sağlayıcı adapter’ında hesaplanıyor ve fiyat kaydı olmayan bir model boot olmayı reddediyor: maliyeti hesaplanamayan bir şey asla trafik almamalı. Ayrıca pahalı bir otomatik yol kapatılmadı, kaldırıldı: en güvenli bütçe koruması, gözetimsiz bir harcayıcının hiç olmaması.

TakasHer istekte bir defteri toplamak bir sorgu maliyeti. Koruduğu model çağrısının yanında ucuz ve sayının doğru olan tek sürümü bu.

Depodaki karşılığı

  • BudgetGuard
  • chat_turn_log
  • ai.usage_window
  • insight_run_log
Uygulamalı AIOtomasyon içerik üretebilir, güven sinyali aslaSeeding servisi app’leri ve problemleri sahibi olan servisin olağan API’siyle yazıyor ve asla oy kullanmıyor.Bütün çıktısı bir skor olan bir platformda uydurulmuş bir skor kısayol değildir; ürünün sattığı tek şey hakkında bir yalandır.

Toplanan kamuya açık veri nötr platform içeriğine normalize ediliyor ve sahibinin olağan write API’siyle yazılıyor; orada her şey gibi inceleme bekleyerek iniyor. Seeder moderasyonu yeniden uygulamıyor, onu besliyor. Tekrar çalıştırma güvenliği event dedup’ından değil, kaynak ve dış id ile anahtarlanmış bir provenance defterinden geliyor: hiç event tüketmiyor ve hiç üretmiyor.

Ayrıca hiç kamuya açık yüzeyi yok: gateway route’u yok, dış API yok, yalnız ağdan izole bir operatör ucu var. Herkesin adına yazan bir bileşene mümkün olduğunca az kişi ulaşabilmeli.

TakasSkorsuz tohumlanmış bir katalog, uydurulmuş skorları olan birinden daha boş görünüyor. Kullanıcısı olmayan bir platformun dürüst hâli bu ve onu göstermek zaten amaç.

Depodaki karşılığı

  • seed-service (8093)
  • provenance (source, external_id)
  • no /api/v1 surface
ÜrünSkor bir formüldür, bir his değilDomain katmanında saf aritmetik: normalize et, yaşa göre söndür, bir prior’a doğru düzenle, eksen ağırlığıyla harmanla.İnsanlardan güvenmelerini istediğiniz bir puanın açıklanabilir olması gerekir. Bir match’in neden 7,4 aldığını kimse söyleyemiyorsa o sayı süstür.

Onu üç seçim taşıyor. Bayesçi bir prior ince örneklemleri ortaya çekiyor; üç coşkulu oy mükemmel bir on üretmiyor. Recency decay eski görüşleri daha az tartıyor (yazılım eskir) ama bir tabanı var, çünkü eski bir değerlendirme değersiz bir değerlendirme değildir. Ve eksen kırılımı başlık sayının yanında yayınlanıyor, çünkü tek bir sayı “neden” sorusunu asla cevaplamaz.

Motor oyları ve bir zaman damgasını alıp bir sonuç döndürüyor. Repository yok, saat yok, config okuması yok; yani aritmetik olarak test ediliyor ve aynı girdiler her zaman aynı çıktıyı veriyor. Diğer servislerin cache’lediği skorlar hesaplanmış değeri taşıyan event’lerle geliyor; böylece yeniden teslim birikmek yerine yakınsıyor.

TakasDüzenleme yeni match’lerin bir süre sıradan görünmesine yol açıyor. Bu dürüst: üç oydan gelen bir skor, üç yüz oydan gelen bir skor gibi görünmemeli.

q        = (rating − 1) / 9                            // 1–10 ballot → [0,1]
decay(d) = 0.75 + 0.25 · σ(0.005 · (730 − d))         // sigmoid recency, 75% floor
axisQ    = (Σ decay·q + k·0.5) / (Σ decay + k)        // Bayesian prior, k = 3
score    = 10 · Σ(wᵢ · axisQᵢ) / Σ wᵢ                 // weighted blend → 0–10
confidence = LOW (<5) · MEDIUM (<30) · HIGH (≥30) ballots

Depodaki karşılığı

  • HandyScoreCalculator
  • pure domain, no I/O
  • HandyScoreCalculatorTest

Frontend

Sistemin diğer yarısı

On üç backend servisi ancak önlerinde tutarlı kalan bir şey varsa işe yarar. Arayüz de aynı içgüdüyle inşa edildi: insanların hatırlaması istenen kurallar değil, build’i düşüren kurallar.

Sunucu önce, ince bir routing katmanıyla

Her şey varsayılan olarak server component; client direktifi yalnız etkileşimin gerçekten gerektirdiği yerde beliriyor. Routing katmanı bilinçli olarak ince kalıyor: parametreleri ve metadata’yı okuyup sayfayı orkestre eden bir view’a devrediyor. Bu ayrım zorlanıyor: routing katmanı bir servis import ederse build düşüyor.

Dört katmanlı veri katmanı

Arkasında on üç servis varken bir kontrat değişikliğinin tam olarak tek bir dosyaya dokunması gerekiyor. Her katman yalnız bir alttakiyle konuşabiliyor ve katman atlamak bir review’ı değil build’i kırıyor.

  1. 1. Endpoints

    Ham URL’ler, domain başına bir kez tanımlı.

  2. 2. API

    Bir network client’a dokunmasına izin verilen tek katman.

  3. 3. Services

    Anti-corruption: dış şekiller iç modellere dönüyor, hatalar normalize ediliyor.

  4. 4. Consumers

    Server action’lar ve query hook’ları. Bir URL’yi hiç görmüyorlar.

Her koşuda denetlenen beş mimari kural

  • 01

    Hiçbir modül başka bir modülün içine uzanmaz, yalnız onun public giriş noktasından geçer

  • 02

    Hiçbir tüketici service katmanını atlamaz

  • 03

    Hiçbir bileşen doğrudan fetch etmek için API katmanını atlamaz

  • 04

    Modüller arasında döngüsel bağımlılık yok

  • 05

    Routing katmanı hiçbir service ya da server modülünü import etmez

Dört dil, sıfır hardcoded string

Dört dil tam paritede ve her birinde bin ikiyüzden fazla anahtar. Parite bir vaat değil: iki audit script’i sözlükleri karşılaştırıyor ve hardcoded metin tarıyor, ikisi de doğrulama komutunun içinde koşuyor. Çevrilmemiş bir string eklemek build’i düşürüyor.

Test ve erişilebilirlik

Testler test ettikleri şeyin yanında duruyor ve canlı bir backend yerine mock’lanmış network handler’larına karşı koşuyor; yani kurgusal olmadan deterministikler. Erişilebilirlik sonradan denetlenmek yerine aynı suite içinde assert ediliyor ve tüm suite React derleyicisi açıkken koşuyor; uygulamanın fiilen yayınlandığı hâliyle.

Her biri bana bir gün kaybettiren üç şey

Derleyici altında sessizce ölen bir form

Bir formu effect içinde reset etmek, derleyici açıldığı anda çalışmayı bırakıyor: hata yok, form sadece tepkisiz kalıyor. Doğru desen varsayılan değerlerle tohumlayıp yeniden doğrulamak ve artık bir sonraki kişinin bakacağı yerde yazılı.

Asenkron bir kimlik sağlayıcı kalıcı bir yanlış cevap üretir

Kimlik doğrulama asenkron yükleniyor. Hazır olmadan atılan kullanıcıya özel bir okuma boş dönüyor ve gerçek diye cache’leniyor; ekran bozuk değil, kendinden emin biçimde yanlış. Okumayı yüklenme durumuna bağlamak tek satır; bug’ı fark etmek zor kısım.

Aracı suçlamadan önce ölç

Geliştirme sunucusunun belleği ölene kadar büyüyordu ve bu tam olarak bizim kodumuzda bir sızıntı gibi görünüyordu. Sadece geliştirmeyi etkileyen, upstream’de sınırsız bir derleme cache’iydi. Bunu kanıtıyla ve geçici çözümüyle belgelemek, tahmin yürütmekten daha değerliydi.

Kalite ve pipeline

Bir şey merge olmadan önce neyin geçmesi gerekiyor

Bu sayfada anlatılan her kural, ancak ihlal edildiğinde build’i reddeden bir şey olduğu için gerçek. Aşağısı o mekanizma.

Dört test katmanı

KatmanNeyi kapsıyor
Offline guard’larMimari kurallar, domain ve use-case testleri, contract golden ve round-trip kontrolleriVarsayılan test fazı
IntegrationGerçek veritabanı ve broker: outbox round-trip’leri, persistence, native sorgular, dead-letter replayVerify fazı
ChaosEnjekte edilen başarısızlık: broker kesintisi, veritabanı kesintisi, gecikmeKendi profili, varsayılan build’den hariç
Uçtan ucaFilo ayağa kaldırılıp health-gate’ten geçiriliyor, sonra gateway üzerinden çalıştırılıyorİstendiğinde ve release tag’lerinde

Sürekli entegrasyon

Path-filtreli dinamik matris

Bir servisteki değişiklik o servisi ve yukarı akış kütüphanelerini build ediyor. Paylaşılan bir kütüphanedeki değişiklik her şeyi build ediyor. Yalnız doküman değişikliği hiçbir şey build etmiyor. Başarısızlıklar kardeşlerini iptal etmiyor; yani bozuk bir servis bir başkasını gizlemiyor.

Release tag’leri

Bir sürüm tag’i buildpack ile on container imajı üretiyor (depoda elle yazılmış tek bir Dockerfile yok) ve uçtan uca katmanı onlara karşı koşuyor.

Tüm iş için tek bir yerel komut

Dört aşamalı bir pipeline: format kontrolü, sonra tüm reaktörün testleri, sonra on container imajı, sonra filonun o taze imajlarla ayağa kaldırılıp her servis hazır diyene kadar health-gate edilmesi. Sadece başarı bildirmekle kalmıyor, filoyu ayakta ve güncel bırakıyor.

Frontend kapısı

Format, lint, mimari kurallar, UI tutarlılık denetimi, iki i18n denetimi, tip kontrolü ve test suite’i: tek komut ve pipeline’ın koştuğu komutun aynısı.

Doküman bir testtir

Bir contract testi paylaşılan kütüphanedeki her event record’unu keşfediyor ve event katalogunda yoksa kök build’i düşürüyor. Yanında serialization golden dosyaları, round-trip kontrolleri ve bir orphan kontrolü var. Yani async API kendisini tarif eden dokümandan uzaklaşamıyor; “doküman güncel kalır”ın şimdiye kadar işe yaramış tek sürümü bu.

Observability ve yakaladığı iki bug

Trace, metrik ve log’lar her servisten otomatik yapılandırılan tek bir starter üzerinden çıkıyor ve correlation-id hem HTTP hem Kafka header’larından geçiyor; böylece bir istek broker atlaması boyunca izlenebiliyor. Servis hedefleri messaging ve güvenlik kurallarının yanında duruyor, alarmlar gerçek bir hedefe ulaşıyor, ve ayrı bir sink yalnızca hataları alıyor — error log seviyesine bağlı olduğu için “neyin hata sayıldığı” kararı bir kez, exception handler’da veriliyor; bir vendor kütüphanesi tarafından ikinci kez değil.

Ekmeğini iki kez çıkardı. Histogram bucket’ları hiç yayınlanmadığı için filo genelinde persentil gecikmeleri kullanılamaz durumdaydı; düzeltme on imajın hepsine gitti, sonrasında her servis hiçbir şey yerine gerçek bir p99 raporladı ve sınırlı bir yük testi edge limiter’ın fazla yükü hiç sunucu hatası vermeden attığını doğruladı. Sonra alarm boru hattının uçtan uca sağlıklı olduğu ve hiç kimseye teslim etmediği ortaya çıktı: bir `null` receiver her bildirimi kabul edip atar, ve bu “söyleyecek bir şeyi olmamak”tan ayırt edilemez. Yasak olduğu belgelenmedi, dosyadan silindi — artık ona işaret eden bir rota config validasyonunda patlıyor.

Teslimat

Bir push’tan, hangi build olduğunu kanıtlayan bir filoya

Bir deployment’ın ilginç yanı başarılı olması değil. İlginç olan, başarılı bir koşu ile hiçbir şey yapmamış bir koşunun birbirinden ayırt edilip edilemediği. Aşağıdaki her şey, en az bir kez ayırt edilemediği için var.

Geliştirme dalına bir push ne yapar

  1. 01

    Bir değişiklik haritası neyin yeniden kurulacağına karar verir

    “Bu commit on dört modülün hangisine dokundu” sorusunun cevabına tek bir dosya sahiptir ve hem entegrasyon workflow’u hem de deployment onu okur. O haritada olmayan bir modül reddedilmez — sessizce atlanır, ve atlanan bir job yeşil rapor eder; bu yüzden harita bir kolaylık değil bir kural olarak tutulur.

  2. 02

    Yalnız değişen modüller imaja dönüşür

    İmajlar buildpack ile kurulur; hiçbir backend modülünde elle yazılmış bir container dosyası yoktur. Her imaj yalnız hareketli bir etiketle değil commit’le de etiketlenir, çünkü hareket eden bir registry etiketi, repoda hiçbir şey değişmeden yaşlanan bir filo demektir.

  3. 03

    Fark değil, kompozisyonun kendisi uygulanır

    Sunucu yalnız değişen servisleri çeker ve yeniden yaratır, sonra çalışanlara dokunmadan kompozisyonda eksik kalan ne varsa onu tamamlar. İlk deployment bunun sebebini gösterdi: bir delta “hedefte ne eksik” sorusunu cevaplayamaz, yalnız “geçen seferden beri ne değişti”yi.

  4. 04

    Bir sağlık kapısı, bir temenni değil

    Koşunun devam edebilmesi için her servisin hazır olduğunu bildirmesi gerekir. Tavan bağlayıcı hâle göre ölçülür — dört çekirdekte soğuk açılan on dört servis ve on bir şema migrasyonu — çünkü uzun tarafta hata yapmak yalnız geç haber verir, kısa tarafta hata yapmak sağlam bir filoyu bozuk gösterir.

  5. 05

    Filoya hangi build olduğu sorulur

    Her modül bir build damgası yayımlar ve deployment bunu hattın az önce ürettiğiyle karşılaştırır. Bu adım, bütün projenin en kötü bulgusu yüzünden var: beş faz boyunca koşan filo koddan eskiydi ve kaynak tarafındaki her kontrol yeşildi. Tarihlenemeyen bir deployment, doğrulanmamış bir deployment’tır.

  6. 06

    Ve bir şeyin takılıp takılmadığı

    Son olarak outbox’ta hiç yayımlanmamış satır var mı diye bakılır. Bozuk bir mesaj hattı, boştaki bir hatta birebir benzer — sağlık yeşil kalır, hata çıkmaz, kuyruk yalnızca ilerlemeyi bırakır — bu yüzden tek dürüst sinyal yayımlanmamış satır sayısıdır.

Kutu ve içeri giden yol

Filonun tamamı tek bir küçük sunucuda koşuyor. Ona internetten hiçbir şey ulaşmıyor: yalnız dışarı arayan bir tunnel edge ağına bağlanıyor, gateway loopback’te dinliyor ve firewall yönetim dışında kapalı kalıyor. Deploy anahtarı da aynı ilkeyle salt-okunur ve hesap geneli bir token yerine tek bir depoya scope’lu: bir kimlik bilgisi işi yapabilmeli, fazlasını değil.

Buradaki iki karar tercih değil ölçümdü. Alan adı bir etiket kaybetmek zorunda kaldı, çünkü bir wildcard sertifika tam olarak bir etiket eşler ve ilk ad el sıkışmayı hiç tamamlayamıyordu — eski bir protokol ayarına benzeyen ama o olmayan bir hata. Ve kaç proxy’ye güvenileceği, hiçbir şey kurulmadan önce tek kullanımlık bir echo container’ından okundu; çünkü o sabit, anonim oran-limitinin bir çağıranı mı yoksa bütün interneti tek kova olarak mı saydığına karar veriyor.

İlk gerçek koşuların bulduğu

Hiç koşmamış bir hat, doğrulanmamış bir hattır. İlk koşulardan üç bulgu; hiçbirini bir gözden geçirme üretemezdi:

İki ay boyunca hiç derlememiş yeşil build’ler

Maven wrapper’ı ilk commit’ten beri çalıştırma biti olmadan commit’lenmişti; yani beş workflow ilk komutta ölürdü. Hiçbir yerde görünmedi, çünkü koşan job’ların ona ihtiyacı yoktu ve ona ihtiyacı olan job’ları değişiklik haritası atlıyordu. Gerçekten derleyici isteyen ilk job, ilk gerçek deployment oldu.

Yalan söyleyen bir güvenlik raporu

İmaj taraması on dört imajda kritik bulgu bildirdi. Bulgu yoktu: runner’ın diski dolmuştu ve tarayıcı tek bir imajı bile açmamıştı. Çöküşü ile bulgusu aynı çıkış kodunu paylaşıyordu, yani daha büyük bir disk isteyen arıza on dört zafiyet olarak raporlandı. Artık ayrı kodlar — ve kapanış tavsiyesi de raporun bir parçası sayılıyor.

Kendi körlüğünü bir hüküm olarak raporlayan doğrulayıcı

Build-damgası kontrolü on üç modülü doğrulanamaz ilan edip hepsinin yeniden kurulmasını önerdi. Damgalar yerindeydi; kontrol kendi kimlik bilgisini kaybetmiş ve bir kimlik doğrulama hatasını “damga yok” diye okuyordu. Hedefine ulaşamayan bir kontrol bunu söylemek zorundadır — “denetlediğim şey yanlış” ile “ben koşamadım” farklı cümlelerdir.

Depoda doğru, internette olmayan bir kural

Tarayıcı politikası on bir kapalı yol beyan ediyor; bunlardan biri üye profillerinin adla indekslenmesine dair bilinçli bir gizlilik kararı. İnternete servis edilen dosyada hiçbiri yok ve tersini söylüyor. Edge ağı, kimsenin açmadığı bir özellik için kendi sürümünü üretiyor; yani kaynak doğru, inceleme doğru, ve kural basitçe yürürlükte değil. Bir ters proxy seçmek, origin’in yerine cevap vermeye yetkili bir şey seçmektir — ve neyi devraldığı yalnız canlı sisteme sorularak öğrenilir, kodu okuyarak asla.

Sırada ne var: bir branch’in seçtiği iki ortam

Bugün tek bir ortam var ve canlı olan o. Aşağıdaki şekil, inşa edilmeden önce yazıldı: kısıtlarından üçü, aksi hâlde pahalı yoldan öğrenilecek kadar sezgiye aykırı.

İnsanın seçtiği tek şey branch’tir

Geliştirme branch’i kalıcı test ortamını, ana branch gerçek olanı deploy eder. Geri kalan her şey — host adları, kimlik instance’ı, imaj etiketleri, sırlar — o tek seçimden türer. Deployment tanımları bilinçle iki kopyaya ayrılmıyor: iki kopya ayrışır ve düzenlenen kopya, o gün kimin açtığına kalır; bu yüzden branch dosyayı değil değerleri seçer.

Yarısı promote edilemez, yarısı edilebilir

Alışılmış pratik, bir artefaktı bir kez kurup ortamlar arasında taşımaktır. Tarayıcı bundle’ı API host’unu derleme anında içine gömer, yani test için kurulan imaj o host’u içinde taşır ve prod yeniden etiketlemek yerine yeniden kurmak zorundadır. Backend imajlarında derleme-zamanı ortam yoktur ve temiz promote olurlar. Tek bir deployment’ın iki yarısının gerçekten farklı kuralları olması, prod’da test API’siyle konuşan bir arayüz üretmeden önce yazılmaya değer.

İki ortam iki makinedir

Bu bir tercih değil bir ölçüm. Filo zaten kutusunun kenarında duruyor ve tek veritabanı cluster’ı bağlantı tavanıyla sınırlı — kompozisyondaki her havuz boyutunun açıkça sınırlanmasının sebebi bu. Tek makinede iki proje bir ortam ayrımı değildir; iki adı ve ortak bir çökme yolu olan tek bir ortamdır.

Test ortamı bir ön kapı alacak, ve o kapı uygulama kodu değil

Uygulamanın içine yazılan bir kapı yalnız tek bir ortamda koşar: onu koruyan ortam tarafından hiç sınanamaz ve prod’a bir bayrağın arkasında ölü kod olarak gider. Yeri edge’dir — isteğin origin’e ulaşmadan reddedildiği yer. Ve dürüst sınırı da söylenmeye değer: API host’unu kapsayamaz, çünkü tarayıcı oraya çerezle değil doğrudan bearer token ile gider, ve tarayıcı bundle’ına derlenen paylaşılan bir sır, sır değildir. Yani gerçekten kapatılmaya değer şey bir URL değil hesap açılışıdır — ki o da üründe bir dal değil, kimlik sağlayıcıda bir ayardır.

Sayılarla teslimat

14

deploy edilen modül

9

CI/CD workflow’u

32

pull request başına config kapısı

49

yazılı tuzak

Bunların hiçbiri sistemi doğru yapmaz. İddialarını doğrulanabilir yapar — ve yanılmaya dayanan tek özellik odur.

Sayılarla

Tahmin değil, sayıldı

Bu sayfadaki her rakam bu tablodan geliyor ve her satır kontrol edilebilsin diye nasıl sayıldığını söylüyor. Satır sayıları yorumları da içeriyor: bu kod tabanında gerekçe bilinçli olarak kodun yanında yaşıyor.

ÖlçütDeğer
Maven modülü25
Servis13
Deploy edilen modül14
Backend kaynak dosyası1.516
Backend kaynak satırı70.869
Backend test dosyası331
Backend test vakası1.524
Integration test sınıfı27
Use-case interactor’ı146
REST controller55
HTTP endpoint’i191
Kafka listener metodu73
Kayıtlı event topic’i26
Veritabanı migration’ı106
Güvenlik denetimi bulgusu54
Pull request başına konfigürasyon kapısı32
Alarm kuralı18
CI/CD workflow’u9
Yazılı tuzak49
Frontend kaynak dosyası616
Frontend kaynak satırı55.022
Frontend test vakası436
Frontend domain modülü12
Dil başına çeviri anahtarı1.298
Depolar genelinde commit270

2026-08-09 tarihinde depolardan sayıldı. Geliştirme aralığı: 2026-02-28 → 2026-08-09.

Durum

Ne bitti, ne ertelendi, ne açık

Sistem deploy edildi; ürün lanse edilmedi. Bunlar farklı cümleler — hangisinin doğru olduğunu, neyin bitmediğini ve nedenini yazmak, her şeyin yayınlandığını ima eden bir sayfadan daha yararlı.

Bitti

  • Modüler monolitten on üç servislik filoya geçiş; kod olarak tamam ve container’larda uçtan uca çalışıyor.
  • Fiziksel database-per-service; servisler arası her okuma yerel bir projeksiyondan karşılanıyor.
  • Filo genelinde transactional outbox, idempotent consumer’lar, dead-letter ve istendiğinde replay.
  • Edge’de tek kimlik doğrulama sınırı; roller token’dan değil identity servisinden çözülüyor.
  • Observability canlı doğrulandı — trace, metrik, gerçek bir hedefe giden alarmlar ve yalnızca hataları alan bir error sink — ve sınırlı yük testi hiç sunucu hatası olmadan geçti.
  • On iki domain modülü boyunca dört dilli bir arayüz; mimari kurallar sürekli entegrasyonda zorlanıyor.
  • Çalışan filoya gerçek arızalar enjekte eden bir dayanıklılık kampanyası. Yedi gerçek kusur çıkardı: identity servisine ulaşılamadığında açık kalan bir edge, geri yüklemesi gereken read-model’i silen dokümante edilmiş bir rebuild yordamı, kaybolan bir broadcast denetim kaydı ve sürekli entegrasyonun hiç build etmediği dört servis bunlar arasında. Yedisi de kapatıldı — kampanyanın başta bilinçli olarak açık bıraktığı üç tutarlılık sorusuyla birlikte.
  • Canlıya çıkmadan önce, nihai saldırı yüzeyine karşı koşsun diye en sona bırakılan on bir fazlık tam bir güvenlik denetimi. 54 bulgu, sıfır yanlış pozitif, dokuzu kritik; her biri ya kapatıldı ya da gerekçesiyle kaydedildi. Geriye bir rapor değil mekanizma bıraktı: her pull request’te konfigürasyon kapıları, taranan bir bileşen envanteri, negatif bir uçtan uca tier ve kendi kodundan eski bir filoyu reddeden bir kontrol.
  • Uçtan uca ve kendini doğrulayan deployment: bir push yalnız değişeni kurar, yayımlar, kompozisyonu sunucuda uygular, her modülün hazır olduğunu bildirmesini bekler, sonra koşan filoya hangi build olduğunu ve takılmış bir mesaj olup olmadığını sorar. Dondurulmuş monolit, filo tam bir gün canlı kaldıktan sonra devreden çıkarıldı — en son ve yalnız açık onayla, tam da planın dediği gibi.
  • Arayüz aynı kutuda, dört dilde ve aynı tünelin arkasında canlı — yani ürün yalnız API olarak değil uçtan uca erişilebilir durumda. Koşan sisteme sorulan ilk probe on üç soru sordu, onu temiz döndü ve bunların arasında bir deponun cevaplayamayacağı olanlar da vardı: health ucu bir durumdan başka hiçbir şey açık etmiyor, on yönetim ucu anonim çağıranı reddediyor, rotalanmamış bir istek sorulanı geri yansıtmayan bir hata belgesi döndürüyor, ve altmış paralel istek kırk cevap ile yirmi redde dönüşüyor.

Ertelendi, gerekçesiyle

Ödemeler

Kodun dışındaki bir sebeple ertelendi: ödeme sağlayıcısı yalnızca kayıtlı bir şirkete aracılık ediyor ve henüz bir şirket yok, dolayısıyla canlı tahsilat yasal olarak açılamıyor. Billing servisi silinmedi; ücretli yüzeyi varsayılan olarak kapalı çıkıyor, bedava-premium rayı çalışmaya devam ediyor. Tetik bir commit değil, bir şirket.

Kayıtlı non-goal’lar

Non-blocking retry topic’leri, outbox için change-data-capture ve autoscaling’li bir Kubernetes topolojisi. Her biri gerekçesi yazılı olarak ertelendi; böylece gelecekteki bir karar sıfırdan değil argümandan başlıyor.

Açık

  • Sistemdeki her eşik — servis hedefleri, oran limitleri, günlük yükleme kotası — kendisini seçtirecek hiçbir trafik yokken seçildi. Hepsi gerekçesi yanına yazılmış bilinçli tahminler, ve bir saatlik gerçek yük aksini söyleyene kadar tahmin olarak kalacaklar.
  • Content Security Policy hâlâ report-only modda. Enforce’a geçmek bir kod değişikliği değil bir ölçüm: bir saatlik gerçek trafik ve boş bir ihlal raporu. Bir politikayı tahminle enforce etmek bir kez zaten “hiç kimsenin giriş yapamaması” demişti.
  • Denetimin kod tarafında kapatamadığı ve kapatmış gibi de yapmadığı iki şey. Prompt-injection savunmasının anlamsal yarısı, bilinçli olarak zehirlenmiş bir korpusa karşı canlı bir koşu ve ücretli bir model çağrısı istiyor. Eşgüdümlü taze hesaplara karşı direnç ise bir oran limiti değil, hesap yaşı üzerine bir ürün kararı: her hesap bir kez oy verdiğinde hiçbir kova dolmuyor.
  • Canlıya çıkış checklist’inin kendisi: beş kırılım, iki tema ve dört dil boyunca canlı bir görsel kabul turu, hattın kabul etmesiyle değil kanalında gözle görülen bir alarm, ve prod kimlik instance’ına karşı koşulmuş bir giriş akışı. Hepsi çalışan bir sistem ve bir çift göz istiyor; bu yüzden hiçbiri burada iddia edilmiyor.

Deploy edilmiş, ölçülebilir ve doğrulanabilir — ve hâlâ bitirmediği şeyler konusunda dürüst. Bu ikisi çelişmiyor: birincisini söylemeye değer kılan şey ikincisi.