ADR Yazmak: Nygard Şablonu, MADR 4.0 ve Y-Statement ile Karar Kaydı
İyi bir ADR yalnız kararı değil, hangi soruya cevap verdiğini, hangi seçeneklere karşı verildiğini, kabul edilen bedeli ve ne zaman yeniden açılacağını yazar ve karar verildikten sonra değiştirilmez. Nygard'ın beş bölümlü şablonu, MADR 4.0.0'ın bölümleri ve front matter'ı, reddedilmiş bir karar kaydı, Y-statement, status yaşam döngüsü ve immutability kuralını PR'da zorlayan gerçek bir script.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'da modular monolith kararı Mart'ta verildi. Kayıt ise Temmuz'da, yeni gelen bir developer "bu yapının bir dokümanı var mı?" diye sorunca yazıldı. Repo'daki docs/decisions/0003-modular-monolith.md dosyasının tamamı şu:
# Modular monolith
We use a modular monolith. Each module lives in src/Modules and has its own
DbContext. Modules talk to each other through public interfaces.
Cümlelerin hepsi doğru. Bir yıl sonra ekibe katılan biri bu dosyadan neyi öğrenemez? Kaç tane soru sayabilirsin? Bir de şunu düşün: bu bir karar kaydı mı, yoksa mevcut yapının bir tarifi mi?
Nygard'ın Şablonu: Beş Bölüm
Yukarıdaki dosya "ne"yi anlatıyor, "neden"i değil. Hangi sorunun çözüldüğü, hangi seçeneklerin konuşulduğu, hangi bedelin kabul edildiği, kimin karar verdiği ve kararın ne zaman yeniden açılacağı yok. Michael Nygard'ın 2011 tarihli "Documenting Architecture Decisions" yazısı bu boşluğu beş bölümle dolduruyor: Title, Context, Decision, Status ve Consequences. Bookstore'un e-posta kararı, yani Mimariye Giriş serisinin "Trade-off Düşüncesi" yazısında verilen karar, bu şablonla şöyle görünür:
# 4. Order e-mail through an in-process queue
Date: 2026-03-19
## Status
Accepted
## Context
Checkout sends the confirmation e-mail inside the request. When the e-mail
provider is slow, checkout takes as long as the provider; when it is down,
checkout fails and customers retry, which creates duplicate orders. Checkout
availability and latency are in our top three characteristics. We have one
part-time ops engineer and no message broker.
## Decision
We will put the confirmation e-mail on a bounded in-process queue
(System.Threading.Channels) and send it from a background worker that retries
until the provider is back.
## Consequences
Checkout no longer depends on the e-mail provider. E-mails still in the queue
are lost when the process stops (deploy, crash); customers can see the order
on the order page and support can resend the e-mail. We will log how many
messages are left in the queue at shutdown and revisit this decision when
more than 10 e-mails a month are lost or another module needs order events.
Title numaralı ve kısa bir isim. Context, kararı doğuran güçleri tarafsız bir dille anlatıyor: teknik, ekip ve proje koşulları, aralarındaki gerilimle birlikte. Decision, Nygard'ın önerdiği gibi aktif bir cümleyle başlıyor: "We will…". Status kararın nerede olduğunu söylüyor. Consequences ise yalnız iyi sonuçları değil, kabul edilen bedeli ve kararın yeniden ele alınma koşulunu da yazıyor; bu son cümle, ilk yazıdaki "körü körüne geri alma" senaryosunu önleyen cümle.
Nygard'ın iki kuralı daha var. Kayıt bir-iki sayfayı geçmez; geçiyorsa ya birden fazla karar içeriyordur ya da bir tasarım dokümanına dönüşmüştür. Kayıtlar sıralı numaralanır ve numara bir daha kullanılmaz. Nygard dosyaları repo'da doc/arch/adr-NNN.md altında tutmayı önerir. Bu format, Nat Pryce'ın adr-tools komut satırı aracının ürettiği formatın ta kendisi; yukarıdaki dosya adr-tools ile oluşturulmuş bir ADR (Architecture Decision Record) ile aynı yapıda.
Kayıtları İngilizce yazıyoruz, çünkü Bookstore'un repo dili İngilizce ve ikinci ülkedeki ekip de bu dosyaları okuyacak. Türkçe yazan bir ekip için şablon değişmez; önemli olan bütün decision log'un tek bir dilde olması.
MADR 4.0: Bölümler ve Front Matter
Nygard şablonu kısa, ama seçenekleri ayrı bir bölüm olarak istemiyor; alternatifler varsa Context'in içinde kayboluyor. MADR (Markdown Architectural Decision Records) bu açığı kapatıyor. Oliver Kopp, Anita Armbruster ve Olaf Zimmermann'ın 2018'de bir makaleyle tanıttığı şablonun güncel sürümü, adr.github.io'da yazım anında kontrol ettiğimiz haliyle, 17 Eylül 2024 tarihli 4.0.0. Bookstore'un modular monolith kararı, açılıştaki dosyanın olması gereken hali:
---
status: accepted
date: 2026-03-12
decision-makers: Ada (tech lead), Alan, Grace
consulted: Linus (ops)
informed: product team
---
# Start with a modular monolith
## Context and Problem Statement
Bookstore is one ASP.NET Core application with one SQL Server database,
built by three developers and a part-time ops engineer who deploy weekly.
The 18-month plan expects three times the order volume, a second country
and four stream-aligned teams. Catalog, Ordering, Inventory, Payment,
Shipping and Notification exist as namespaces, but nothing stops Ordering
from reading Catalog tables directly. How do we structure the system so we
can ship weekly today and split the work across teams later?
## Decision Drivers
* Modifiability: a new payment method takes at most 3 developer-days and changes only Payment
* Availability: checkout is available 99.9% of the month
* One team and one part-time ops engineer; no platform team yet
* Weekly deploys stay a single pipeline
## Considered Options
* Layered monolith (today's structure)
* Modular monolith
* Microservices, one service per module
## Decision Outcome
Chosen option: "Modular monolith", because it is the only option that gives
the module boundaries our modifiability scenario needs without adding network
calls, separate deployments and an operational load that one part-time ops
engineer cannot carry.
### Consequences
* Good, because module boundaries become explicit and testable before the teams split.
* Good, because there is still one deployment, one database server and one pipeline.
* Bad, because a memory leak in one module can still take the whole process down.
* Bad, because boundaries inside one process erode easily and need automated checks.
### Confirmation
An architecture test (ArchUnitNET) in CI fails when a module uses another
module's internal types. The monthly architecture review checks new
cross-module calls.
## Pros and Cons of the Options
### Layered monolith (today's structure)
* Good, because there is no migration work.
* Bad, because a payment change touches the Web, Business and Data layers.
* Bad, because nothing shows which code will belong to which team.
### Modular monolith
* Good, because each module owns its tables and exposes a public contract.
* Neutral, because the database server stays shared; each module gets its own schema.
* Bad, because moving code into modules costs about two sprints.
### Microservices, one service per module
* Good, because modules could be deployed and scaled independently.
* Bad, because independent deployability is explicitly a "not now" characteristic.
* Bad, because tracing, retries and on-call for six services exceed our ops capacity.
## More Information
Revisit when a second team needs its own release cadence, or when one module
must scale independently during campaigns. Related: ADR-0002 (SQL Server as
the single database), ADR-0004 (order e-mail through an in-process queue).
Dosyanın başındaki YAML (YAML Ain't Markup Language) front matter, kararın meta verisini gövdeden ayırıyor. status ve date kararın nerede olduğunu ve en son ne zaman güncellendiğini söylüyor. decision-makers, consulted ve informed alanları ilk yazıdaki "kararın sahibi kim?" sorusunun cevabı; consulted, iki yönlü iletişim kurulan uzmanlar, informed ise yalnız bilgilendirilenler. 3.0 sürümündeki deciders alanının adı 4.0'da decision-makers oldu.
Gövdede zorunlu bölümler üç: Context and Problem Statement, Considered Options ve Decision Outcome. Problem statement'ı bir soruyla bitirmek iyi bir alışkanlık; okur hangi sorunun cevabını okuduğunu bilir. Decision Drivers, seçenekleri tartmak için kullanılan güçler; burada Mimariye Giriş serisinden gelen ölçülebilir senaryoları kullandık, "esnek olsun" gibi dilekleri değil. Decision Outcome tek cümleyle seçeneği ve gerekçesini veriyor, Consequences bedeli yazıyor.
4.0'ın getirdiği en önemli değişiklik, eski Validation bölümünün Confirmation adıyla Decision Outcome'ın altına taşınması. Bu bölüm "kararın uygulandığını nasıl anlayacağız?" sorusunu soruyor. Cevap bir architecture test, bir CI (Continuous Integration) kontrolü ya da bir review adımı olabilir. Bu serinin altıncı yazısı tamamen bu bölümü otomatik kontrole çevirmek üzerine. Pros and Cons of the Options her seçeneği "Good, because" ve "Bad, because" kalıbıyla değerlendiriyor; aynı kalıbın Consequences'ta da kullanılması bilinçli bir MADR tercihi, okur iki bölümü aynı gözle okuyabiliyor. More Information ise kararın yeniden açılma koşulunu ve ilişkili ADR'ları tutuyor.
Martin Fowler'ın Mart 2026'da yayımladığı ADR yazısındaki bir öneri burada işe yarıyor: kaydı haber yazısı gibi, ters piramit düzeninde yaz; en önemli bilgi en üstte, ayrıntı aşağıda. MADR'ın sırası buna uygun: önce soru, sonra seçilen seçenek, en sonda seçeneklerin ayrıntılı artı ve eksileri. Fowler ve Microsoft'un Well-Architected dokümanı bir şey daha öneriyor: kararın güven düzeyini yazmak. Az bilgiyle verilmiş bir kararı "düşük güven" diye işaretlemek, onu ilk fırsatta yeniden açmayı meşrulaştırır. MADR'da bunun için ayrı bir alan yok; More Information'a bir cümle yeterli.
Reddedilen Karar da Bir Kayıttır
İlk yazıda "PostgreSQL'e geçsek mi?" tartışmasının her yeni developer'la tekrar açıldığını söylemiştik. Bookstore ekibi bu tartışmayı Mayıs'ta yaptı ve SQL (Structured Query Language) Server'da kalmaya karar verdi. Kayıt:
---
status: rejected
date: 2026-05-06
decision-makers: Ada, Grace
consulted: Linus (ops), Alan
informed: all developers
---
# Use PostgreSQL instead of SQL Server
## Context and Problem Statement
The second country needs its own production database, and SQL Server
licensing for one more environment is a new cost. Two developers have used
PostgreSQL before. Should Bookstore move from SQL Server to PostgreSQL before
the second country goes live?
## Decision Drivers
* Licensing cost of one more production environment
* 41 EF Core migrations, three of them with SQL Server specific SQL (rowversion, filtered indexes, full-text catalog)
* Linus knows SQL Server backups, monitoring and failover; nobody has run PostgreSQL in production
* The second country goes live in four months
## Considered Options
* Move to PostgreSQL now
* Stay on SQL Server
* PostgreSQL only for new modules
## Decision Outcome
Chosen option: "Stay on SQL Server", because moving the database four months
before a launch puts the launch at risk for a cost saving that is smaller
than the migration work, and running two database engines doubles the
operational knowledge we need.
### Consequences
* Good, because the launch does not depend on a database migration.
* Bad, because we pay for one more SQL Server environment.
## Pros and Cons of the Options
### Move to PostgreSQL now
* Good, because there is no license cost for new environments.
* Bad, because the full-text catalog search has to be rewritten and tested again.
* Bad, because nobody on the team has operated PostgreSQL in production.
### Stay on SQL Server
* Good, because operations, backups and monitoring stay as they are.
* Bad, because licensing cost grows with every environment.
### PostgreSQL only for new modules
* Good, because new modules could try it with low risk.
* Bad, because two engines mean two backup, monitoring and upgrade processes.
## More Information
Reopen when a third production environment is planned, or when the team has
someone with PostgreSQL operations experience.
Status rejected: önerilen değişiklik kabul edilmedi. Başlık önerinin kendisi ("Use PostgreSQL instead of SQL Server"), seçilen seçenek ise mevcut durumun korunması. İlk okuyuşta tuhaf gelebilir, ama decision log'da arama yapan biri "PostgreSQL" kelimesiyle bu kaydı bulur ve tartışmanın sonucunu, gerekçesini ve hangi koşulda yeniden açılacağını tek dosyada görür. Bookstore'un bütün diğer serilerde SQL Server ile devam etmesinin nedeni de artık bir dosyada yazılı.
Y-Statement: Kararı Tek Cümlede Söylemek
Bazen bir kararın özetine ihtiyaç var: bir PR (pull request) açıklamasında, bir release notunda, decision log'un index'inde. Olaf Zimmermann'ın 2012'de SEI'nin (Software Engineering Institute) SATURN (SEI Architecture Technology User Network) konferansında önerdiği Y-statement, bir kararı altı parçalı tek bir cümleye sığdırıyor: bağlam, sorun, seçilen seçenek, reddedilenler, kazanılan kalite ve kabul edilen bedel. ADR-0003'ün Y-statement'ı:
In the context of a growing Bookstore that will be split across four teams,
facing modules that can freely read each other's tables,
we decided for a modular monolith
and neglected the layered monolith and microservices,
to achieve module boundaries that we can test before the teams split,
accepting that one module's failure can still take the whole process down
and that the boundaries need automated checks.
Cümle uzun, ama zorlayıcı: "to achieve" kısmını yazamıyorsan kararın hangi kaliteyi hedeflediğini bilmiyorsundur, "accepting that" kısmını yazamıyorsan bedeli görmüyorsundur. Zimmermann, cümleyi okumayı zor bulanlar için onu iki-üç cümleye bölmeyi ve gerekirse sona bir "because" eklemeyi de öneriyor. Y-statement'ı tek başına bir kayıt formatı olarak da kullanan ekipler var; biz onu MADR kaydının ilk paragrafı ya da index'teki özet olarak kullanıyoruz.
Status Yaşam Döngüsü ve Immutability
MADR'ın status listesi şöyle: proposed, rejected, accepted, deprecated ve superseded by ADR-NNNN. Bir ADR proposed olarak doğar; review'da kabul edilirse accepted, edilmezse rejected olur. Kabul edilmiş bir karar zamanla geçerliliğini yitirirse iki yol var. Yerine yeni bir karar geldiyse eski kayıt superseded by ile yenisini gösterir. Yerine bir şey gelmeden geçersiz kaldıysa, örneğin bir module tamamen kaldırıldıysa, deprecated olur.
En önemli kural: karar verilmiş bir ADR'ın gövdesi değişmez. Nygard'ın yazısından Fowler'ın 2026 tarihli yazısına ve Microsoft'un dokümanına kadar her kaynak aynı şeyi söylüyor: kararı değiştirmek istiyorsan yeni bir kayıt yaz ve eskisini supersede et. Neden? Çünkü decision log'un değeri tarihçesinde. Mart'ta in-process queue seçilmişti, Ekim'de outbox'a geçildi; bu iki bilgi ayrı ayrı durursa altı ay sonra biri hem neyin neden seçildiğini hem de neyin neden değiştiğini okuyabilir. Eski kaydın üzerine yazılırsa Mart'taki gerekçe ve o gerekçenin hangi koşulda geçersiz kaldığı kaybolur. Yazım hatası düzeltmek ya da kırık bir link'i onarmak gibi anlamı değiştirmeyen düzenlemeler bu kuralın istisnası sayılabilir; ama bunu ekip olarak açıkça kararlaştırmak gerekir.
Dosya adları için MADR'ın önerisi docs/decisions/NNNN-title-with-dashes.md: dört haneli sıralı numara, küçük harf ve tire ile başlık. Numara dosya sistemi sıralamasını kronolojik yapıyor ve bir repo'da 10.000 kayda ulaşılmayacağı varsayılıyor. Başlık kararın kendisini söylemeli, konusunu değil: 0003-modular-monolith.md değil, 0003-start-with-a-modular-monolith.md.
ADR'ı Pull Request ile Review Etmek
ADR kodla aynı repo'da, Markdown olarak durduğu için onu kodla aynı şekilde review edebiliriz. Bir karar proposed statüsüyle bir pull request olarak açılır, yorumlar PR'da toplanır, onay geldiğinde status accepted yapılır ve PR merge edilir. Merge anı, kararın tarihi. Git ve GitHub serisinde gördüğümüz CODEOWNERS dosyası, karar kayıtlarının doğru kişilerin onayı olmadan merge edilmemesini sağlıyor:
# Karar kayıtları en az bir tech lead'in onayı olmadan merge edilmez
/docs/decisions/ @bookstore-org/tech-leads
Immutability kuralını insan dikkatine bırakmak yerine küçük bir script ile kontrol ediyoruz. Script, PR'da değişen her ADR'ın base branch'teki statüsüne bakıyor; proposed değilse yalnız status ve date satırlarının değişmesine izin veriyor ve dosyanın silinmesini reddediyor:
#!/usr/bin/env bash
# Karar verilmiş bir ADR'ın gövdesi değişmez; yalnız status ve date satırları güncellenebilir
set -euo pipefail
base="${1:-origin/main}"
failed=0
while IFS=$'\t' read -r change path; do
[[ "$change" == "A" ]] && continue
old_status=$(git show "$base:$path" | sed -n 's/^status: *"\{0,1\}\([a-z]*\).*/\1/p' | head -n 1)
[[ "$old_status" == "proposed" ]] && continue
if [[ "$change" == "D" ]]; then
echo "::error file=$path::ADR is '$old_status'; do not delete it, mark it deprecated or superseded"
failed=1
continue
fi
body_changes=$(git diff -U0 "$base"...HEAD -- "$path" \
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-](status|date):' || true)
if [[ -n "$body_changes" ]]; then
echo "::error file=$path::ADR is '$old_status'; do not edit it, write a new ADR that supersedes it"
echo "$body_changes" | head -n 4
failed=1
fi
done < <(git diff --name-status --no-renames "$base"...HEAD -- docs/decisions)
[[ $failed -eq 0 ]] && echo "ADR immutability check passed"
exit $failed
Ekim'de Alan, sipariş e-postasını outbox'a taşıyan bir PR açtı ve "doküman da güncel olsun" diye ADR-0004'ün Decision Outcome satırını değiştirdi. Script'in yerel bir repo'daki gerçek çıktısı:
::error file=docs/decisions/0004-order-email-through-an-in-process-queue.md::ADR is 'accepted'; do not edit it, write a new ADR that supersedes it
-Chosen option: "In-process queue", because it removes the e-mail provider
+Chosen option: "Transactional outbox", because it removes the e-mail provider
exit code: 1
Başka bir PR'da biri eskimiş görünen ADR-0004'ü silerek "temizlik" yapmak istedi:
::error file=docs/decisions/0004-order-email-through-an-in-process-queue.md::ADR is 'accepted'; do not delete it, mark it deprecated or superseded
exit code: 1
Doğru yol: yeni bir ADR-0011 açmak ve ADR-0004'ün statüsünü superseded by ADR-0011 yapmak. Diff'te eski kayıtta tek satır değişiyor:
.../decisions/0004-order-email-through-an-in-process-queue.md | 2 +-
.../0011-order-events-through-a-transactional-outbox.md | 11 +++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
ADR immutability check passed
exit code: 0
Script'i her PR'da çalıştıran GitHub Actions workflow'u aşağıda. Script'i ve üç senaryoyu yerelde çalıştırdık; workflow'u GitHub üzerinde bu yazı için koşturmadık. ::error file=…:: satırları GitHub Actions'ın workflow command formatı; PR'da ilgili dosyanın üzerinde bir hata notu olarak görünür.
name: adr-check
on:
pull_request:
paths: ["docs/decisions/**"]
jobs:
immutability:
runs-on: ubuntu-latest
steps:
# Base branch ile karşılaştırmak için tüm geçmiş gerekli
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: ./tools/check-adr-immutability.sh "origin/${{ github.base_ref }}"
Review'da neye bakılmalı? Bizim kontrol listemiz kısa: problem statement bir soru mu, en az iki gerçek seçenek var mı (biri "hiçbir şey yapma" olabilir), her seçeneğin en az bir "Bad, because" maddesi var mı, Consequences'ta bedel yazıyor mu, Confirmation bir kontrol mekanizması söylüyor mu ve More Information'da yeniden açılma koşulu var mı. Bütün "Good" maddeleri seçilen seçenekte, bütün "Bad" maddeleri ötekilerde toplanmışsa, karar muhtemelen önce verilmiş ve kayıt sonradan gerekçelendirilmiştir.
Sık Düşülen Tuzaklar
ADR'ı karar verildikten aylar sonra yazmak. Açılıştaki dosya bunun sonucu: Mart'taki tartışma Temmuz'da hatırlanmıyor, geriye yalnız yapının tarifi kalıyor. ADR'ı kararla aynı PR'da, ya da karardan önce proposed olarak aç.
Seçenekleri göstermeden sonucu yazmak. "Modular monolith kullanıyoruz" bir karar değil, bir durum. Considered Options ve Pros and Cons olmadan okur kararın neye karşı verildiğini bilemez ve aynı tartışmayı yeniden yapar.
Sahte seçenekler. Kimsenin ciddi olarak düşünmediği bir seçeneği listeyi doldurmak için yazmak, kaydın güvenilirliğini düşürür. Seçenekler gerçekten masada olanlar olmalı.
Kabul edilmiş ADR'ı güncellemek. Koşullar değişti diye eski kaydı düzeltmek tarihçeyi siler. Yeni kayıt yaz, eskisini supersede et; yukarıdaki script bunu mekanik olarak zorluyor.
Bir dosyada birden fazla karar. "Modular monolith ve module başına schema ve module'ler arası event'ler" üç ayrı karar; biri değişince öteki ikisinin kaydı da belirsizleşir. Bir karar, bir dosya.
ADR'ı tasarım dokümanına çevirmek. Class diyagramları, endpoint listeleri ve migration adımları ADR'ın yeri değil. Microsoft'un önerisi açık: destekleyici materyale link ver, ama karar o materyal olmadan da anlaşılır olsun.
Küçük Bir Deneme
★☆☆ Açılıştaki üç satırlık dosyayı MADR 4.0 şablonuyla yeniden yaz ve en sona Y-statement'ını ekle. Hangi bölümü yazarken en çok zorlandın? Zorlandığın bölüm, ekibin o kararda en az konuştuğu konu olabilir.
★★☆ check-adr-immutability.sh'e iki kural ekle: dosya adı NNNN-title-with-dashes.md kalıbına uymuyorsa ve status alanı MADR'ın listesinde değilse hata versin. Kendi küçük repo'nda üç senaryoyu tekrar çalıştır.
★★★ Ekibinde verilmiş ama kaydı olmayan bir kararı seç, kararı veren kişilerle 30 dakika konuş ve geriye dönük bir ADR yaz. Status accepted, date gerçek karar tarihi olsun; More Information'a kaydın sonradan yazıldığını ve kimlerle konuşularak oluşturulduğunu not et. Hangi bilgiyi artık kimse hatırlamıyor?
Bu yazıdaki seçenekler küçük ve tanıdıktı; Pros and Cons birkaç maddeyle doldu. Bir sonraki yazıda daha zor bir duruma geçiyoruz: seçeneklerin artısı ve eksisi tartışmalı olduğunda onları öncelikli senaryolara karşı nasıl tartacağımız, ATAM'dan (Architecture Tradeoff Analysis Method) gelen sensitivity ve trade-off point kavramları ve Bookstore kataloğuna cache eklemek için yaptığımız bir spike.