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

Architecture Decision Nedir? Architecturally Significant Kararları Tanımak

Mert Özen 8 Kas 2026 11 dk 3 görüntülenme
Architecture Decision Nedir? Architecturally Significant Kararları Tanımak

Bir karar geri alması pahalıysa, birden fazla module'ü ya da sistem sınırını etkiliyorsa veya öncelikli bir quality attribute senaryosunu oynatıyorsa mimaridir ve kaydı hak eder. Bookstore'un ilk aylarındaki on kararı bu üç ölçütle C#'ta sınıflandırıyor; hangisinin ADR, hangisinin design doc, hangisinin PR açıklaması hak ettiğini, kararın sahibini ve kayıtsız kararların bedelini konuşuyoruz.

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

Önce Tahmin Et

Bookstore ekibinin ilk aylarında masaya gelen on karar aşağıda. Hepsi gerçek bir toplantıda, bir PR'da (pull request) ya da bir Slack thread'inde konuşulmuş; bazıları beş dakika sürmüş, bazıları iki hafta. Kararları bir C# record'u olarak yazdık: her birinin geri alınma maliyeti, değişirse kimleri etkilediği ve Mimariye Giriş serisinde seçtiğimiz ilk üç characteristic'ten (availability, performance, modifiability) hangisini oynattığı.

using Bookstore.Decisions;

Decision[] firstQuarter =
[
    new("D01", "Start as a modular monolith, one deployment", Reversibility.OneWay, Reach.ManyModules, ["modifiability", "availability"]),
    new("D02", "SQL Server as the single database", Reversibility.Costly, Reach.ManyModules, ["performance"]),
    new("D03", "Angular SPA in front of an ASP.NET Core API", Reversibility.Costly, Reach.OutsideSystem, ["modifiability"]),
    new("D04", "Order e-mail through an in-process queue", Reversibility.Cheap, Reach.OneModule, ["availability", "performance"]),
    new("D05", "Public order number format BK-2026-000123", Reversibility.OneWay, Reach.OutsideSystem, []),
    new("D06", "Mapperly instead of hand-written mapping in Catalog", Reversibility.Cheap, Reach.OneModule, []),
    new("D07", "Outbox for events between Ordering, Inventory, Payment", Reversibility.Costly, Reach.ManyModules, ["availability"], OptionsAreOpen: true),
    new("D08", "Redis cache in front of the catalog", Reversibility.Costly, Reach.OneModule, ["performance"], OptionsAreOpen: true),
    new("D09", "Rename OrderService.Place to PlaceOrderAsync", Reversibility.Cheap, Reach.OneClass, []),
    new("D10", "AsNoTracking in the book details query", Reversibility.Cheap, Reach.OneClass, ["performance"]),
];

