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

Kararları Yaşatmak: Decision Log, Architecture Review ve Advice Process

Mert Özen 8 Kas 2026 12 dk 5 görüntülenme
Kararları Yaşatmak: Decision Log, Architecture Review ve Advice Process

Bir decision log ancak keşfedilebilir kaldığı ve düzenli olarak gözden geçirildiği sürece işe yarar: üretilen bir index, CI'da kontrol edilen supersede zinciri, takvim ve koşul tetikleyicileri ve ayda bir 30 dakikalık review. Bookstore'un 15 ADR'lık log'unu denetleyen bir C# tool'u, advice process ve architecture advisory forum, ADR araçlarının yazım anındaki bakım durumu ve altı kişilik ekip için minimum süreç.

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

Önce Tahmin Et

Bookstore ekibi altı kişiye çıktı: dört developer, bir ops, bir ürün yöneticisi. ADR (Architecture Decision Record) yazmaya başlayalı sekiz ay oldu. Decision log klasörünün bugünkü hali:

$ ls docs/decisions
0001-record-architecture-decisions.md
0002-sql-server-as-the-single-database.md
0003-start-with-a-modular-monolith.md
0004-order-email-through-an-in-process-queue.md
0005-public-order-number-format.md
0006-angular-spa-in-front-of-the-api.md
0007-use-postgresql-instead-of-sql-server.md
0008-cache-catalog-search-with-hybridcache.md
0009-payment-retries-with-a-resilience-pipeline.md
0010-public-api-compatibility-within-a-major-version.md
0011-order-events-through-a-transactional-outbox.md
0013-every-migration-can-be-rolled-back.md
0014-initial-bundle-stays-under-250-kb.md
0015-stay-on-a-supported-lts-dotnet-release.md
12-Shipping-Labels-As-PDF.md

Bu hafta ekibe katılan Barbara iki soru soruyor: "Sipariş e-postaları neden queue üzerinden gidiyor?" ve "Shipping etiketleri için bir karar var mı?" Listeye bakarak ikisini de cevaplayabilir mi? Bir de şunu düşün: bu on beş kayıttan hangisi bugün, kimse hiçbir şeyi değiştirmeden, yeniden konuşulmayı hak ediyor? Listede bunu söyleyen bir şey var mı?

Decision Log'u Keşfedilebilir Tutmak

Listeden üç sorun çıkıyor. Birincisi, 0004'ü açan Barbara in-process queue kararını okuyacak, ama o kayıt artık geçerli değil; yerine geçen ADR-0011'e nasıl ulaşacağını dosya adı söylemiyor. İkincisi, shipping kararı listenin en sonunda, başka bir adlandırmayla duruyor ve ls'in sıralamasında kaybolmuş; üstelik kimse 12 numaranın neden 0012 olmadığını bilmiyor. Üçüncüsü, hiçbir dosya adı kararın statüsünü, konusunu ya da ne zaman gözden geçirileceğini söylemiyor.

Decision log'un değeri, içindeki kayıtların bulunabilirliğiyle sınırlı. Bunun için üç şey gerekiyor: her kaydın statüsünü ve konusunu gösteren bir index, supersede zincirinin kopmaması ve kayıtların biçiminin tutarlı olması. Üçünü de insan dikkatine bırakmak yerine küçük bir tool'a verdik. MADR (Markdown Architectural Decision Records) front matter'ına iki alan ekledik: tags (konu) ve review-after (bir sonraki gözden geçirme tarihi). MADR'ın front matter'ı isteğe bağlı alanlardan oluşuyor ve ekibin kendi alanlarını eklemesine engel yok:

---
status: accepted
date: 2026-03-12
tags: [structure]
review-after: 2026-09-12
---

Tool, klasördeki her ADR'ı okuyor, kuralları kontrol ediyor, gözden geçirilmesi gerekenleri listeliyor ve hata yoksa index'i, yani README.md'yi yeniden yazıyor:

using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Bookstore.DecisionLog;

