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

ADR'dan Otomatik Kontrole: Kararları Fitness Function ve CI ile Doğrulamak

Mert Özen 8 Kas 2026 15 dk 4 görüntülenme
ADR'dan Otomatik Kontrole: Kararları Fitness Function ve CI ile Doğrulamak

Her ADR'ın Confirmation bölümü bir fitness function'ın spesifikasyonudur: karar ya her PR'da, ya takvimle, ya production'da sürekli kontrol edilir ya da kısa bir manuel listeye düşer. Bookstore'da public API kararını oasdiff ile, bundle boyutu kararını Angular budget'larıyla, migration'ların geri alınabilirliğini bir xUnit test'iyle ve .NET destek süresini haftalık bir kontrolle gerçek çıktılarıyla doğruluyoruz.

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

Önce Tahmin Et

Mobil uygulama yola çıkmadan önce Bookstore ekibi public API (Application Programming Interface) için bir karar verdi. ADR'ın (Architecture Decision Record) özü:

---
status: accepted
date: 2026-07-15
decision-makers: Ada, Grace
consulted: mobile app team, Customer Experience team
informed: all teams
---

# Public API stays compatible within a major version

## Context and Problem Statement

The mobile app and the second country's web app will call /api/v1. A mobile
release takes up to two weeks to reach users, so old app versions keep calling
the API long after a deploy. How do we change the API without breaking them?

## Decision Outcome

Chosen option: "No breaking change inside a major version", because old
mobile clients cannot be updated on our schedule. Breaking changes go to
/api/v2, and v1 stays for at least six months after v2 ships.

### Confirmation

* CI generates the OpenAPI document at build time and runs
  `oasdiff breaking` against `openapi/bookstore-v1.json`; any ERR fails the PR.
* The baseline file changes only in a PR labelled "api-release".

Bugünkü v1 API'si üç endpoint'ten oluşan küçük bir minimal API:

using Microsoft.AspNetCore.Http.HttpResults;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

WebApplication app = builder.Build();
app.MapOpenApi();

BookResponse dune = new(1, "Dune", "Frank Herbert", 18.49m, "EUR");

RouteGroupBuilder books = app.MapGroup("/api/v1/books");
books.MapGet("/{id:int}", Results<Ok<BookResponse>, NotFound> (int id) =>
    id == dune.Id ? TypedResults.Ok(dune) : TypedResults.NotFound());
books.MapGet("/", (string? query, int page = 1) => TypedResults.Ok(new[] { dune }));

app.MapPost("/api/v1/orders", (PlaceOrderRequest request) =>
    TypedResults.Created("/api/v1/orders/1042", new OrderResponse(1042, request.BookId, request.Quantity)));

app.Run();

public sealed record BookResponse(int Id, string Title, string Author, decimal Price, string Currency);
public sealed record PlaceOrderRequest(int BookId, int Quantity);
public sealed record OrderResponse(int OrderId, int BookId, int Quantity);

Bir PR (pull request) üç satırı değiştiriyor: Price'ın adı UnitPrice oluyor, kitap listesine zorunlu bir currency parametresi geliyor ve yanıta Isbn ekleniyor.

-BookResponse dune = new(1, "Dune", "Frank Herbert", 18.49m, "EUR");
+BookResponse dune = new(1, "Dune", "Frank Herbert", 18.49m, "EUR", "9780441013593");
-books.MapGet("/", (string? query, int page = 1) => TypedResults.Ok(new[] { dune }));
+books.MapGet("/", (string? query, string currency, int page = 1) => TypedResults.Ok(new[] { dune }));
-public sealed record BookResponse(int Id, string Title, string Author, decimal Price, string Currency);
+public sealed record BookResponse(int Id, string Title, string Author, decimal UnitPrice, string Currency, string? Isbn);

