Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Architecture Decision'lar — #04

RFC ve Design Doc: Büyük Kararı Yazıyla Tartışmak

Mert Özen 8 Kas 2026 13 dk 3 görüntülenme
RFC ve Design Doc: Büyük Kararı Yazıyla Tartışmak

Çözümü belirsiz ve birden fazla ekibi etkileyen bir karar önce bir design doc'ta, gerekirse kuralları yazılı bir RFC süreciyle tartışılır; sonuç ise ayrı ve değişmez bir ADR olarak kaydedilir. Bookstore'un sipariş servisine outbox eklemek için yazılmış iki sayfalık design doc üzerinden Context and Scope, ölçülebilir Goals, gerçek Non-Goals, Alternatives Considered, cross-cutting concerns, yaşam döngüsü ve RFC'nin yorum süresi.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'da sipariş e-postaları her deploy'da kayboluyor; Mimariye Giriş serisinde bilerek kabul edilen bu bedel, Inventory ve Payment de aynı event'e ihtiyaç duyunca kabul edilemez hale geldi. Ada bir design doc yazmaya başladı. İlk taslağın Goals ve Non-Goals bölümleri şöyle:

## Goals

* Order events reach Inventory, Payment and Notification after the order is saved
* Fast checkout

## Non-Goals

* The system must not lose events
* No downtime during deployment
* Exactly-once delivery to consumers
* Moving Bookstore to microservices

Non-Goals altındaki dört maddeden hangileri gerçekten non-goal? Hangileri aslında birer goal'ün olumsuz cümleyle yazılmış hali? Goals'taki iki maddeden hangisi bir reviewer'ın "bunu nasıl ölçeceğiz?" sorusuna cevap veremez?

Design Doc Nedir, Ne Değildir

Malte Ubl'un 2020 tarihli "Design Docs at Google" yazısı, design doc'u kodlamaya başlamadan önce yazılan, çözümün üst seviye stratejisini ve asıl önemlisi bu stratejideki trade-off'ları kayda geçiren gayriresmî bir doküman olarak tanımlıyor. Kilit kelime trade-off. Design doc'un işi bir çözümü anlatmak değil, birden fazla makul çözüm arasında neden birinin seçildiğini, henüz kod yazılmamışken, değişiklik ucuzken tartışmaya açmak.

Bir ADR (Architecture Decision Record) ile farkı burada. ADR verilmiş bir kararın kaydı: kısa, kararın kendisine odaklı ve karar verildikten sonra değişmiyor. Design doc ise kararı vermek için yapılan tartışmanın aracı: daha uzun, tasarımın ayrıntılarına giriyor ve review süresince sürekli değişiyor. İlk yazıdaki sınıflandırmada D07'nin, yani outbox kararının, doğrudan ADR'a değil önce design doc'a gitmesinin nedeni buydu: üç module etkileniyordu ve çözüm henüz belirsizdi.

Ubl, design doc'un ne zaman yazılmaması gerektiğini de söylüyor. Çözüm tartışmasızsa, yani makul tek bir yol varsa, design doc yazmak bir implementasyon kılavuzu yazmaya dönüşür ve kimse onu okumaz. Hızlı prototipleme gereken, sorunun ancak denenerek anlaşılacağı durumlarda da doküman zaman kaybıdır; bir önceki yazıdaki cache spike'ı buna örnek. Uzunluk için önerisi büyük projelerde 10–20 sayfa, küçük işlerde 1–3 sayfalık "mini design doc". Bookstore ölçeğinde neredeyse her design doc mini olmalı.

Context and Scope, Goals ve Non-Goals

Outbox design doc'unun ilk sayfası, taslağın review'dan sonraki hali:

# Order events through a transactional outbox

Status: In review (comments until 2026-10-09)
Authors: Ada, Grace · Reviewers: Linus (ops), Fulfillment team, Alan
Decision record: ADR-0011 (to be written after review)

## Context and Scope

Ordering saves the order and then hands OrderPlaced to an in-process queue
(ADR-0004). Messages in the queue are lost when the process stops; we see
3-12 lost e-mails per weekly deploy. Inventory and Payment now need the same
event to reserve stock and capture payment, and losing it there means an
order that is never shipped or never charged. This document covers how
Ordering publishes order events reliably inside the modular monolith. It does
not cover how consumers process them.

