Mimariyi Belgelemek: arc42, Docs as Code ve C4 Diyagramlarını ADR'larla Yaşatmak
Mimariyi yaşayan bir belge yapmak için arc42 yapı, C4 view'ları diyagram, ADR'lar karar olarak aynı repo'da durur ve CI diyagramların modelle tutarlı olduğunu her PR'da kontrol eder. Bookstore'un docs klasöründe arc42 bölümleri ile ADR ve C4 view eşlemesi, Structurizr'in !docs ve !adrs directive'leri, inspect ve bayatlık kontrolü, hangi diyagramın hangi okura gösterileceği.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un mimari dokümanı artık repo'nun içinde, kodla aynı pull request akışında. docs/ klasörünün yapısı:
docs/
workspace.dsl
arc42/
01-introduction-and-goals.md
02-constraints.md
03-context-and-scope.md
...
09-architecture-decisions.md
10-quality-requirements.md
11-risks-and-technical-debt.md
12-glossary.md
decisions/
0003-start-with-a-modular-monolith.md
0004-order-email-through-an-in-process-queue.md
0007-use-postgresql-instead-of-sql-server.md
0011-order-events-through-a-transactional-outbox.md
diagrams/
structurizr-Context.mmd
structurizr-Containers.mmd
tools/
check-docs.sh
Ürün ekibi mobil uygulamayı onayladı ve Alan, mobil uygulamayı C4 (Context, Container, Component, Code) modeline ekleyen bir PR (pull request) açtı. workspace.dsl'e eklenen üç satır:
+ mobile = container "Mobile App" "Catalog and orders on the phone." "iOS, Android" "Mobile"
+ customer -> bookstore.mobile "Orders books on the phone" "HTTPS"
+ bookstore.mobile -> bookstore.api "Calls" "JSON/HTTPS"
Alan diyagramları yeniden üretmeyi unuttu. CI'daki (Continuous Integration) kontrol script'i üç adım çalıştırıyor: modeli doğrulayan validate, modeli kurallara karşı denetleyen inspect ve commit'lenmiş diyagramların güncel olup olmadığına bakan bir karşılaştırma. Hangisi kırmızıya düşer? Bir de şunu düşün: müşteri artık sisteme iki yoldan geliyor; System Context diyagramı değişir mi?
Belgelemek de Bir Karar
Bu seriye kadar kararları sınıflandırdık, kaydettik, tarttık ve tartıştık. Ama bir sisteme yeni katılan biri ADR (Architecture Decision Record) listesini okuyarak sistemi anlayamaz; ADR'lar tek tek kararları anlatır, bütünün resmini değil. Bütünün resmi için iki şey gerekiyor: bir yapı (hangi bilgi nerede?) ve diyagramlar (sistem neye benziyor?). Bu yazıda yapı için arc42'yi, diyagramlar için C4 (Context, Container, Component, Code) modelini, ikisini ADR'larla aynı yerde tutmak için de docs as code yaklaşımını kullanıyoruz.
C4'ün kendisi, yani dört seviye, notation kuralları ve "container Docker container değildir" uyarısı, Yazılım Tasarımı kategorisindeki UML (Unified Modeling Language) ve Modeling serisinin "C4 Model: Context, Container, Component, Code ve Notation Kuralları" yazısında anlatıldı. Structurizr DSL'in (Domain-Specific Language) model ve view ayrımını, validate ve export komutlarını da aynı serinin "Mermaid ve Structurizr DSL" yazısı gösterdi. Burada notation'ı ve aracı tekrar etmiyoruz; soru şu: bu diyagramlar, arc42 dokümanı ve ADR'lar nasıl tek bir yaşayan bütün haline gelir?
arc42'nin On İki Bölümü
arc42, Peter Hruschka ve Gernot Starke'nin hazırladığı, Creative Commons Attribution-ShareAlike 4.0 lisanslı bir mimari dokümantasyon şablonu. İçerik değil yapı veriyor: her mimari dokümanın sorması gereken soruları on iki bölüme ayırıyor. Introduction and Goals (temel requirement'lar ve en önemli kalite hedefleri), Constraints, Context and Scope (dış sistemler ve interface'ler), Solution Strategy (temel fikirler), Building Block View (statik yapı, seviye seviye), Runtime View (önemli senaryolarda parçaların etkileşimi), Deployment View (altyapı ve yazılımın nerede çalıştığı), Crosscutting Concepts, Architecture Decisions, Quality Requirements (kalite ağacı ve senaryolar), Risks and Technical Debt ve Glossary.
Şablonun güzel tarafı, bölümlerin çoğunun bu serinin ve Mimariye Giriş serisinin ürettiği şeylerle birebir eşleşmesi. Aşağıdaki figür Bookstore'un docs/ klasöründe her bölümün içeriğinin nereden geldiğini gösteriyor:

C4'ün System Context view'u, arc42'nin Context and Scope bölümünün diyagramı; Container ve Component view'ları Building Block View'un birinci ve ikinci seviyesi; dynamic view'lar ya da UML sequence diyagramları Runtime View; deployment view'lar Deployment View. ADR'lar Architecture Decisions bölümünü dolduruyor, Solution Strategy ise en önemli birkaç ADR'ın ana fikrini verip onlara link veriyor (figürdeki kesikli ok). Mimariye Giriş'in üçüncü yazısındaki utility tree ve senaryolar Introduction and Goals'taki kalite hedeflerini ve Quality Requirements bölümünü, bir önceki yazılardaki ATAM (Architecture Tradeoff Analysis Method) oturumunun risk'leri ve risk temaları da Risks and Technical Debt'i besliyor. Gri bölümler (Constraints, Crosscutting Concepts, Glossary) elle yazılıyor.
arc42'nin dokuzuncu bölümünün kendi tanımında önemli bir ayrıntı var: önemli kararlar, başka bir yerde anlatılmıyorlarsa. Yani ADR'larını ayrı tutuyorsan bu bölüm onları kopyalamaz, onlara yönlendirir. Bookstore'da bölümün tamamı üç cümle:
## Architecture Decisions
Decisions are recorded as MADR files in `docs/decisions` and imported with
`!adrs`. This section only links the decisions that shaped the structure:
ADR-0003 (modular monolith) and ADR-0011 (outbox).
Docs as Code: Model, Doküman ve Kararlar Aynı Repo'da
Docs as code, dokümanı kod gibi ele almak demek: düz metin (Markdown, AsciiDoc, DSL), kodla aynı repo, aynı branch'ler, pull request ile review ve CI'da (Continuous Integration) otomatik kontroller. Kazancı kodun kazancıyla aynı: diff alınabilir, kimin neyi neden değiştirdiği bellidir ve doküman, onu bayatlatabilecek kod değişikliğiyle aynı PR'da güncellenebilir.
Structurizr'in iki directive'i bu yaklaşımı model seviyesine taşıyor. !docs bir klasördeki Markdown dosyalarını, !adrs ise ADR'ları workspace'e dokümantasyon olarak bağlıyor. Bookstore'un workspace.dsl'i:
workspace "Bookstore" "Online bookstore: catalog, ordering, payment, shipping." {
!identifiers hierarchical
model {
customer = person "Customer" "Searches books and places orders."
support = person "Support Agent" "Looks up orders and resends e-mails."
paymentProvider = softwareSystem "Payment Provider" "Authorizes and captures card payments." "External"
emailProvider = softwareSystem "E-mail Provider" "Delivers transactional e-mail." "External"
carrier = softwareSystem "Carrier API" "Creates shipping labels and tracking numbers." "External"
bookstore = softwareSystem "Bookstore" "Sells books online." {
!docs arc42
!adrs decisions madr
spa = container "Web App" "Catalog, cart and checkout UI." "Angular 22" "Browser"
api = container "Bookstore.Web" "Modular monolith: Catalog, Ordering, Inventory, Payment, Shipping, Notification." "ASP.NET Core, .NET 10"
db = container "Bookstore Database" "One schema per module." "SQL Server" "Database"
}
customer -> bookstore.spa "Searches and orders books" "HTTPS"
support -> bookstore.spa "Finds orders" "HTTPS"
bookstore.spa -> bookstore.api "Calls" "JSON/HTTPS"
bookstore.api -> bookstore.db "Reads from and writes to" "EF Core"
bookstore.api -> paymentProvider "Captures payments" "HTTPS"
bookstore.api -> emailProvider "Sends order e-mails" "HTTPS"
bookstore.api -> carrier "Creates shipments" "HTTPS"
}
views {
systemContext bookstore "Context" {
include *
autolayout lr
}
container bookstore "Containers" {
include *
autolayout lr
}
styles {
element "External" {
background #999999
}
element "Database" {
shape cylinder
}
element "Browser" {
shape webbrowser
}
}
}
configuration {
scope softwaresystem
}
}
Dikkat edilecek üç ayrıntı var. Birincisi, directive'ler workspace'e değil bookstore software system'ine bağlı ve configuration bloğunda workspace'in kapsamı tek bir software system olarak tanımlı. İlk denememizde directive'leri workspace seviyesine koymuştuk; inspect komutu bunu hata olarak raporladı. Directive'leri hiç koymadığımızda çıktı şuydu:
ERROR | model.softwaresystem.documentation | The software system "Bookstore" has containers, but is missing documentation.
ERROR | model.softwaresystem.decisions | The software system "Bookstore" has containers, but is missing decisions.
exit code: 1
İkincisi, !adrs yalnız DSL dosyasının bulunduğu klasördeki ya da altındaki bir yolu kabul ediyor. MADR'ın (Markdown Architectural Decision Records) önerdiği docs/decisions yolunu korumak için workspace.dsl'i docs/'un köküne koyduk. Directive'in ikinci parametresi importer'ı seçiyor; madr importer'ı front matter'daki status ve date alanlarını okuyor. Workspace'i JSON (JavaScript Object Notation) olarak export edip kararlara baktığımızda:
11 accepted Order events through a transactional outbox
3 accepted Start with a modular monolith
4 superseded by ADR-0011 Order e-mail through an in-process queue
7 rejected Use PostgreSQL instead of SQL Server
Bir tuzak: MADR 4.0 şablonu status değerini YAML'da (YAML Ain't Markup Language) tırnak içinde yazmayı öneriyor. Aynı ADR'ı status: "superseded by ADR-0011" olarak yazdığımızda Structurizr tırnakları da status'un parçası olarak aldı ve kararı tırnaklı bir değerle gösterdi. Bookstore'da status'u tırnaksız yazıyoruz; YAML için ikisi de geçerli bir string.
Üçüncüsü, arc42 bölümleri diyagramları kopyalamıyor, gömüyor. Context and Scope bölümü:
## Context and Scope
Customers and support agents use the web app; Bookstore depends on three
external systems. Interfaces and owners are listed below the diagram.