string folder = args[0];
DateOnly today = DateOnly.Parse(args.ElementAtOrDefault(1) ?? DateTime.UtcNow.ToString("yyyy-MM-dd"), CultureInfo.InvariantCulture);

Adr[] log = [.. Directory.GetFiles(folder, "*.md").Where(f => Path.GetFileName(f) != "README.md").Order().Select(Adr.Load)];
List<string> errors = [.. DecisionLogRules.Check(log)];
List<string> reviews = [.. DecisionLogRules.DueForReview(log, today)];

foreach (string e in errors) Console.WriteLine($"error: {e}");
foreach (string r in reviews) Console.WriteLine($"review: {r}");

if (errors.Count == 0)
{
    File.WriteAllText(Path.Combine(folder, "README.md"), DecisionLogRules.Index(log));
    Console.WriteLine($"index written: {log.Length} decisions");
}
return errors.Count == 0 ? 0 : 1;

namespace Bookstore.DecisionLog
{
    public sealed record Adr(string File, int Number, string Title, string Status, DateOnly Date, string[] Tags, DateOnly? ReviewAfter)
    {
        public static Adr Load(string path)
        {
            string[] lines = System.IO.File.ReadAllLines(path);
            Dictionary<string, string> meta = lines.Skip(1).TakeWhile(l => l != "---")
                .Select(l => l.Split(':', 2))
                .ToDictionary(p => p[0].Trim(), p => p[1].Trim().Trim('"'));
            string file = Path.GetFileName(path);
            int number = int.TryParse(file.Split('-')[0], out int n) ? n : 0;
            string title = lines.First(l => l.StartsWith("# ")).Substring(2);
            string[] tags = meta.TryGetValue("tags", out string? t) ? t.Trim('[', ']').Split(", ") : [];
            DateOnly? review = meta.TryGetValue("review-after", out string? r) ? DateOnly.Parse(r, CultureInfo.InvariantCulture) : null;
            return new Adr(file, number, title, meta["status"], DateOnly.Parse(meta["date"], CultureInfo.InvariantCulture), tags, review);
        }

        public string Id => $"ADR-{Number:D4}";
    }

    public static partial class DecisionLogRules
    {
        private static readonly string[] Statuses = ["proposed", "accepted", "rejected", "deprecated"];

        [GeneratedRegex(@"^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$")]
        private static partial Regex FileName();

        [GeneratedRegex(@"^superseded by (ADR-\d{4})$")]
        private static partial Regex SupersededBy();

        public static IEnumerable<string> Check(Adr[] log)
        {
            Dictionary<string, Adr> byId = log.Where(a => a.Number > 0).ToDictionary(a => a.Id);
            foreach (Adr adr in log)
            {
                if (!FileName().IsMatch(adr.File))
                    yield return $"{adr.File}: file name must look like NNNN-title-with-dashes.md";

                Match superseded = SupersededBy().Match(adr.Status);
                if (superseded.Success)
                {
                    // Zincir kopmasın: yeni kayıt var olmalı ve kabul edilmiş olmalı
                    string next = superseded.Groups[1].Value;
                    if (!byId.TryGetValue(next, out Adr? target))
                        yield return $"{adr.File}: superseded by {next}, which does not exist";
                    else if (target.Status != "accepted")
                        yield return $"{adr.File}: superseded by {next}, which is '{target.Status}'";
                }
                else if (!Statuses.Contains(adr.Status))
                    yield return $"{adr.File}: unknown status '{adr.Status}'";
            }
        }

        public static IEnumerable<string> DueForReview(Adr[] log, DateOnly today)
        {
            foreach (Adr adr in log)
            {
                if (adr.Status == "accepted" && adr.ReviewAfter is DateOnly due && due <= today)
                    yield return $"{adr.Id} '{adr.Title}' was due for review on {due:yyyy-MM-dd}";
                if (adr.Status == "proposed" && today.DayNumber - adr.Date.DayNumber > 30)
                    yield return $"{adr.Id} '{adr.Title}' has been proposed for {today.DayNumber - adr.Date.DayNumber} days";
            }
        }