## Goals

* Every committed order produces exactly one OrderPlaced record, in the same transaction
* Consumers receive the event at least once, within 5 seconds at p95
* A deploy or crash loses no events
* Checkout p95 stays under 800 ms

## Non-Goals

* Exactly-once delivery: consumers must be idempotent (Inbox, see Messaging series)
* A message broker: we stay in-process until a second deployment unit exists
* Event versioning and schema registry
* Moving existing events of Catalog and Inventory to the outbox

Context and Scope kısa ve nesnel: ne oluyor, neden şimdi sorun, doküman neyi kapsıyor ve neyi kapsamıyor. Ubl'un uyarısı burada önemli: bu bölüm bir requirement dokümanı değil, okurun geri kalanı anlaması için gereken minimum arka plan. Sayılar var (deploy başına 3–12 kayıp e-posta), çünkü sorunun büyüklüğü çözümün ne kadar karmaşık olmayı hak ettiğini belirliyor.

Goals'a dikkat et: "Fast checkout" gitti, yerine Mimariye Giriş serisinin üçüncü yazısındaki ölçülebilir senaryo geldi (checkout p95 < 800 ms). "Events reach consumers" da "en az bir kez, p95'te 5 saniye içinde" oldu. Bir goal'ün ölçülebilir olup olmadığını anlamanın kolay yolu, onu doğrulayacak testi ya da metriği hayal etmek; hayal edemiyorsan goal değil, bir dilek yazmışsın.

Non-Goals'ta açılıştaki taslağın iki maddesi yok oldu. "The system must not lose events" bir non-goal değil, olumsuz cümleyle yazılmış bir goal; yeni Goals listesinde "A deploy or crash loses no events" olarak duruyor. "No downtime during deployment" da öyle; üstelik bu design doc'un konusu değil, hiçbir listede durmamalı. Ubl'un tanımı net: non-goal, makul olarak bir goal olabilecekken bilinçli olarak dışarıda bırakılan şey. Yazısındaki örnek bir veritabanı tasarımında ACID (Atomicity, Consistency, Isolation, Durability) uyumluluğunu non-goal yapmak: makul bir hedef, ama bu sistem için bilerek seçilmiyor. Bizim listemizde "exactly-once delivery" tam böyle bir madde. Birisi "outbox ekliyorsak exactly-once da yapalım" diyebilir; non-goal olarak yazmak bu tartışmayı review'dan önce kapatıyor ve sonucunu söylüyor: consumer'lar idempotent olmak zorunda.

.NET'in dotnet/designs repo'sundaki proposal şablonunda da Goals ve Non-Goals ayrı başlıklar. Şablonun Non-Goals açıklaması aynı şeyi söylüyor: insanların senin çözdüğünü düşünebileceği, ama bilerek kapsam dışı bıraktığın problemler. Şablon, bu listenin genellikle ilk geri bildirimlerden sonra büyüdüğünü de not ediyor; review'da "bunu da çözecek misiniz?" sorusunun her "hayır" cevabı bir non-goal maddesi.

Design ve Alternatives Considered

## Design

PlaceOrder writes the order and an OutboxMessage row in one SaveChanges call.
A background dispatcher reads unprocessed rows in id order, publishes them to
the in-process bus and marks them processed. Rows older than 7 days are deleted.

    ordering.OutboxMessages
      Id            bigint identity   -- dispatch order
      Type          nvarchar(200)     -- e.g. Bookstore.Ordering.OrderPlaced
      Payload       nvarchar(max)     -- JSON, no personal data
      OccurredUtc   datetime2
      ProcessedUtc  datetime2 null

The dispatcher runs in every instance; rows are claimed with an UPDATE
that uses the UPDLOCK and READPAST hints, so two instances never publish
the same row at the same time. It can still publish a row twice after a crash between
publish and mark-processed; that is why consumers must be idempotent.

## Alternatives Considered

* Keep the in-process queue (ADR-0004): no new table, but losses continue
  and Inventory and Payment cannot accept them.
* Publish to a broker inside the database transaction: needs a broker with
  distributed transactions; we have no broker and do not want 2PC.
* Change data capture on the Orders table: no code in Ordering, but it
  exposes table structure as the contract and needs CDC operations work.