Build geçiyor, unit test'ler geçiyor, Angular uygulaması aynı PR'da güncellendiği için o da çalışıyor. Bu üç değişiklikten hangileri iki hafta önce mağazaya çıkmış mobil uygulamayı kırar? Code review'da kim fark eder?

Confirmation Bölümünü Okumak

İkinci yazıda MADR (Markdown Architectural Decision Records) 4.0'ın Confirmation bölümünü görmüştük: "kararın uygulandığını nasıl anlayacağız?" Mimariye Giriş serisinin "Mimariyi Zaman İçinde Korumak" yazısı bu sorunun bir cevabını fitness function adıyla verdi: bir ya da birkaç architecture characteristic'in hâlâ korunup korunmadığını objektif olarak değerlendiren her mekanizma. Aynı yazı fitness function'ları atomic/holistic, triggered/continuous ve static/dynamic eksenlerinde sınıflandırdı; Clean, Onion ve Hexagonal Architecture serisinin sekizinci yazısı da ArchUnitNET ile dependency kurallarını test'e çevirdi. Bu yazı o temeli tekrar etmiyor; bağı kuruyor: her ADR'ın Confirmation bölümü, bir fitness function'ın spesifikasyonu olmalı.

Bunun pratik anlamı şu. ADR'ı review ederken Confirmation'a bak ve üç sorudan birine cevap ara. Bu karar her PR'da otomatik kontrol edilebilir mi (triggered)? Belli aralıklarla kontrol edilmesi mi gerekiyor (temporal)? Production'da sürekli izlenmesi mi gerekiyor (continual)? Hiçbiri değilse, kontrol bir insanın kontrol listesine düşer. Bookstore'un decision log'undan örnekler:

KararKontrolNe zamanTür
ADR-0003 modular monolithArchUnitNET kuralı: module'ler birbirinin internal type'larını kullanmazHer PRTriggered, yapısal
ADR-0010 public API uyumluluğuOpenAPI diff, ERR seviyesinde kırmızıHer PRTriggered
ADR-0014 initial bundle < 250 kBAngular budget, maximumErrorHer production buildTriggered
ADR-0013 geri alınabilir migrationHer migration'ın Down'u dolu ya da gerekçeliHer PRTriggered
ADR-0015 desteklenen LTS (Long Term Support) .NETDestek bitişine en az 180 günHaftalıkTemporal
ADR-0008 katalog cache1.000 aramada veritabanı sorgusu, eski fiyat süresiGece load test'i, production metriğiTemporal ve continual
ADR-0011 outboxPayload'da kişisel veri yokHer PR, review'daManuel

Tablonun ilk satırı yapısal bir kontrol ve onu Clean, Onion ve Hexagonal serisinde gördük; module sınırlarına özel kuralları da ilerideki Modular Monolith serisi ele alacak. Bu yazıda yapısal olmayanlara bakıyoruz.

OpenAPI Diff: Public API Kararını CI'a Bağlamak

ADR-0010'u kontrol etmek için iki şeye ihtiyaç var: build sırasında üretilen güncel OpenAPI dokümanı ve repo'da duran bir baseline. .NET 10'da Microsoft.Extensions.ApiDescription.Server paketi, AddOpenApi ile tanımlanan dokümanı build sırasında bir dosyaya yazıyor. Project dosyası:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../../artifacts/openapi</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.14" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.14">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>

OpenApiDocumentsDirectory, dosyanın nereye yazılacağını söylüyor. v1 yayına çıktığında üretilen dosya openapi/bookstore-v1.json olarak commit'lendi; bu dosya contract'ın kendisi. Karşılaştırma için Tufin'in başlattığı açık kaynak oasdiff aracını, 1.36.0 sürümünün Docker image'ıyla kullandık. Açılıştaki PR'ın gerçek çıktısı:

$ oasdiff breaking openapi/bookstore-v1.json artifacts/openapi/Bookstore.Web.json --fail-on ERR
3 changes: 3 error, 0 warning, 0 info
error	[new-required-request-parameter] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books
		added the new required `query` request parameter `currency`