Bu on karardan kaçı mimari bir karar? Hangisi yazılı bir kaydı hak ediyor, hangisi PR açıklamasında birkaç cümleyle geçilebilir? Bir de şunu düşün: D04 (sipariş e-postasını process içi bir queue'ya bırakmak) Mimariye Giriş serisinin "Trade-off Düşüncesi" yazısında iki yönlü bir kapı olarak anlatılmıştı, yani kolayca geri alınabiliyor. Kayda değer mi?

Architecture Decision ve ASR: Tanımlar

adr.github.io, bu alandaki terimleri toplayan ve MADR (Markdown Architectural Decision Records) şablonunu da barındıran topluluk sitesi. Oradaki tanıma göre bir AD (Architecture Decision), mimari açıdan önemli bir fonksiyonel ya da fonksiyonel olmayan requirement'ı karşılayan, gerekçelendirilmiş bir tasarım seçimidir. Bu "mimari açıdan önemli" requirement'ın da bir adı var: ASR (Architecturally Significant Requirement), sistemin mimarisi ve kalitesi üzerinde ölçülebilir etkisi olan requirement. Bir AD'yi gerekçesi, trade-off'ları ve sonuçlarıyla kayda geçiren dokümana ADR (Architecture Decision Record), bir projedeki ADR'ların toplamına da decision log denir.

Tanımın iki ucu birbirine bağlı. Mimariye Giriş serisinin üçüncü yazısındaki "kampanya zirvesinde katalog araması p95 < 300 ms" senaryosu bir ASR: ölçülebilir ve mimariyi etkiliyor, çünkü onu tutturmak için cache, read model ya da arama motoru gibi yapısal seçenekler gündeme geliyor. Bu seçeneklerden birini seçmek bir AD. Seçimi, reddedilen seçeneklerle birlikte yazmak da bir ADR.

Peki hangi karar "mimari"? Michael Nygard, ADR fikrini ortaya koyduğu 2011 tarihli "Documenting Architecture Decisions" yazısında bunu beş alanla sınırlar: yapıyı, fonksiyonel olmayan karakteristikleri, dependency'leri, interface'leri ya da yapım tekniklerini etkileyen kararlar. Microsoft'un Azure Well-Architected Framework dokümanı ise daha kısa bir süzgeç önerir: sistemin yapısını ya da önemli quality attribute'larını etkileyen veya geri alınması zor olan seçimler. Mimariye Giriş serisinin ilk yazısında gördüğümüz tanımla da örtüşüyor: mimari, değiştirmesi zor algılanan şeylerdir.

Üç Ölçüt: Geri Alma Maliyeti, Etki Alanı, Quality Attribute

Bu tanımları üç soruya indirgedik ve programın geri kalanında koda döktük:

foreach (Decision d in firstQuarter)
{
    Triage t = DecisionTriage.Classify(d);
    string why = t.Reasons.Count == 0 ? "-" : string.Join("; ", t.Reasons);
    Console.WriteLine($"{d.Id} {t.Record,-12} {why}");
}

Console.WriteLine();
foreach (IGrouping<RecordKind, Decision> g in firstQuarter.GroupBy(d => DecisionTriage.Classify(d).Record))
    Console.WriteLine($"{g.Key,-12} {g.Count()}: {string.Join(", ", g.Select(d => d.Id))}");

namespace Bookstore.Decisions
{
    public enum Reversibility { Cheap, Costly, OneWay }

    // Karar değişirse kimin değişmesi gerekir? Küçükten büyüğe sıralı
    public enum Reach { OneClass, OneModule, ManyModules, OutsideSystem }

    public enum RecordKind { PullRequest, Adr, DesignDoc }

    public sealed record Decision(
        string Id,
        string Title,
        Reversibility Reversibility,
        Reach Reach,
        string[] AffectedQualities,
        bool OptionsAreOpen = false);

    public sealed record Triage(RecordKind Record, IReadOnlyList<string> Reasons);

    public static class DecisionTriage
    {
        // Mimariye Giriş 02-03'te seçilen ilk üç characteristic
        private static readonly string[] Prioritized = ["availability", "performance", "modifiability"];

        public static Triage Classify(Decision d)
        {
            List<string> reasons = [];

            if (d.Reversibility == Reversibility.OneWay)
                reasons.Add("one-way door");
            else if (d.Reversibility == Reversibility.Costly)
                reasons.Add("costly to reverse");
            if (d.Reach == Reach.OutsideSystem)
                reasons.Add("crosses the system boundary");
            else if (d.Reach == Reach.ManyModules)
                reasons.Add("spans modules");

            // Tek class'ın içinde kalan bir iyileştirme mimari değildir
            string[] qualities = [.. d.AffectedQualities.Intersect(Prioritized)];
            if (qualities.Length > 0 && d.Reach > Reach.OneClass)
                reasons.Add($"moves {string.Join("+", qualities)}");

            RecordKind record = reasons.Count switch
            {
                0 => RecordKind.PullRequest,
                _ when d.OptionsAreOpen && d.Reach >= Reach.ManyModules => RecordKind.DesignDoc,
                _ => RecordKind.Adr,
            };
            return new Triage(record, reasons);
        }
    }
}
D01 Adr          one-way door; spans modules; moves modifiability+availability
D02 Adr          costly to reverse; spans modules; moves performance
D03 Adr          costly to reverse; crosses the system boundary; moves modifiability
D04 Adr          moves availability+performance
D05 Adr          one-way door; crosses the system boundary
D06 PullRequest  -
D07 DesignDoc    costly to reverse; spans modules; moves availability
D08 Adr          costly to reverse; moves performance
D09 PullRequest  -
D10 PullRequest  -

Adr          6: D01, D02, D03, D04, D05, D08
PullRequest  3: D06, D09, D10
DesignDoc    1: D07

Birinci ölçüt geri alma maliyeti. Mimariye Giriş serisinin "Trade-off Düşüncesi" yazısında Bezos'un one-way/two-way door ayrımını görmüştük; burada onu yalnızca bir sınıflandırma girdisi olarak kullanıyoruz. D05, yani müşteriye e-postada ve kargo etiketinde gösterilen sipariş numarasının formatı, tek yönlü bir kapı: bir kez müşterinin elinde, muhasebenin raporunda ve kargo firmasının sisteminde göründükten sonra değiştirmek bir migration değil, bir geçiş projesi olur. D01'deki "tek deployment" kararı da öyle; geri dönmek mümkün ama aylar sürer.

İkinci ölçüt etki alanı: karar değişirse kimin değişmesi gerekir? Reach enum'ı bunu tek class'tan sistem sınırının dışına kadar sıralıyor. D03 (Angular SPA (Single-Page Application) ve API (Application Programming Interface) ayrımı) sistem sınırını geçiyor, çünkü mobil uygulama ve ikinci ülkenin arayüzü aynı API'yi kullanacak. D02'deki veritabanı seçimi bütün module'lerin persistence kodunu bağlıyor.

Üçüncü ölçüt quality attribute etkisi: karar, öncelikli senaryolardan birinin tutup tutmayacağını değiştiriyor mu? D08'deki Redis cache doğrudan arama latency'sini ve availability'yi oynatıyor. Bu ölçütte bir koşul var ve sınırdaki kararları ayıran da o.

Sınırdaki Kararlar

D04 ile D10'u yan yana koy. İkisi de geri alması ucuz, ikisi de performance'ı etkiliyor. Program D04'ü ADR'a, D10'u PR açıklamasına gönderiyor. Fark etki alanında: AsNoTracking tek bir sorgunun içinde kalan, kimsenin bilmesine gerek olmayan bir iyileştirme. Yarın biri kaldırırsa yalnız o sorgu yavaşlar ve EF (Entity Framework) Core serisindeki performans yazısını okuyan herkes nedenini kodun kendisinden anlar. D04 ise bir module'ün davranışını değiştiriyor: sipariş verildikten sonra e-postanın gidip gitmediği artık bir ara durum, deploy sırasında e-posta kaybolabiliyor ve müşteri hizmetleri bunu bilmek zorunda. Geri alması kolay, ama kabul edilen bedeli görünmez. ADR'ın asıl işi de bu: bedeli görünür kılmak.

Koddaki d.Reach > Reach.OneClass koşulu bu ayrımı yapıyor. Onu çıkarırsan D10 da ADR'a düşer ve ekip ilk haftada "her sorgu optimizasyonu için ADR mı yazacağız?" diye sürece isyan eder. Ölçütü fazla gevşek tutarsan bu kez D04 gibi kararlar PR açıklamasında kaybolur. Bu programı bir karar makinesi olarak değil, bir konuşma aracı olarak düşün: yeni bir karar geldiğinde üç soruyu sorduruyor ve cevaplardan biri "evet" ise kayıt açılmasını öneriyor. Son sözü yine ekip söylüyor.

Bir de zamanla yer değiştiren kararlar var. D06'daki Mapperly seçimi bugün tek module'de ve PR açıklaması yeterli. Üç module aynı mapping library'sini kullanmaya başladığında ve library'nin lisans ya da bakım durumu bir risk olduğunda, karar module'ler arası bir konuya dönüşür. Mimariye Giriş serisinin ilk yazısında anlatılan architecture ile design arasındaki spectrum tam olarak bu: kararın yeri sabit değil, etkisi büyüdükçe mimariye doğru kayıyor.

ADR mı, Design Doc mu, PR Açıklaması mı?

Programın üç çıktısı var, ama gerçek hayatta kayıt türleri biraz daha zengin. Hangisinin ne zaman yazılacağına dair kendi kuralımız:

KayıtNe zamanUzunlukBookstore örneği
PR açıklamasıKarar tek module'ün içinde, ucuz geri alınıyor, öncelikli senaryoları oynatmıyorBirkaç cümleD06, D09, D10
ADRÜç ölçütten en az biri evet; seçenekler belli, tartışma kısaBir-iki sayfaD01, D02, D03, D04, D05, D08
Design doc, ardından ADRMimari karar ve çözüm henüz belirsiz; birden fazla module ya da ekip etkileniyor2–10 sayfaD07 (outbox)
RFC (Request for Comments)Etkilenen kitle ekibin dışında; yazılı yorum süreci ve tarihli bir karar gerekiyorDesign doc kadarBugün yok; ekip sayısı arttığında

D07 neden doğrudan ADR değil? Çünkü "outbox mı, broker'ın transaction desteği mi, CDC (Change Data Capture) mı?" sorusunun cevabı henüz belli değil ve Ordering, Inventory ve Payment'ın aynı tasarım üzerinde anlaşması gerekiyor. ADR bir karar kaydıdır; kararı vermek için gereken tartışma başka bir yerde yapılır. Bu yer design doc ve dördüncü yazının konusu. Design doc biter, karar verilir, sonuç kısa bir ADR olarak decision log'a girer ve design doc'a link verir.

.NET'in kendisi de bu ayrımı yapıyor. github.com/dotnet/designs repo'su runtime, framework ve SDK (Software Development Kit) için design proposal'ların toplandığı yer: her proposal bir pull request olarak açılıyor, accepted/2026/ gibi yıllara göre klasörlenmiş Markdown dosyalarında duruyor ve şablonunda Goals ile Non-Goals başlıkları var. C# dilinin kararları ise ayrı bir repo'da, dotnet/csharplang'da, LDM (Language Design Meeting) notları olarak tutuluyor. Küçük bir bug fix'in gerekçesi ise PR açıklamasında kalıyor. Aynı organizasyon, kararın etki alanına göre üç farklı kayıt biçimi kullanıyor.

Kararın Sahibi Kim?

Bir kararın mimari olduğunu söylemek, onu kimin vereceğini söylemez. Üç yaygın model var. Birincisinde architect karar verir; tutarlılık yüksek, ama architect darboğaz olur ve ekip kararları sahiplenmez. İkincisinde ekip birlikte karar verir; sahiplenme yüksek, ama her şey toplantıya döner ve ekip büyüdükçe konsensüs imkânsızlaşır. Üçüncüsü Andrew Harmel-Law'un martinfowler.com'da 2021'de yazdığı advice process: herkes mimari bir karar verebilir, yeter ki karardan etkilenenlere ve konunun uzmanlarına danışmış olsun. Danışmak, tavsiyeye uymak zorunda olmak demek değil; ama tavsiyeyi istemek ve kayda geçirmek zorunlu.

Bookstore'un bugünkü haliyle (3 developer, yarı zamanlı 1 ops) bu modeller arasındaki fark küçük; herkes her toplantıda. Ama sınıflandırma kime danışılacağını da söylüyor. D05'teki sipariş numarası müşteri hizmetlerini ve muhasebeyi etkiliyor, onlara sorulmadan verilmemeli. D01'deki tek deployment kararı ops'un nöbet yükünü belirliyor, Linus'un görüşü kayıtta olmalı. Bir kararın etki alanı ne kadar genişse, "consulted" listesi o kadar uzun. Advice process'i ve architecture advisory forum'u bu serinin son yazısında ekip ritmine çevireceğiz.

Kayıtsız Kararların Bedeli

Sekiz ay sonra ekibe katılan bir developer, bir müşteri şikâyetini inceliyor: "Siparişimin e-postası gelmedi." Kodda bir Channel<OrderConfirmation> ve arka planda çalışan bir worker görüyor, deploy sırasında queue'da kalan mesajların kaybolduğunu fark ediyor. Mantıklı görünen düzeltmeyi yapıyor: e-postayı yeniden checkout request'inin içinde senkron gönderiyor. Bir sonraki kampanyada e-posta sağlayıcısı yavaşlayınca checkout da yavaşlıyor, müşteriler tekrar deniyor ve çift sipariş şikâyetleri başlıyor. Ekip, sekiz ay önce bilerek kaçtığı sorunu yeniden üretmiş oluyor.

Nygard'ın yazısındaki teşhis tam bu: gerekçesini bilmediğin bir kararla karşılaşınca iki seçeneğin kalır. Ya körü körüne kabul edersin ve koşullar değişmiş olsa bile kararı sürdürürsün, ya da körü körüne geri alırsın ve kararın koruduğu, ama hiçbir testin yakalamadığı bir özelliği bozarsın. git blame sana kimin ve ne zaman yazdığını söyler, commit mesajı belki "add email queue" der; ama hangi seçeneklerin konuşulduğunu, hangi bedelin bilerek kabul edildiğini ve kararın hangi koşulda yeniden açılması gerektiğini söylemez.

Kayıtsız kararların bir bedeli daha var: aynı tartışmanın tekrar tekrar yapılması. "PostgreSQL'e geçsek mi?" sorusu her yeni gelen developer'la yeniden açılır, çünkü geçen sefer neden geçilmediği kimsenin aklında değildir. Bir sonraki yazıda bu tartışmayı reddedilmiş bir ADR olarak kayda geçireceğiz; reddedilen kararı yazmak da en az kabul edileni yazmak kadar değerli.

Sık Düşülen Tuzaklar

Her kararı mimari saymak. Her sorgu optimizasyonu, her method adı için kayıt istenirse ekip süreci atlamanın yolunu bulur ve önemli kararlar da kaydedilmez. D10 PR açıklamasında kalmalı.

Yalnız büyük ve pahalı kararları kaydetmek. D04 gibi geri alması kolay ama bedeli görünmez kararlar en çok "körü körüne geri alma"ya kurban gidenlerdir. Quality attribute ölçütü bunun için var.

Kararı teknoloji adıyla sınırlamak. "Redis kullanıyoruz" bir karar özeti değil; asıl karar "katalog aramasında veritabanı yükünü ve latency'yi cache ile düşürüyoruz, tutarlılık bedelini şu şekilde kabul ediyoruz" cümlesi. Teknoloji, kararın bir sonucu.

Kararın sahibini ve danışılanları yazmamak. "Biz karar verdik" cümlesi altı ay sonra kimseye bir şey söylemez. Kim karar verdi, kime danışıldı, kim bilgilendirildi; bir sonraki yazıdaki MADR şablonu bunları ayrı alanlar olarak istiyor.

Ölçütleri bir kez yazıp unutmak. Öncelikli characteristic'ler değişirse (ikinci ülke, mobil uygulama) hangi kararın mimari olduğu da değişir. Prioritized dizisi sabit değil; Mimariye Giriş'teki utility tree güncellendikçe güncellenmeli.

Küçük Bir Deneme

★☆☆ Kendi projenden son bir ayda verilen beş kararı Decision record'u olarak yaz ve programı çalıştır. Çıkan sınıflandırmaya katılıyor musun? Katılmadığın bir sonuç için hangi ölçüt eksik ya da fazla?

★★☆ Decision'a Consulted adında bir string[] ekle ve Reach.OutsideSystem ya da Reversibility.OneWay olan bir kararın danışılanlar listesi boşsa programın uyarı vermesini sağla. D05'i bu kuralla tekrar çalıştır.

★★★ Ekibinle bir saatlik bir oturumda son altı ayın on kararını listele, her birine üç ölçütü uygula ve ADR'ı olması gerekip de olmayanları işaretle. Bunlardan en riskli olanı, yani gerekçesi unutulursa en pahalı geri alınacak olanı seç; bir sonraki yazıyı okuduktan sonra onun ADR'ını yaz.

Hangi kararın kayıt hak ettiğini artık biliyoruz. Bir sonraki yazıda kaydın kendisine geçiyoruz: Nygard şablonu, MADR 4.0 ve Y-statement ile ADR yazmak, status yaşam döngüsü ve ADR'ı bir pull request olarak review etmek.