Design bölümü önce bir genel bakışla başlıyor, ayrıntılara sonra giriyor. Veri saklama kaba hatlarıyla yazılmış; tam şema, index ve migration yok, çünkü review'ın konusu bunlar değil. API'nin (Application Programming Interface) de yalnız trade-off'la ilgili kısmı var. Design doc'a koyduğumuz tek kod parçası, Ordering'in geri kalanının göreceği interface'lerin taslağı; Bookstore.Ordering project'inde derlendiğini kontrol ettik:

namespace Bookstore.Ordering;

public sealed record OutboxMessage(long Id, string Type, string Payload, DateTime OccurredUtc, DateTime? ProcessedUtc);

public interface IOutbox
{
    // Kayıt, siparişle aynı SaveChanges çağrısında yazılır; ayrı bir transaction yok
    void Add<TEvent>(TEvent domainEvent) where TEvent : notnull;
}

public interface IOutboxDispatcher
{
    // İşlenmemiş satırları Id sırasıyla yayınlar ve kaç satır yayınladığını döner
    Task<int> DispatchPendingAsync(int batchSize, CancellationToken ct);
}

Ubl'un kuralı: design doc'ta kod ve pseudo-code nadir olmalı, yalnız yeni bir algoritmayı ya da bir interface'in şeklini anlatmak için kullanılmalı. Burada interface'in şekli önemli, çünkü Add'in bir transaction açmaması, yani siparişle aynı SaveChanges'e katılması, tasarımın bütün garantisinin dayandığı nokta.

Alternatives Considered, Ubl'un en önemli bölümlerden biri saydığı kısım. Her alternatif için iki şey var: neden makul olduğu ve hangi trade-off yüzünden seçilmediği. CDC (Change Data Capture) alternatifi kötü bir fikir değil; Ordering'e hiç kod eklemeden çalışırdı. Reddedilmesinin nedeni tablo yapısının bir contract'a dönüşmesi ve yeni bir operasyon işi. Bu satır, altı ay sonra "neden CDC kullanmadık?" diye soran birine bir sonraki yazının konusu olan ADR'dan önce cevap veriyor. Transactional outbox'ın kendisini, dual write problemini ve inbox tarafını Messaging ve Event-Driven Architecture serisinin outbox yazısında ayrıntılı görmüştük; design doc onu tekrar anlatmıyor, ona link veriyor.

Cross-Cutting Concerns ve Rollout

## Cross-Cutting Concerns

* Security: the dispatcher uses the application's database login; no new
  credentials.
* Privacy: payloads carry ids, never e-mail addresses or names. Notification
  reads contact data from Customer when it sends.
* Observability: metrics outbox.pending (rows not processed) and
  outbox.lag_seconds (age of the oldest pending row); alert when lag > 60 s.
* Operations: deleting processed rows is a scheduled job; table size is on
  the database dashboard.

## Rollout

1. Add the table and the dispatcher behind a feature flag (off).
2. Write to both the queue and the outbox for one week, compare counts.
3. Switch Notification, then Inventory and Payment, to the outbox.
4. Remove the in-process queue; ADR-0011 supersedes ADR-0004.

## Open Questions

* Is 7 days of retention enough for support investigations?

Cross-cutting concerns bölümü, tasarımı her design doc'ta sorulması gereken sorulardan geçiriyor: security, privacy, observability. Ubl, Google'da privacy için ayrı bir design doc ve ayrı review'lar zorunlu olduğunu yazıyor; Bookstore ölçeğinde bir madde yeterli, ama madde boş geçilmemeli. Privacy maddesi tasarımı değiştirdi: ilk taslakta payload'da müşterinin e-posta adresi vardı. Outbox tablosu yedi gün saklanıyor, yedeklere giriyor ve ileride başka consumer'lara açılabilir; kişisel veriyi oraya koymak, OWASP (Open Worldwide Application Security Project) Top 10 serisinde konuştuğumuz veri yayılmasının tipik bir örneği olurdu. Observability maddesi ise yeni bir arıza biçimini görünür kılıyor: dispatcher durursa hiçbir şey hata vermez, yalnız outbox.lag_seconds büyür.