        public static string Index(Adr[] log)
        {
            StringBuilder md = new();
            md.AppendLine("# Decision log").AppendLine();
            md.AppendLine("| ADR | Title | Status | Date | Tags |");
            md.AppendLine("|-----|-------|--------|------|------|");
            foreach (Adr a in log.OrderBy(a => a.Number))
                md.AppendLine($"| [{a.Id}]({a.File}) | {a.Title} | {a.Status} | {a.Date:yyyy-MM-dd} | {string.Join(", ", a.Tags)} |");
            return md.ToString();
        }
    }
}

Açılıştaki klasörde çalıştırınca:

$ dotnet run -- ../../docs/decisions 2026-11-08
error: 12-Shipping-Labels-As-PDF.md: file name must look like NNNN-title-with-dashes.md
error: 12-Shipping-Labels-As-PDF.md: superseded by ADR-0021, which does not exist
review: ADR-0003 'Start with a modular monolith' was due for review on 2026-09-12
review: ADR-0009 'Payment retries with a resilience pipeline' has been proposed for 68 days
exit code: 1

İki hata, açılıştaki iki sorunun mekanik karşılığı: dosya adı kalıba uymuyor ve kayıt var olmayan bir ADR-0021 tarafından supersede edilmiş görünüyor. Kayıt, başka bir PR'dan (pull request) kalma bir taslak status satırıyla merge edilmiş. Dosyayı 0012-shipping-labels-as-pdf.md olarak düzeltip status'u geri aldıktan sonra:

$ dotnet run -- ../../docs/decisions 2026-11-08
review: ADR-0003 'Start with a modular monolith' was due for review on 2026-09-12
review: ADR-0009 'Payment retries with a resilience pipeline' has been proposed for 68 days
index written: 15 decisions
exit code: 0
# Decision log

| ADR | Title | Status | Date | Tags |
|-----|-------|--------|------|------|
| [ADR-0001](0001-record-architecture-decisions.md) | Record architecture decisions | accepted | 2026-03-02 | process |
| [ADR-0002](0002-sql-server-as-the-single-database.md) | SQL Server as the single database | accepted | 2026-03-05 | data |
| [ADR-0003](0003-start-with-a-modular-monolith.md) | Start with a modular monolith | accepted | 2026-03-12 | structure |
| [ADR-0004](0004-order-email-through-an-in-process-queue.md) | Order e-mail through an in-process queue | superseded by ADR-0011 | 2026-03-19 | messaging, ordering |
| [ADR-0005](0005-public-order-number-format.md) | Public order number format | accepted | 2026-03-26 | ordering, api |

Barbara artık index'te ADR-0004'ün superseded by ADR-0011 olduğunu görüyor ve link'ten yeni kayda geçiyor; "messaging" etiketi ikisini birlikte buluyor. Tool, ikinci yazıdaki immutability script'iyle birlikte CI'da (Continuous Integration) docs/decisions'a dokunan her PR'da çalışıyor; exit code 1 ile PR kırmızıya düşüyor. Index'in üretilen bir dosya olması da önemli: elle güncellenen bir index, decision log'un kendisi gibi bayatlar.

"Bu Karar Hâlâ Geçerli mi?" Tetikleyicileri

Tool'un iki "review" satırı, decision log'un en çok ihmal edilen işini görünür yapıyor: kararları yeniden ele almak. Bir ADR verildiği gün doğruydu; bağlam değişti mi? Bunu sormanın beş tetikleyicisi var ve Bookstore'da hepsinin yeri belli.

Takvim. review-after alanı, karar verilirken "bunu altı ay sonra tekrar düşünelim" demenin yazılı hali. ADR-0003'ün tarihi Eylül'de geçti; modular monolith kararı, dört ekibe bölünme planı netleşmişken gözden geçirilmeli. Her karara tarih koymak gerekmiyor; yalnız düşük güvenle verilenlere ve hızla değişen bağlamdakilere.