| Partner | Interface | Owner |
|------------------|-------------------|------------------|
| Payment Provider | HTTPS, card API | Ordering & Payment |
| E-mail Provider | HTTPS | Customer Experience |
| Carrier API | HTTPS | Fulfillment |
, Structurizr'in Markdown'a eklediği bir uzantı: Context anahtarlı view'u, dokümanın içinde canlı olarak çizer. Diyagram modelden üretildiği için bir container'ın adı değiştiğinde doküman da değişmiş olur. Bu uzantı Structurizr'in kendi arayüzünde çalışıyor; GitHub'da Markdown olarak açıldığında görünmez. O okurlar için aşağıdaki bölümde ürettiğimiz Mermaid çıktılarını kullanıyoruz.
Hangi Diyagram Hangi Okura
Simon Brown'ın C4'ü dört seviyeye ayırmasının bir nedeni farklı okurlar. Bookstore'da kimin hangi belgeyle başladığını yazılı hale getirdik:
| Okur | İlk bakacağı yer | Neden |
|---|---|---|
| Yeni gelen developer | Context ve Container view, arc42 4 ve 5, decision log index'i | Sistemin parçaları, temel fikirler ve "bu neden böyle?" sorusunun cevapları |
| Ops | Deployment view, arc42 7, 8 ve 11 | Neyin nerede çalıştığı, ortak konular (logging, yedekleme), bilinen risk'ler |
| Ürün ve yönetim | Context view, arc42 1, 10 ve 11 | Sistemin sınırı, kalite hedefleri ve iş açısından risk'ler |
| Başka ekip | Context view, API (Application Programming Interface) contract'ı, ilgili ADR'lar | Bizim sistemimizle nasıl konuşacağı ve hangi kararların onu bağladığı |
Tablodaki önemli ayrıntı, kimsenin on iki bölümün hepsini okumaması. Component view'u neredeyse yalnız o module'de çalışan developer'lar okuyor; Code seviyesini ise hiç çizmiyoruz, IDE (Integrated Development Environment) zaten gösteriyor. Bir diyagramı çizmeden önce "bunu kim, hangi soruyu cevaplamak için okuyacak?" sorusunu sor; cevap yoksa diyagram da gereksiz.
Bayatlamayı Önlemek: CI'da Render ve Sahiplik
Mimari diyagramların en yaygın sorunu yanlış olmaları değil, bir zamanlar doğru olmaları. Wiki'deki bir PNG (Portable Network Graphics) dosyası, çizildiği gün doğruydu; sonra sistem değişti, çizim değişmedi. Docs as code bu sorunu kendiliğinden çözmez, ama kontrol edilebilir hale getirir. Bookstore'un CI script'i:
#!/usr/bin/env bash
# Mimari dokümanı CI'da: model geçerli mi, inspection temiz mi, commit'lenen diyagramlar güncel mi?
set -euo pipefail
run() { docker run --rm -v "$PWD/docs:/usr/local/structurizr" structurizr/structurizr "$@" -w /usr/local/structurizr/workspace.dsl; }
run validate
run inspect
run export -f mermaid -o /usr/local/structurizr/diagrams > /dev/null
if ! git diff --quiet -- docs/diagrams || [[ -n "$(git ls-files --others --exclude-standard docs/diagrams)" ]]; then
echo "::error::docs/diagrams is out of date; run tools/check-docs.sh locally and commit the result"
git status --short -- docs/diagrams
exit 1
fi
echo "architecture docs are up to date"
Script modeli doğruluyor, inspection'ları çalıştırıyor (inspect hata bulduğunda exit code 1 ile çıkıyor), diyagramları Mermaid olarak docs/diagrams'a export ediyor ve git ile karşılaştırıyor. Commit'lenmiş Mermaid dosyaları diff'lenebiliyor: PR'da diyagramın neyinin değiştiği satır satır görünüyor. Mermaid ayrıca GitHub'ın Markdown içinde render ettiği bir format; Structurizr'e erişimi olmayan biri de diyagramı görebiliyor. İlk çalıştırmada diyagramlar henüz yoktu:
::error::docs/diagrams is out of date; run tools/check-docs.sh locally and commit the result
?? docs/diagrams/
exit code: 1
Üretilen dosyaları commit'ledikten sonra:
architecture docs are up to date
exit code: 0
Açılıştaki soru: Alan'ın PR'ı. validate ve inspect geçiyor, çünkü model geçerli ve kurallara uygun; kırmızıya düşen karşılaştırma:
::error::docs/diagrams is out of date; run tools/check-docs.sh locally and commit the result
M docs/diagrams/structurizr-Containers.mmd
exit code: 1
docs/diagrams/structurizr-Containers.mmd | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
Yalnız Containers diyagramı değişti. Context diyagramı aynı kaldı, çünkü Structurizr müşteriden sisteme giden ilişkileri context seviyesinde tek bir ok olarak topluyor ve o ok zaten vardı. Bu doğru davranış, ama bir şey söylüyor: "müşteri artık telefondan da geliyor" bilgisi Context diyagramında görünmüyor. Bunu görünür yapmak istiyorsan ilişkinin açıklamasını değiştirmek gerekiyor; CI yalnız modelin ve diyagramların tutarlılığını kontrol edebilir, modelin söylediklerinin yeterli olup olmadığını değil.
Script'i her PR'da çalıştıran workflow kısa; script'i ve senaryoları yerelde çalıştırdık, workflow'u GitHub üzerinde bu yazı için koşturmadık:
name: architecture-docs
on:
pull_request:
paths: ["docs/**"]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: ./tools/check-docs.sh
Otomatik kontrolün ötesinde bir sahiplik kuralı da gerekiyor. Bookstore'da üç kural var. Bir container ekleyen, kaldıran ya da iki container arasına yeni bir ilişki getiren PR, workspace.dsl'i de değiştirir; review'da bu sorulur. docs/ klasörü ikinci yazıdaki CODEOWNERS kuralına tabi. Aylık architecture review'da (son yazının konusu) Context ve Container view'larına beş dakika bakılır: "bu hâlâ doğru mu?"
Ne Kadar Belge Yeter?
arc42'nin kendi FAQ'ı (Frequently Asked Questions) da her şeyi doldurmamayı, yalnız paydaşların gerçekten ihtiyaç duyduğu bölümleri yazmayı öneriyor; bunun için de paydaşlara tek tek sormayı. Üç developer'lık bir ekip için bizim minimumumuz şu: Context ve Container view'ları, decision log, arc42'nin 1, 3, 4, 9, 10 ve 11. bölümleri. Toplam birkaç sayfa. Deployment View, ortam sayısı ikiye çıktığında; Runtime View, kimsenin tek başına anlatamadığı bir akış ortaya çıktığında yazılır.
Belgelemenin de bir maliyeti var ve bu maliyet bakımda. Her diyagram ve her bölüm, sistem değiştikçe güncellenmesi gereken bir şey. Okuru olmayan bir bölüm, bakımı olmayan bir bölüme ve oradan da yanlış bir bölüme dönüşür. Az ama güncel, çok ama bayattan her zaman iyidir.
Sık Düşülen Tuzaklar
Diyagramı modelden değil elle çizmek. İki ayrı çizimde aynı container iki farklı adla durur ve hangisinin doğru olduğu bilinmez. Elementi bir kez tanımla, view'ları modelden üret.
Kararları arc42'nin içine kopyalamak. Dokuzuncu bölüm ADR'ların bir kopyası olursa, bir ADR supersede edildiğinde iki yer güncellenmek zorunda kalır ve biri unutulur. Link ver, kopyalama.
Üretilen diyagramları commit'lememek ya da kontrol etmemek. Yalnız CI'da üretilip bir artifact'e konan diyagram PR'da görünmez; commit'lenip kontrol edilmeyen diyagram ise bayatlar. İkisini birlikte yap.
CI yeşil diye diyagramı doğru sanmak. Bayatlık kontrolü modelin ve çıktıların tutarlı olduğunu söyler; modelin gerçek sistemi anlattığını değil. Mobil uygulama örneğinde Context diyagramı tutarlıydı ama eksik bir hikâye anlatıyordu.
Her okura aynı belgeyi vermek. Yönetime Component view, yeni gelen developer'a arc42'nin tamamı göndermek, ikisini de belgeyi okumamaya iter.
Tooling'e takılmak. arc42 Word'de, C4 bir beyaz tahtada da yaşayabilir. Docs as code bir hedef değil, bayatlamayı kontrol edilebilir yapmanın bir yolu.
Küçük Bir Deneme
★☆☆ Kendi projen için figürdeki mapping'i yap: arc42'nin on iki bölümünün her biri için içerik bugün nerede (wiki, kafalar, kod, hiçbir yer)? Hiçbir yerde olmayan en önemli bölüm hangisi?
★★☆ Bookstore'un workspace.dsl'ine bir deployment view ekle (production ortamı, iki uygulama instance'ı ve SQL (Structured Query Language) Server). check-docs.sh'ı çalıştır; yeni Mermaid dosyası oluşuyor mu ve script bunu bayatlık olarak yakalıyor mu?
★★★ Context diyagramının "müşteri telefondan da geliyor" bilgisini göstermemesi sorununu çöz: hangi model değişikliği bunu görünür yapar? Sonra check-docs.sh'a bir adım ekle: docs/decisions'taki her ADR'ın arc42'nin 4. ya da 9. bölümünden en az birinde ya da bir başka ADR'da anıldığını kontrol etsin.
Diyagramlar ve doküman artık modelle birlikte kontrol ediliyor. Ama ADR'ların kendisi hâlâ yalnız insan dikkatine emanet: "public API'de breaking change yok" diyen bir kararı kim kontrol ediyor? Bir sonraki yazıda MADR'ın Confirmation bölümünü otomatik kontrollere çeviriyoruz: OpenAPI diff, Angular budget'ları, migration'ların geri alınabilirliği ve kontrol edilemeyen kararlar için manuel checklist.