error	[response-required-property-removed] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books
		removed the required property `items/price` from the response with the `200` status

error	[response-required-property-removed] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books/{id}
		removed the required property `price` from the response with the `200` status


exit code: 1

Üç değişiklikten ikisi breaking. Zorunlu bir query parametresi eklemek, o parametreyi göndermeyen eski istemciyi kırar. Price'ın adını değiştirmek, C# tarafında bir rename refactoring'i, ama JSON (JavaScript Object Notation) tarafında price alanının yok olması demek; eski mobil uygulama fiyatı boş gösterir. Code review bunu neden kaçırır? Çünkü diff'te yalnız bir record'un parametre adı değişiyor ve aynı PR'da Angular istemcisi de güncellendiği için her şey çalışıyor görünüyor. Kırılan istemci repo'da değil, kullanıcıların telefonunda.

ADR'a uygun hali: Price kalıyor, currency isteğe bağlı oluyor (string? currency), Isbn ekleniyor:

$ oasdiff breaking openapi/bookstore-v1.json artifacts/openapi/Bookstore.Web.json --fail-on ERR
No breaking changes to report, but the specs are different.
Run 'oasdiff diff' to see structural differences.
exit code: 0
$ oasdiff changelog openapi/bookstore-v1.json artifacts/openapi/Bookstore.Web.json
3 changes: 0 error, 0 warning, 3 info
info	[new-optional-request-parameter] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books
		added the new optional `query` request parameter `currency`

info	[response-required-property-added] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books
		added the required property `items/isbn` to the response with the `200` status

info	[response-required-property-added] at artifacts/openapi/Bookstore.Web.json
	in API GET /api/v1/books/{id}
		added the required property `isbn` to the response with the `200` status

changelog komutu breaking olmayan değişiklikleri de listeliyor; PR açıklamasına eklemek için iyi bir kaynak. Bir ayrıntı dikkatimizi çekti: string? Isbn nullable olmasına rağmen şemada "required" işaretlendi, çünkü positional record'un constructor parametresi. Yanıta yeni bir required alan eklemek istemciyi kırmaz, ama bu alanı ileride kaldırmak breaking olur. Yani bugün eklenen her alan, v1 boyunca taşınacak bir taahhüt.

CI (Continuous Integration) adımı:

name: decision-checks
on: pull_request

jobs:
  api-compatibility:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      - run: dotnet build src/Bookstore.Web
      # ADR-0010: v1 contract'ı kırılırsa PR kırmızıya düşer
      - run: >
          docker run --rm -v "$PWD:/specs" -w /specs tufin/oasdiff:v1.36.0
          breaking openapi/bookstore-v1.json artifacts/openapi/Bookstore.Web.json --fail-on ERR

Komutları ve senaryoları yerelde çalıştırdık; workflow'u GitHub'da bu yazı için koşturmadık. ADR'daki ikinci Confirmation maddesi de önemli: baseline dosyası yalnız "api-release" etiketli bir PR'da değişebilir. Aksi halde breaking change'i geçirmenin en kolay yolu baseline'ı güncellemek olur; ikinci yazıdaki CODEOWNERS kuralını openapi/ klasörüne de uyguluyoruz.

Angular Budget'ları: Bundle Boyutu Kararı

İkinci ülkenin kullanıcılarının çoğu mobil ağdan geliyor ve ekip bunun için bir karar verdi: ADR-0014, Angular uygulamasının initial bundle'ı 250 kB'ı geçmez. Angular serisinin "Angular Build ve Deploy" yazısında budget'ları görmüştük; burada onları bir ADR'ın Confirmation'ı olarak kullanıyoruz. Angular 22.2 ile oluşturulan bir projede angular.json'ın production configuration'ındaki budget'lar, kararın sayılarıyla:

{
  "budgets": [
    {
      "type": "initial",
      "maximumWarning": "230kB",
      "maximumError": "250kB"
    },
    {
      "type": "anyComponentStyle",
      "maximumWarning": "4kB",
      "maximumError": "8kB"
    }
  ]
}

Boş proje 217.10 kB ile sınırın altında. Bir PR, raflardaki kitapları ikişerli gruplamak için lodash'in chunk fonksiyonunu kullanıyor:

import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import _ from 'lodash';

@Component({
  imports: [RouterOutlet],
  selector: 'app-root',
  styleUrl: './app.css',
  templateUrl: './app.html',
})
export class App {
  protected readonly title = signal('bookstore-web');
  protected readonly shelves = _.chunk(['Dune', 'Emma', 'The Hobbit', 'Middlemarch'], 2);
}

Node 24 ile ng build'in gerçek çıktısı:

✔ Building...
Initial chunk files | Names         |  Raw size | Estimated transfer size
main-XNBDORRN.js    | main          | 291.50 kB |                83.67 kB
styles-5INURTSO.css | styles        |   0 bytes |                 0 bytes

                    | Initial total | 291.50 kB |                83.67 kB

Application bundle generation failed. [1.009 seconds] - 2026-11-08T00:47:56.296Z

▲ [WARNING] bundle initial exceeded maximum budget. Budget 230.00 kB was not met by 61.50 kB with a total of 291.50 kB.


▲ [WARNING] Module 'lodash' used by 'src/app/app.ts' is not ESM

  CommonJS or AMD dependencies can cause optimization bailouts.
  For more information see: https://angular.dev/tools/cli/build#configuring-commonjs-dependencies


✘ [ERROR] bundle initial exceeded maximum budget. Budget 250.00 kB was not met by 41.50 kB with a total of 291.50 kB.


exit code: 1

Tek bir fonksiyon için bütün library bundle'a girdi: 74.4 kB. maximumWarning sarı bir uyarı verdi, maximumError build'i durdurdu. Aradaki uyarı da kendi başına bir ipucu: lodash CommonJS olduğu için tree-shaking çalışmıyor. Budget'ın değeri tam burada; kural "lodash kullanma" değil, "initial bundle 250 kB'ı geçmez". Geliştirici ES (ECMAScript) module olarak dağıtılan bir alternatif seçebilir, kodu lazy-loaded bir route'a taşıyabilir ya da iki satırlık fonksiyonu kendisi yazabilir. Karar bir sonucu korur, bir çözümü dayatmaz.

Bağımlılıklar ve Migration'lar

Bağımlılık kararları için NuGetAudit'i OWASP (Open Worldwide Application Security Project) Top 10 serisinin A03 yazısında ayrıntılı gördük. Bir ADR'ın Confirmation'ı olarak tek fark, uyarının hangi seviyede build'i durduracağının kararın kendisinde yazılı olması:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <NuGetAuditMode>all</NuGetAuditMode>
    <NuGetAuditLevel>high</NuGetAuditLevel>
    <WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
  </ItemGroup>

</Project>
Bookstore.Audit.csproj : error NU1903: Warning As Error: Package 'Newtonsoft.Json' 12.0.3 has a known high severity vulnerability, https://github.com/advisories/GHSA-5crp-9r3c-p9vr
exit code: 1

Bu yazıyı hazırlarken bir sürpriz yaşadık: aşağıdaki migration örneği için eklediğimiz Microsoft.EntityFrameworkCore.SqlServer 10.0.14, Microsoft.Data.SqlClient 6.1.7 üzerinden System.Security.Cryptography.Pkcs 9.0.11'i getirdi ve restore bu paket için high seviyede bir NU1903 uyarısı verdi. Kural sabit bir eşikse, böyle bir uyarı bir sabah bütün PR'ları kırmızıya çevirebilir; ADR, kimin ne kadar sürede ne yapacağını da söylemeli.