Koşul. İkinci yazıdan beri her ADR'ın More Information bölümünde bir yeniden açılma koşulu var: ADR-0008 için "4'ten fazla instance", ADR-0004 için "ayda 10'dan fazla kayıp e-posta". Bu koşulları kimse takvimden takip etmez; bir metrik ya da bir gelişme tetikler. Altıncı yazıdaki fitness function'lar burada devreye giriyor: bir kontrol kırmızıya döndüğünde ilk soru "kodu mu düzeltelim, kararı mı yeniden açalım?" olmalı.

Bekleyen kayıtlar. ADR-0009 altmış sekiz gündür proposed. Uzun süre bekleyen bir öneri, ya kimsenin karar vermediği bir risktir (üçüncü yazıdaki "omission" türünden) ya da artık kimsenin umursamadığı bir konu. İkisinde de cevap aynı: karar ver, reddet ya da kaydı geri çek.

Incident. Bir incident'in kök neden analizinde "bunu biliyorduk" cümlesi geçiyorsa, kabul edilen bir bedel gerçekleşmiş demektir. Analiz, ilgili ADR'ın numarasını yazmalı ve kararın yeniden ele alınıp alınmayacağını söylemeli.

Yeni kişi. Barbara'nın "bu neden böyle?" sorusu da bir tetikleyici. Cevap bir ADR'da yoksa, eksik bir kayıt bulduk demektir; cevap bir ADR'da varsa ama Barbara ikna olmadıysa, kayıt zayıf ya da bağlam değişmiş olabilir. Yeni gelenlerin soruları, decision log'un ücretsiz bir review'u.

Hafif Architecture Review

Tetikleyicilerin bir toplanma yeri olmalı, yoksa her biri ayrı bir Slack mesajında kaybolur. Bookstore'da bu yer ayda bir yapılan, 30 dakikalık bir toplantı. Gündem, decision log'un kendisinde bir dosya olarak duruyor:

# Architecture review, first Tuesday of the month, 30 minutes

1. Decision log since last review (5 min): new, superseded, rejected ADRs
2. Due reviews (10 min): output of the decision log check; for each one
   "still valid / write a superseding ADR / move review-after"
3. Proposed ADRs older than 30 days (10 min): decide, reject or drop
4. Context and Container views (5 min): "is this still true?"

Output: one commit to docs/decisions with the outcomes; nothing else.

Toplantının çıktısı bir commit: status değişiklikleri, yeni review-after tarihleri, supersede eden yeni ADR'ların taslakları. Toplantıda tasarım yapılmıyor; bir kararın yeniden tartışılması gerekiyorsa, birine üçüncü yazıdaki gibi bir spike ya da dördüncü yazıdaki gibi bir design doc görevi veriliyor. Beşinci yazıdaki Context ve Container view'larına bakılan beş dakika, bayatlık kontrolünün yakalayamadığı şeyi, yani modelin gerçeği eksik anlatıp anlatmadığını soruyor.

Altı kişilik bir ekip için ayda 30 dakika, yılda altı saat eder. Karşılığında ekip, hangi kararların hâlâ geçerli olduğunu bilen bir decision log'a sahip oluyor. Toplantı iki kez üst üste "konuşacak bir şey yok" diye biterse, süresini kısalt; decision log hızla büyüyorsa iki haftada bire çek.

Advice Process ve Architecture Advisory Forum

Ekip büyüdükçe "kararı kim verir?" sorusu yeniden gelir. İlk yazıda Andrew Harmel-Law'un martinfowler.com'da Aralık 2021'de yayımladığı "Scaling the Practice of Architecture, Conversationally" yazısındaki advice process'e değinmiştik. Tek kuralı: herkes mimari bir karar verebilir, ama önce karardan anlamlı biçimde etkilenen herkese ve konunun uzmanlarına danışmak zorundadır. Karar veren tavsiyeye uymak zorunda değil; tavsiyeyi istemek ve kaydetmek zorunda. Amaç uzlaşma değil, geniş bir girdi.