Rollout planı, kararın geri alınabilirliğini adım adım koruyor: feature flag, bir hafta paralel yazma ve sayıları karşılaştırma, consumer'ları tek tek taşıma. İlk yazıdaki dille, tek yönlü görünen bir kapıyı iki yönlü adımlara bölüyor. Open Questions ise reviewer'lardan özellikle istenen cevapları topluyor.

Yaşam Döngüsü: Taslak, Review, Implementasyon, Bakım

Ubl'un yazısı design doc'un dört aşamasını tarif ediyor. Taslak aşamasında yazar, gerekirse bir ortak yazarla, konuyu bilen birkaç kişiyle dokümanı hızla iterasyona sokar. Review aşamasında doküman daha geniş bir kitleye açılır; bu, bir e-posta listesine gönderip yorum beklemek kadar hafif ya da kıdemli mühendislerle yapılan resmî bir toplantı kadar ağır olabilir. Implementasyon aşamasında doküman bitmiş sayılmaz: gerçeklik plandan ayrıldığında doküman da güncellenir. Bakım aşamasında ise sistemi anlamak isteyenler için bir giriş noktası olarak kalır; Ubl yazarlara bir-iki yıl sonra kendi dokümanlarına dönüp neyi doğru, neyi yanlış öngördüklerine bakmalarını öneriyor.

Bookstore'da outbox design doc'u şöyle ilerledi: Ada ve Grace iki gün içinde taslağı yazdı, Linus ile bir saat oturup cross-cutting concerns'ü birlikte doldurdular. Doküman bir pull request olarak açıldı, Fulfillment ekibi ve Alan reviewer olarak eklendi ve yorum süresi bir haftaydı. Review'da üç önemli değişiklik çıktı: privacy maddesi, non-goal'lerin düzeltilmesi ve rollout'taki bir haftalık paralel yazma adımı. Implementasyon sırasında dispatcher'ın satır kilitleme yöntemi değişti ve doküman güncellendi. Karar ise ADR-0011 olarak ayrı bir dosyaya yazıldı; design doc'un statüsü "Implemented" oldu ve ADR'dan link aldı.

Design doc'un ADR'dan farklı olarak değişebilmesi bilinçli bir tercih. ADR kararın değişmez kaydı; design doc ise tasarımın yaşayan açıklaması. İkisini aynı dosyada tutmaya çalışmak, ya ADR'ın değişmezliğini ya da design doc'un güncelliğini bozar.

RFC Süreci: Yorum Süresi, Karar ve ADR

Design doc'u kimin review edeceği belliyse yukarıdaki akış yeterli. Karardan etkilenen kitle ekibin dışına taştığında, ya da kimin etkilendiği bile belli olmadığında, sürece bir RFC (Request for Comments) adı verilir ve kuralları yazılı hale gelir. İsim internetin kendisinden geliyor: Steve Crocker'ın 7 Nisan 1969 tarihli RFC 1'i, tartışmayı davet eden bir ağ protokolü notuydu ve o gelenek bugünkü internet standartlarına kadar sürüyor. Yazılım ekiplerindeki RFC ise bu fikrin küçük ölçekli bir uyarlaması: herkesin yorum yapabildiği, süresi ve karar vericisi belli bir yazılı tartışma.

Rust'ın RFC süreci iyi bilinen bir örnek: bir RFC karar aşamasına geldiğinde final comment period (FCP) başlar ve en az beş iş gününe denk gelsin diye on takvim günü sürer. dotnet/designs'ta ise proposal bir pull request olarak açılır ve geniş bir uzlaşma oluştuğunda merge edilir. Bookstore ekip sayısı arttığında kullanacağı süreç, repo'daki docs/rfcs/README.md'de beş adım:

# RFC process

1. Copy docs/rfcs/0000-template.md to docs/rfcs/NNNN-short-title.md and
   open a pull request with the "rfc" label.
2. Announce the PR in the #architecture channel with a comment deadline,
   at least 5 working days away.
3. Anyone may comment; the author answers every comment in the PR.
4. On the deadline the deciders named in the RFC decide: accepted,
   rejected or postponed. The outcome is written as the last PR comment.
5. The RFC is merged in every case. Accepted RFCs get an ADR that links
   back to them; rejected and postponed ones keep their reason.