Migration'lar için karar ADR-0013: her migration geri alınabilir, alınamıyorsa gerekçesi yazılır. Neden önemli? Deploy sonrası bir sorun çıktığında geri dönüş planı genellikle "önceki sürümü deploy et, migration'ı geri al" ve boş bir Down bu planı sessizce iptal eder. EF (Entity Framework) Core'un Migration class'ı, Down'un ürettiği işlemleri DownOperations property'siyle veriyor; veritabanına bağlanmadan kontrol edebiliriz:

using Microsoft.EntityFrameworkCore;

namespace Bookstore.Data;

public sealed class BookstoreDbContext(DbContextOptions<BookstoreDbContext> options) : DbContext(options);

// Down'u bilinçli olarak boş bırakılan migration, gerekçesini ve ilgili ADR'ı taşır
[AttributeUsage(AttributeTargets.Class)]
public sealed class IrreversibleAttribute(string reason) : Attribute
{
    public string Reason { get; } = reason;
}
using System.Reflection;
using Bookstore.Data;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Bookstore.ArchitectureTests;

// ADR-0013: Her migration geri alınabilir; alınamıyorsa gerekçesi [Irreversible] ile yazılır
public sealed class MigrationTests
{
    [Fact]
    [Trait("Category", "Architecture")]
    public void Every_migration_can_be_rolled_back_or_says_why_not()
    {
        string[] violations =
        [
            .. typeof(BookstoreDbContext).Assembly.GetTypes()
                .Where(t => t.IsSubclassOf(typeof(Migration)) && !t.IsAbstract)
                .Where(t => t.GetCustomAttribute<IrreversibleAttribute>() is null)
                .Where(t => ((Migration)Activator.CreateInstance(t)!).DownOperations.Count == 0)
                .Select(t => t.GetCustomAttribute<MigrationAttribute>()!.Id)
        ];

        Assert.True(violations.Length == 0,
            $"Migrations with an empty Down and no [Irreversible] reason: {string.Join(", ", violations)}");
    }
}

İki kolonu birleştiren bir migration, doğası gereği kolay geri alınamaz:

using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Bookstore.Data.Migrations;

[DbContext(typeof(BookstoreDbContext))]
[Migration("20261020141500_MergeAuthorNames")]
public sealed class MergeAuthorNames : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>("FullName", "Authors", maxLength: 200, nullable: true);
        migrationBuilder.Sql("UPDATE Authors SET FullName = FirstName + ' ' + LastName");
        migrationBuilder.DropColumn("FirstName", "Authors");
        migrationBuilder.DropColumn("LastName", "Authors");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
    }
}
failed Bookstore.ArchitectureTests.MigrationTests.Every_migration_can_be_rolled_back_or_says_why_not (5ms)
  Migrations with an empty Down and no [Irreversible] reason: 20261020141500_MergeAuthorNames
    at Bookstore.ArchitectureTests.MigrationTests.Every_migration_can_be_rolled_back_or_says_why_not() in ./tests/Bookstore.ArchitectureTests/MigrationTests.cs:23
./tests/Bookstore.ArchitectureTests/bin/Debug/net10.0/Bookstore.ArchitectureTests.dll (net10.0|arm64) failed with 1 error(s) (495ms)
  failed: 1

Test, migration'ı geri alınabilir yapmaya zorlamıyor; gerekçesini yazmaya zorluyor. Gerekçe eklendiğinde test geçiyor:

[Irreversible("Merges two columns into one; roll back by restoring the backup taken before deploy (ADR-0013).")]

Bu tür kontrollerin ortak bir özelliği var: geçmenin iki yolu var. Ya kararı uygularsın ya da kararın istisnasını, gerekçesiyle, görünür biçimde yazarsın. Kontrolün susturulması (bir Skip, bir eşik artışı) bir PR'da görünür ve review edilir.

Temporal ve Continual Kontroller