Harmel-Law bu kuralı dört destekleyici unsurla birlikte öneriyor. Birincisi decision record'lar; tavsiyelerin de içine yazıldığı ADR'lar. İkincisi architecture advisory forum: haftada bir saatlik, açık bir toplantı; ekipler önerilen kararları, yeni spike'ları ve yeniden ele alınan kararları burada anlatıyor. Forum karar vermiyor; katılanlar yalnız tavsiye verebilir ya da danışılması gereken başka birini önerebilir. Üçüncüsü ekiplerin kendi yazdığı, test edilebilir mimari prensipler (yazıda 8–15 tane öneriliyor). Dördüncüsü ekibin kendi tech radar'ı. Fowler'ın Mart 2026 tarihli ADR yazısı da aynı bağı kuruyor: ADR taslağı yazılırken toplanan tavsiyenin kısa bir özeti ADR'a girmeli.

Bookstore'un aylık review'u, küçük bir ekip için architecture advisory forum'un hafif bir hali. Advice process'i somut yapan şey ise MADR'ın front matter'ı: consulted alanı boş bir ADR, PR review'da "kime danıştın?" sorusunu doğuruyor. İlk yazıdaki üç ölçüt de kime danışılacağını söylüyor: sistem sınırını geçen bir kararda dış ekipler ve ürün, tek yönlü bir kapıda ops ve kararın uzun vadeli sahipleri. Ekip dört stream-aligned ekibe bölündüğünde forum haftalık olacak ve bir ekibin kararının öteki ekipleri nasıl etkilediği orada konuşulacak.

Tooling'e Bakış