Üç kuralın her biri bir sorunu kapatıyor. Yorum süresi, sessizliğin onay sayılmasını meşru kılıyor; "kimse itiraz etmedi" cümlesi ancak itiraz için bir süre tanınmışsa anlamlı. İsimli karar vericiler, RFC'nin sonsuz bir tartışmaya dönüşmesini engelliyor. Reddedilen RFC'nin de merge edilmesi, ikinci yazıdaki reddedilmiş ADR ile aynı mantık: aynı tartışma altı ay sonra açıldığında ilk RFC ve reddedilme gerekçesi bulunabilsin.

ADR, RFC ve Design Doc Arasındaki İlişki

DokümanAmacıNe zaman değişirBookstore'da yeri
Design docÇözümü ve trade-off'larını tartışmaya açmakReview ve implementasyon boyuncadocs/design/
RFCDesign doc'u ekip dışına açan, kuralları yazılı süreçYorum süresi bitene kadardocs/rfcs/, pull request ile
ADRVerilen kararın kısa, değişmez kaydıHiç; yenisiyle supersede edilirdocs/decisions/

Akış tek yönlü: design doc tartışmayı taşır, RFC gerektiğinde o tartışmanın kurallarını koyar, ADR sonucu kaydeder. Her ADR, kendisini doğuran design doc'a ya da RFC'ye link verir; tersi de doğru. Küçük kararların çoğu ise ikisine de uğramadan doğrudan ADR olur; ilk yazıdaki on kararın yalnız biri design doc gerektiriyordu.

Sık Düşülen Tuzaklar

Non-goal'ü negatif goal sanmak. "Sistem çökmemeli", "veri kaybolmamalı" non-goal değil, olumsuz cümleyle yazılmış goal'lerdir. Non-goal, makul olarak hedeflenebilecekken bilerek dışarıda bırakılan şeydir.

Ölçülemeyen goal'ler. "Hızlı checkout" review'da tartışılamaz, implementasyondan sonra doğrulanamaz. Mimariye Giriş'teki senaryo kalıbını kullan.

Alternatives Considered'ı boş ya da göstermelik bırakmak. Tek seçenekli bir design doc ya gereksizdir ya da kararı önceden vermiş bir doküman. İkisinde de review boşa gider.

Design doc'u implementasyon kılavuzuna çevirmek. Class listeleri, tüm endpoint'ler ve migration script'leri dokümanı uzatır ama tartışılacak bir şey eklemez. Ubl'un ölçütü: trade-off yoksa design doc da gerekmez.

Kararı design doc'un içinde bırakmak. Doküman değiştikçe "sonunda ne karar vermiştik?" sorusu cevapsız kalır. Kararı ayrı ve değişmez bir ADR'a yaz.

Süresiz yorum. Bitiş tarihi olmayan bir review, en sessiz kişinin onayını beklemeye dönüşür. Tarih ve karar verici, dokümanın ilk satırlarında olmalı.

Küçük Bir Deneme

★☆☆ Açılıştaki taslağın dört non-goal maddesini sınıflandır: gerçek non-goal, olumsuz yazılmış goal, kapsam dışı konu. Sonra kendi projenden bir design doc ya da büyük bir PR (pull request) açıklaması bul ve aynı sınıflandırmayı yap.

★★☆ Outbox design doc'una dördüncü bir alternatif ekle: "Inventory ve Payment'ı Ordering'den senkron çağırmak". Neden makul olduğunu ve hangi goal'ü ihlal ettiği için seçilmediğini tek paragrafta yaz.

★★★ Ekibinde bekleyen büyük bir değişiklik için 2 sayfalık bir mini design doc yaz: Context and Scope, ölçülebilir Goals, en az iki gerçek Non-Goal, iki alternatif ve security, privacy, observability maddeleri. Bir haftalık yorum süresiyle bir PR olarak aç; süre bitince kararı bir ADR olarak kaydet ve design doc'a link ver.

Kararları verdik, yazdık ve tartıştık. Ama diyagramlar hâlâ birinin bilgisayarındaki bir çizim dosyasında, arc42 gibi bir yapı da yok. Bir sonraki yazıda mimariyi belgelemeye geçiyoruz: arc42 şablonunun bölümleri, docs as code ve C4 (Context, Container, Component, Code) diyagramlarını ADR'larla aynı repo'da yaşatmak.