Bazı kararlar kodla değil zamanla bozulur. ADR-0015, Bookstore'un her zaman desteklenen bir LTS .NET sürümünde kalmasını söylüyor. Kod hiç değişmese de bu karar bir gün ihlal edilir: destek biter. Building Evolutionary Architectures kitabının temporal dediği fitness function'lar bunun için: belli bir zamanda ya da aralıkla çalışan kontroller. .NET'in resmî release metadata'sını okuyan küçük bir program:

using System.Net.Http.Json;
using System.Text.Json.Serialization;

// ADR-0015'in Confirmation'ı: hedeflediğimiz .NET sürümünün en az 180 gün desteği kalmış olmalı
string channel = args.ElementAtOrDefault(0) ?? "10.0";
int minDays = int.Parse(args.ElementAtOrDefault(1) ?? "180");
DateOnly today = DateOnly.FromDateTime(TimeProvider.System.GetUtcNow().UtcDateTime);

using HttpClient http = new();
ReleaseIndex index = (await http.GetFromJsonAsync<ReleaseIndex>(
    "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json"))!;

Release release = index.Releases.Single(r => r.Channel == channel);
int daysLeft = release.EolDate.DayNumber - today.DayNumber;
Console.WriteLine($".NET {channel} ({release.Type}, {release.Phase}): end of support {release.EolDate:yyyy-MM-dd}, {daysLeft} days left");

if (daysLeft < minDays)
{
    Console.WriteLine($"FAIL: less than {minDays} days of support left; plan the upgrade (ADR-0015)");
    return 1;
}
return 0;

public sealed record ReleaseIndex([property: JsonPropertyName("releases-index")] Release[] Releases);

public sealed record Release(
    [property: JsonPropertyName("channel-version")] string Channel,
    [property: JsonPropertyName("release-type")] string Type,
    [property: JsonPropertyName("support-phase")] string Phase,
    [property: JsonPropertyName("eol-date")] DateOnly EolDate);
$ dotnet run -- 10.0 180
.NET 10.0 (lts, active): end of support 2028-11-14, 737 days left
exit code: 0
$ dotnet run -- 8.0 180
.NET 8.0 (lts, maintenance): end of support 2026-11-10, 2 days left
FAIL: less than 180 days of support left; plan the upgrade (ADR-0015)
exit code: 1

Bu yazının yazıldığı gün .NET 8'in desteğinin bitmesine iki gün kalmıştı; aynı tarih .NET 9 için de geçerli. .NET 8'de kalan bir ekip, haftalık bir kontrolle bunu en az altı ay önce kırmızı bir iş olarak görürdü. Program PR'da değil takvimle çalışıyor:

name: decision-schedule
on:
  schedule:
    # Her pazartesi 06:00 UTC
    - cron: "0 6 * * 1"

jobs:
  support-window:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      # ADR-0015: hedef .NET sürümünün en az 180 gün desteği kalmalı
      - run: dotnet run --project tools/Bookstore.SupportCheck -- 10.0 180

Continual kontroller ise production'da sürekli çalışır. ADR-0008'in "eski fiyat en fazla 60 saniye" maddesi bir test'le değil, production'da cache'ten dönen fiyatla veritabanındaki fiyatı karşılaştıran bir synthetic check ile doğrulanır; "kampanyada arama p95 < 300 ms" ise bir SLO'nun (Service Level Objective) konusu. Bunların kurulumu ve error budget ile işletilmesi ilerideki SRE (Site Reliability Engineering) serisinin işi. Mimari açıdan bizim sorumluluğumuz, ADR'ın Confirmation bölümünde metriğin adını ve eşiğini yazmak; böylece alarm kurulduğunda hangi karara hizmet ettiği bellidir.

Kontrol Edilemeyen Kararlar: Manuel Checklist