ADR için yazılmış araçlar var ve hepsinin bakım durumunu yazım anında GitHub ve NuGet üzerinden kontrol ettik. Nat Pryce'ın adr-tools'u Nygard formatının referans aracı; son sürümü 3.0.0 (Temmuz 2018), son commit'i Mart 2020. log4brains, ADR'ları statik bir web sitesine çeviren bir araç; son sürümü v1.1.0 (Aralık 2024) ve o tarihten beri commit yok. .NET global tool olarak dağıtılan dotnet-adr'nin NuGet'teki son sürümü 1.1.8 (Mart 2024); repo'daki son commit Temmuz 2025 tarihli bir bağımlılık güncellemesi. MADR'ın kendisi 4.0.0'da (Eylül 2024) duruyor, ama repo'su aktif. Olaf Zimmermann ve ekibi Haziran 2026'da MADR'ın YAML (YAML Ain't Markup Language) karşılığı olan YADR'ı (YAML ADR Template) tanıttı; makinelerin de okuyabildiği bir format arayanlar için yeni bir seçenek.

Bu tablo bir şey söylüyor: ADR araçları küçük ve çoğu bakımsız, ama buna ihtiyaç da az. Bir ADR bir Markdown dosyası; yeni kayıt açmak bir şablonu kopyalamak. Asıl değer üretilen index'te, supersede zincirinin kontrolünde ve immutability kuralında. Bunların hepsi bu serideki iki küçük script ve bir console uygulamasıyla, ekibin kendi repo'sunda, kimsenin bakımına bağımlı olmadan çalışıyor. Bir araç seçeceksen ölçüt, araç kaybolduğunda geriye düz Markdown kalıp kalmadığı olmalı; adr-tools, MADR ve beşinci yazıdaki Structurizr !adrs importer'ı bu açıdan güvenli.

Küçük Ekip İçin Minimum Süreç

Serinin yedi yazısını Bookstore'un altı kişilik ekibi için tek bir listeye indirgersek:

  • Bir karar geri alması pahalıysa, birden fazla module'ü ya da sistem sınırını etkiliyorsa veya öncelikli bir senaryoyu oynatıyorsa ADR yazılır; değilse PR açıklaması yeter (1. yazı).
  • ADR, MADR 4.0 şablonuyla, proposed statüsünde bir PR olarak açılır; en az iki gerçek seçenek, bedel ve yeniden açılma koşulu içerir (2. yazı).
  • Seçenekler tartışmalıysa, öncelikli senaryolara karşı ve gerekirse bir spike ile tartılır; sensitivity point, trade-off point ve risk'ler ADR'a girer (3. yazı).
  • Çözüm belirsiz ve birden fazla ekip etkileniyorsa önce bir mini design doc yazılır; karar sonra ADR olur (4. yazı).
  • ADR'lar, arc42 bölümleri ve C4 (Context, Container, Component, Code) modeli aynı repo'da durur; diyagramlar modelden üretilir ve CI'da bayatlık kontrolünden geçer (5. yazı).
  • Her ADR'ın Confirmation'ı ya bir otomatik kontroldür ya da kısa bir PR checklist'inin maddesi (6. yazı).
  • Decision log index'i üretilir, supersede zinciri ve immutability CI'da kontrol edilir, ayda bir 30 dakikalık review yapılır ve her kararda danışılanlar consulted alanına yazılır (bu yazı).

Listenin hiçbir maddesi özel bir araç ya da ayrı bir rol gerektirmiyor. Gerektirdiği şey, kararların koddan ayrı bir şey olmadığını kabul etmek: aynı repo'da yaşıyor, aynı PR'larda review ediliyor ve aynı CI'da kontrol ediliyorlar.

Sık Düşülen Tuzaklar

Yazıp unutmak. Decision log'a yalnız ekleme yapılıp hiç bakılmıyorsa, bir süre sonra okunmayan bir arşive dönüşür. Takvimli ve koşullu tetikleyiciler olmadan kayıtlar sessizce eskir.

Index'i elle tutmak. Elle güncellenen bir README, ilk yoğun haftada geride kalır. Index'i üret ve üretimi CI'da kontrol et.

Supersede zincirini kırmak. Var olmayan ya da kabul edilmemiş bir kayda yönlendiren superseded by, okuru hiçbir yere götürmez. Zinciri tool'a kontrol ettir.

Advice process'i konsensüse çevirmek. Herkesin onayını beklemek, kararı en yavaş ve en itirazcı kişiye bağlar. Kural danışmak ve kaydetmek; karar, veren kişide kalır.

Review toplantısını tasarım toplantısına çevirmek. Otuz dakikada bir kararı yeniden tasarlayamazsın. Toplantı karar verir: geçerli, yeniden açılacak (sahibi ve tarihiyle) ya da kapatılacak.

Araca bağımlı olmak. Bakımı biten bir aracın formatına kilitlenmiş bir decision log, aracın kaderini paylaşır. Düz Markdown ve repo'daki küçük script'ler en uzun yaşayan çözüm.

Küçük Bir Deneme

★☆☆ Kendi decision log'unda (yoksa ilk yazıdaki on kararla) tool'u çalıştır. Kaç hata, kaç review satırı çıktı? Hiç review-after alanı olmayan kararlardan hangisine tarih koyardın?

★★☆ Tool'a iki kural ekle: tags alanı boş olan kayıt bir uyarı versin ve accepted bir ADR'ın consulted alanı yoksa hata versin. Sonra index'e her etiket için ayrı bir bölüm ekle.

★★★ Ekibinle ilk 30 dakikalık architecture review'u yap. Tool'un çıktısıyla başla, gündemdeki dört maddeyi sırayla geç ve toplantının çıktısını tek bir commit olarak at. Bir ay sonra ikinci toplantıda şunu sor: ilk toplantıda yeniden açılmasına karar verilen kayıtlardan kaçı gerçekten yeniden ele alındı?

Seri burada bitiyor. Yedi yazı boyunca bir kararı tanımayı, kaydetmeyi, tartmayı, yazıyla tartışmayı, belgelemeyi, otomatik olarak korumayı ve yaşatmayı konuştuk. Bookstore'un decision log'undaki kararların çoğu (modular monolith, outbox, cache) kategorinin diğer serilerinde ayrıntısıyla işleniyor; bir sonraki durak, ADR-0003'ün kendisini koda döken ilerideki Modular Monolith serisi.