Her karar otomatikleştirilemez. "Outbox payload'ında kişisel veri yok" kuralını bir regex ile yakalamaya çalışabilirsin, ama bir müşteri adının Note alanına girdiğini anlamak bir insanın işi. Böyle kararlar için Bookstore'un PR şablonunda kısa bir liste var:

## Architecture decisions touched by this PR

- [ ] ADR-0003: no module reads another module's tables or internal types
- [ ] ADR-0008: checkout never uses a cached price
- [ ] ADR-0011: new events go through the outbox; payloads carry no personal data
- [ ] None of the above apply to this PR

Listenin kısa olması bilinçli. Her ADR'ı buraya koymak, kimsenin okumadığı ve herkesin hepsini işaretlediği bir form üretir. Yalnız otomatikleştirilemeyen ve sık ihlal edilen kararlar buraya girer; bir madde otomatik bir kontrole dönüştüğünde listeden çıkar. Listenin kendisi de bir sinyal: hangi kararların hâlâ insan dikkatine emanet olduğunu gösteriyor.

Sık Düşülen Tuzaklar

Confirmation'ı boş ya da "code review" diye bırakmak. "Review'da bakılır" bir kontrol değil, bir umut. Ya bir mekanizma yaz ya da kararın neden kontrol edilemediğini ve kimin baktığını yaz.

Kontrolü karardan koparmak. Gerekçesi olmayan bir budget ya da bir OpenAPI kuralı, ilk kırmızıda gevşetilir. Her kontrolün mesajı ya da yorumu ilgili ADR'ın numarasını taşımalı; bu yazıdaki örneklerin hepsi öyle.

Baseline'ı korumamak. Breaking change'i geçirmenin en kolay yolu bookstore-v1.json'ı yeniden üretmekse, kontrolün bir anlamı yok. Baseline değişikliği ayrı bir onay istemeli.

Kararı değil çözümü kontrol etmek. "lodash yasak" kuralı bir sonraki büyük library'yi kaçırır; "initial bundle < 250 kB" kuralı hepsini yakalar. Kontrol, ADR'ın Decision Outcome'ındaki sonucu ölçmeli.

Zamanla bozulan kararları unutmak. Destek süreleri, sertifikalar, lisanslar, deprecated API'ler kod değişmeden bozulur. Takvimli bir kontrol yoksa ilk haberi bir incident'ten alırsın.

Checklist'i şişirmek. Her ADR için bir onay kutusu, kutuların okunmadan işaretlenmesiyle sonuçlanır. Kısa tut, otomatikleştirdikçe kısalt.

Küçük Bir Deneme

★☆☆ Kendi decision log'undan beş ADR seç ve bu yazıdaki tabloyu doldur: kontrol, ne zaman, tür. Kaç tanesi otomatikleştirilebilir, kaç tanesi manuel kalmak zorunda?

★★☆ oasdiff ile şu değişiklikleri tek tek dene ve hangilerinin ERR, hangilerinin WARN ya da INFO olduğunu not et: bir endpoint'i kaldırmak, bir yanıt alanının tipini int'ten string'e çevirmek, bir request alanını isteğe bağlıdan zorunluya çevirmek, yeni bir 404 yanıtı eklemek.

★★★ MigrationTests'e ikinci bir kural ekle: bir tabloyu ya da kolonu silen her migration'ın aynı release'te değil, bir önceki release'te "artık kullanılmıyor" diye işaretlenmiş olmasını kontrol et (expand/contract). Bu bilgiyi nerede tutacağına sen karar ver ve kararını bir ADR olarak yaz.

Kararlarımız artık yazılı, tartışılmış, belgelenmiş ve bir kısmı otomatik olarak korunuyor. Geriye en zor kısım kalıyor: bütün bunların bir yıl sonra da çalışması. Serinin son yazısında decision log'u keşfedilebilir tutmayı, "bu karar hâlâ geçerli mi?" tetikleyicilerini, hafif architecture review'u ve advice process'i konuşuyoruz.