Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Mimariye Giriş — #09

Mimariyi Zaman İçinde Korumak: Architecture Erosion ve Fitness Function'lar

Mert Özen 7 Kas 2026 13 dk 5 görüntülenme
Mimariyi Zaman İçinde Korumak: Architecture Erosion ve Fitness Function'lar

Mimari kararlar code review'da değil, her PR'da çalışan fitness function'larla korunur: ArchUnitNET ile kapalı layer kuralı, ratchet'li coupling eşiği ve sorgu sayısını sayan bir performans test'i, build'i geçen ama mimariyi bozan bir PR'ı yakalıyor. Erosion ve drift, fitness function türleri, GitHub Actions adımı ve kategori yol haritası.

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

Önce Tahmin Et

Kampanya haftasından önceki cuma, Bookstore'a iki küçük değişiklik içeren bir PR (pull request) geliyor. Pazarlama "en çok sipariş veren müşteriler" raporunu acil istiyor; raporu yazan arkadaş Business layer'ında yeni bir service açmak yerine endpoint'i doğrudan DbContext'e bağlıyor:

using Bookstore.Data;
using Microsoft.EntityFrameworkCore;

namespace Bookstore.Web;

// "Rapor acil" diye Business'ı atlayıp DbContext'e doğrudan giden endpoint
public static class ReportEndpoints
{
    public static void MapReports(this WebApplication app) =>
        app.MapGet("/reports/top-customers", async (BookstoreDbContext db) =>
            await db.Orders.GroupBy(o => o.CustomerId)
                .Select(g => new { CustomerId = g.Key, Orders = g.Count() })
                .OrderByDescending(x => x.Orders).Take(10).ToListAsync());
}

Aynı PR'da, "son siparişler" sayfasının toplamlarını hesaplayan method "daha okunur olsun" diye yeniden yazılmış:

using Bookstore.Data;
using Microsoft.EntityFrameworkCore;

namespace Bookstore.Business;

public sealed record OrderSummary(int OrderId, string CustomerId, int ItemCount, decimal Total);

public sealed class OrderSummaryService(BookstoreDbContext db)
{
    // PR: "toplamları C#'ta hesaplayalım, daha okunur"
    public async Task<List<OrderSummary>> GetRecentAsync(int take, CancellationToken ct = default)
    {
        List<Order> orders = await db.Orders.OrderByDescending(o => o.Id).Take(take).ToListAsync(ct);
        List<OrderSummary> result = [];
        foreach (Order order in orders)
        {
            List<OrderItem> items = await db.OrderItems.Where(i => i.OrderId == order.Id).ToListAsync(ct);
            result.Add(new OrderSummary(order.Id, order.CustomerId, items.Sum(i => i.Quantity), items.Sum(i => i.UnitPrice * i.Quantity)));
        }
        return result;
    }
}

Build geçiyor, mevcut unit test'ler geçiyor, sayfalar doğru sonucu gösteriyor. Code review'da iki kişi "LGTM (looks good to me)" yazmış. Bu PR mimariye aykırı bir şey içeriyor mu? İçeriyorsa kaç tane ve bunları hangi otomatik kontrol yakalayabilir? Bir de şunu düşün: bu PR'ın etkisi bugün değil, altı ay sonra nerede görünür?

Architecture Erosion ve Drift

Dewayne Perry ve Alexander Wolf, 1992'de yayımladıkları "Foundations for the Study of Software Architecture" makalesinde mimarinin zamanla bozulmasını iki kavramla ayırdı. Erosion, mimari kararların doğrudan ihlal edilmesi: kapalı olması gereken bir layer'ın atlanması, yasak bir dependency. Drift ise mimariye duyarsızlık: kararlar ihlal edilmiyor ama kimse onları bilmediği ya da önemsemediği için sistem yavaş yavaş başka bir yöne kayıyor. İkisi birbirini besliyor; drift'in açtığı belirsizlik, bir sonraki erosion'ı "nasılsa kural yok" diye kolaylaştırıyor.

Açılıştaki PR iki kategoriye de örnek. ReportEndpoints doğrudan bir erosion: beşinci yazıda Business'ı kapalı layer olarak tanımlamıştık, endpoint onu atlıyor. OrderSummaryService'teki değişiklik ise mimari bir kararı değil, bir characteristic'i bozuyor: tek sorgu olan sayfa artık 20 sipariş için 21 sorgu çalıştırıyor. Üçüncü yazıdaki gibi checkout ve arama için latency hedefleri koyduysan, bu tür bir değişiklik o hedefleri kampanya günü, yük altında sessizce yer.

Code review bunları neden kaçırıyor? Çünkü diff küçük ve her satır kendi içinde makul. Layer ihlali bir using satırında saklı; N+1 sorgu ise ancak çalışırken görünüyor. İnsan gözü niyeti değerlendirmede iyidir, ama yüzlerce PR boyunca aynı kuralı aynı titizlikle uygulamada kötüdür. Kuralları bir makinenin her PR'da kontrol etmesi gerekiyor.

Fitness Function: Characteristic'i Ölçen Test

Neal Ford, Rebecca Parsons ve Patrick Kua'nın Building Evolutionary Architectures kitabı (O'Reilly, 2017; Pramod Sadalage'ın katıldığı ikinci baskı 2022) bu ihtiyaca bir isim veriyor: architectural fitness function. Fitness function, bir ya da birkaç architecture characteristic'in hâlâ korunup korunmadığını objektif olarak değerlendiren her mekanizmadır. Bir unit test, bir architecture test, bir load test eşiği, production'daki bir monitoring alarmı, hatta düzenli yapılan bir manuel kontrol olabilir. Evolutionary architecture fikri de buna dayanıyor: mimari tek seferde doğru kurulmaz, küçük adımlarla değişir ve her adımda fitness function'lar yönün korunup korunmadığını söyler.

Kitap fitness function'ları birkaç eksende sınıflandırıyor. Bookstore'dan örneklerle en kullanışlı üçü:

EksenSeçeneklerBookstore örneği
KapsamAtomic: tek bir characteristic'i tek bağlamda ölçer. Holistic: birkaç characteristic'in birlikte davranışını ölçer.Atomic: "Web, Data'ya doğrudan bağımlı değil". Holistic: kampanya yükünde checkout p95 ve hata oranı birlikte.
Ne zaman çalışırTriggered: build, PR, deploy gibi bir event'le. Continuous: production'da sürekli.Triggered: PR'da architecture test. Continuous: checkout availability'sini izleyen alarm.
SonuçStatic: sabit bir eşik, geçti ya da kaldı. Dynamic: eşik bağlama göre değişir.Static: "en fazla 1 SQL (Structured Query Language) sorgusu". Dynamic: trafiğe göre ölçeklenen latency eşiği.

Bu yazıda triggered, atomic ve static olanlara odaklanıyoruz, çünkü CI (Continuous Integration) hattında her PR'da çalışabilen ve sonucu kesin olanlar onlar. Continuous ve holistic olanlar production'da yaşıyor; SLO (Service Level Objective) ve error budget gibi kavramlarla birlikte DevOps kategorisindeki SRE (Site Reliability Engineering) serisinin konusu.

Layer Kuralını Test'e Çevirmek: ArchUnitNET

ArchUnitNET, Java'daki ArchUnit'in .NET karşılığı: derlenmiş assembly'leri okuyup type'lar arasındaki dependency'lerden bir model kuruyor ve bu model üzerinde fluent bir dille kural yazmana izin veriyor. Test project'i xUnit v3 ve ArchUnitNET'in xUnit v3 entegrasyonuyla kuruyoruz:

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

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <OutputType>Exe</OutputType>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="4.0.2" />
    <PackageReference Include="TngTech.ArchUnitNET.xUnitV3" Version="0.13.5" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Bookstore.Web\Bookstore.Web.csproj" />
  </ItemGroup>

</Project>

xUnit v3'ün 4.x sürümü Microsoft.Testing.Platform üzerinde çalışıyor. .NET 10 SDK (Software Development Kit) ile dotnet test'in bu platformu kullanması için solution kökündeki global.json'a bir satır gerekiyor; bu satır olmadan dotnet test, VSTest hedefinin artık desteklenmediğini söyleyen bir hatayla duruyor:

{
  "sdk": { "version": "10.0.402", "rollForward": "latestFeature" },
  "test": { "runner": "Microsoft.Testing.Platform" }
}

Kurallar, beşinci yazıdaki Bookstore.Web → Bookstore.Business → Bookstore.Data yapısını kodluyor:

using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnitV3;
using Bookstore.Business;
using Bookstore.Data;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Bookstore.ArchitectureTests;

[Trait("Category", "Architecture")]
public sealed class LayerTests
{
    // Assembly'ler bir kez yüklenir, bütün kurallar aynı modeli kullanır
    private static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies(typeof(Program).Assembly, typeof(OrderSummaryService).Assembly, typeof(BookstoreDbContext).Assembly)
        .Build();

    private static readonly IObjectProvider<IType> Web =
        Types().That().ResideInAssembly(typeof(Program).Assembly).As("Web layer");
    private static readonly IObjectProvider<IType> Business =
        Types().That().ResideInAssembly(typeof(OrderSummaryService).Assembly).As("Business layer");
    private static readonly IObjectProvider<IType> Data =
        Types().That().ResideInAssembly(typeof(BookstoreDbContext).Assembly).As("Data layer");

    [Fact]
    public void Data_layer_does_not_depend_on_upper_layers() =>
        Types().That().Are(Data).Should().NotDependOnAny(Web).AndShould().NotDependOnAny(Business)
            .Because("Data is the bottom layer").Check(Architecture);

    [Fact]
    public void Web_layer_does_not_use_data_layer_directly() =>
        Types().That().Are(Web).Should().NotDependOnAny(Data)
            .Because("Business is a closed layer; Web goes through it").Check(Architecture);
}

Neden compiler yetmiyor? Project reference'lar döngüye izin vermediği için Data, Web'e referans veremez; ilk kural bu yapıda zaten compiler tarafından korunuyor, ama tek project'li bir layered uygulamada (klasör ve namespace'lerle ayrılmış) hiçbir koruma yok. İkinci kural ise compiler'ın hiç yakalamadığı bir şey: SDK tarzı project'lerde project reference'lar transitive, yani Web, Business üzerinden Data'yı da görüyor ve BookstoreDbContext'i doğrudan kullanabiliyor. Açılıştaki PR'ı bu test'lerle çalıştırınca:

failed Bookstore.ArchitectureTests.LayerTests.Web_layer_does_not_use_data_layer_directly (167ms)
  ArchUnitNET.xUnitV3.FailedArchRuleException : "Types that are Web layer should not depend on any Data layer because Business is a closed layer; Web goes through it" failed:
  	Bookstore.Web.ReportEndpoints does depend on "Bookstore.Data.BookstoreDbContext" and "Bookstore.Data.Order"

Mesaj kuralı, gerekçesini (Because) ve ihlal eden type'ı aynı yerde söylüyor. Gerekçeyi yazmak önemli: altı ay sonra test'i kırmızı gören kişi kuralı neden koyduğumuzu bilmezse, en kısa yol test'i silmek olur. Gerekçenin uzun hâli bir ADR'da (Architecture Decision Record) durmalı; test o kararın otomatik bekçisi. Bu bağlantıyı ilerideki Architecture Decision'lar serisinin "ADR'dan Otomatik Kontrole" yazısı ayrıntılı ele alıyor.

Bir de test'in kendisini test et. Denememizde bir şey dikkatimizi çekti: Program.cs top-level statement'larla AddDbContext<BookstoreDbContext> çağırıyor, yani Web'in composition root'u Data'ya bağımlı. Kural bu satırı hiç raporlamadı. Aynı çağrıyı adı olan bir class'ın method'una taşıdığımızda ise kural kırmızıya döndü. ArchUnitNET 0.13.5'in yüklediği modelde Program type'ı hiçbir Bookstore dependency'si olmadan görünüyordu. Bu örnekte sonuç işimize geliyor, çünkü composition root'un Data'yı tanıması meşru; ama bunu tesadüfen öğrenmek iyi değil. Her yeni kuralı en az bir kez bilinçli olarak ihlal edip kırmızıya düştüğünü gör, istisnaları da (composition root gibi) kuralın içinde açıkça yaz.

Coupling'i Ölçmek ve Ratchet

Her kural "sıfır ihlal" olarak başlayamaz. Bookstore'da Business layer'ı bugün EF (Entity Framework) Core'u doğrudan kullanıyor; ekip sorguları zamanla Data layer'ındaki query object'lerine taşımaya karar verdi, ama bunu bir sprintte yapamaz. Böyle durumlarda ratchet (cırcır) pattern'i işe yarıyor: bugünkü değeri tavan olarak yaz, sayı düşebilsin ama artamasın.

using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using Bookstore.Business;
using Bookstore.Data;

namespace Bookstore.ArchitectureTests;

[Trait("Category", "Architecture")]
public sealed class CouplingTests(ITestOutputHelper output)
{
    private static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies(typeof(Program).Assembly, typeof(OrderSummaryService).Assembly, typeof(BookstoreDbContext).Assembly)
        .Build();

    // Ratchet: bugünkü değer tavan; sayı düşebilir, artamaz
    private const int MaxBusinessTypesUsingEfCore = 1;

    [Fact]
    public void Business_types_using_ef_core_do_not_increase()
    {
        string[] offenders = Architecture.Types
            .Where(t => t.Namespace.FullName.StartsWith("Bookstore.Business"))
            .Where(t => t.Dependencies.Any(d => d.Target.FullName.StartsWith("Microsoft.EntityFrameworkCore")))
            .Select(t => t.Name)
            .Distinct()
            .Order()
            .ToArray();

        output.WriteLine($"Business types using EF Core: {offenders.Length} [{string.Join(", ", offenders)}]");
        Assert.True(offenders.Length <= MaxBusinessTypesUsingEfCore,
            $"{offenders.Length} Business types use EF Core (max {MaxBusinessTypesUsingEfCore}): {string.Join(", ", offenders)}");
    }
}
passed Bookstore.ArchitectureTests.CouplingTests.Business_types_using_ef_core_do_not_increase (150ms)
  Standard output
    Business types using EF Core: 1 [OrderSummaryService]

Bu bir coupling metriği: Business'tan EF Core'a giden efferent coupling'in (dışarıya bağımlılık) type sayısıyla ölçülmüş hâli. Aynı modelden afferent ve efferent coupling'i, instability'yi ve SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisindeki "Component Prensipleri" yazısının anlattığı main sequence uzaklığını da hesaplayabilirsin. Önemli olan sayıyı tek seferlik bir rapor olarak değil, her PR'da kontrol edilen bir eşik olarak kullanmak. Ratchet'in tek kuralı var: biri sayıyı düşürdüğünde sabiti de düşür. Yoksa kazanılan alan bir sonraki acil işte geri kaybedilir.

Performansı Fitness Function'a Çevirmek

CI'da "bu endpoint 200 ms'nin altında" diye bir test yazmak cazip ama güvenilmez: paylaşılan CI makinesinin yükü, ısınma süresi ve gürültü test'i rastgele kırar, ekip de birkaç hafta sonra test'i görmezden gelmeyi öğrenir. Deterministik bir vekil ölçü daha iyi çalışır. Bookstore'un "son siparişler" sayfası için bu vekil, çalışan SQL sorgusu sayısı. EF Core'un interceptor'ı her komutu sayıyor, SQLite in-memory veritabanı da test'i hızlı ve bağımsız tutuyor:

using System.Data.Common;
using Bookstore.Business;
using Bookstore.Data;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Bookstore.ArchitectureTests;

[Trait("Category", "Fitness")]
public sealed class QueryBudgetTests
{
    [Fact]
    public async Task Recent_orders_page_uses_a_single_query()
    {
        await using SqliteConnection connection = new("DataSource=:memory:");
        await connection.OpenAsync(TestContext.Current.CancellationToken);
        CommandCounter counter = new();
        DbContextOptions<BookstoreDbContext> options = new DbContextOptionsBuilder<BookstoreDbContext>()
            .UseSqlite(connection).AddInterceptors(counter).Options;

        await using (BookstoreDbContext seed = new(options))
        {
            await seed.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken);
            seed.Orders.AddRange(Enumerable.Range(1, 20).Select(n => new Order
            {
                CustomerId = $"customer-{n % 7}",
                Items = [new() { Isbn = "978-0441013593", UnitPrice = 12.50m, Quantity = 1 },
                         new() { Isbn = "978-0141439518", UnitPrice = 6.00m, Quantity = 2 }]
            }));
            await seed.SaveChangesAsync(TestContext.Current.CancellationToken);
        }

        await using BookstoreDbContext db = new(options);
        counter.Reset();
        List<OrderSummary> page = await new OrderSummaryService(db).GetRecentAsync(20, TestContext.Current.CancellationToken);

        Assert.Equal(20, page.Count);
        Assert.True(counter.Count <= 1, $"Recent orders page ran {counter.Count} SQL commands (budget: 1)");
    }

    // Çalışan her DbCommand'ı sayar
    private sealed class CommandCounter : DbCommandInterceptor
    {
        public int Count { get; private set; }
        public void Reset() => Count = 0;

        public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
            DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
        {
            Count++;
            return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
        }
    }
}

Asıl implementation, toplamları veritabanında hesaplayan tek bir projection sorgusu:

// Tek sorgu: projection ile toplamlar veritabanında hesaplanır
public Task<List<OrderSummary>> GetRecentAsync(int take, CancellationToken ct = default) =>
    db.Orders
        .OrderByDescending(o => o.Id)
        .Take(take)
        .Select(o => new OrderSummary(o.Id, o.CustomerId, o.Items.Sum(i => i.Quantity), o.Items.Sum(i => i.UnitPrice * i.Quantity)))
        .ToListAsync(ct);

Açılıştaki PR'daki döngülü sürümle test şunu söylüyor:

failed Bookstore.ArchitectureTests.QueryBudgetTests.Recent_orders_page_uses_a_single_query (462ms)
  Recent orders page ran 21 SQL commands (budget: 1)

21 sorgu: siparişler için bir, her sipariş için bir tane daha. EF Core serisindeki "EF Core'da Performans Temelleri: N+1, Projection, AsNoTracking ve Compiled Query" yazısının anlattığı klasik N+1. Test ne kadar sürdüğünü değil, kaç kez veritabanına gittiğini ölçtüğü için her makinede aynı sonucu veriyor. Gerçek latency'yi ölçen holistic fitness function'lar yine gerekli; onlar üçüncü yazıdaki p95 senaryoları gibi, production'a yakın bir yük altında çalışmalı.

CI Pipeline'ına Eklemek

Bütün fitness function'lar aynı test project'inde, trait ile kategorilere ayrılmış durumda. Microsoft.Testing.Platform ile çalışan dotnet test, xUnit v3'ün --filter-trait seçeneğini doğrudan kabul ediyor:

dotnet test --no-build --filter-trait "Category=Architecture"
Test run summary: Passed!
  total: 3
  failed: 0
  succeeded: 3
  skipped: 0

Bookstore'un GitHub Actions workflow'una eklenecek adımlar şöyle. Architecture test'lerini ayrı bir adımda çalıştırmak, PR'daki kırmızı işaretin "bir test kırıldı" değil "mimari bir karar ihlal edildi" diye okunmasını sağlıyor:

name: ci
on:
  pull_request:
  push:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          global-json-file: global.json
      - run: dotnet build -c Debug
      # Mimari kararlar: hızlı, deterministik, her PR'da
      - name: Architecture fitness functions
        run: dotnet test --no-build --filter-trait "Category=Architecture"
      - name: All tests
        run: dotnet test --no-build

Workflow'daki komutları yerel olarak çalıştırdık; GitHub'daki koşuyu bu yazı için denemedik. ArchUnitNET derlenmiş binary'leri okuduğu için test'leri build çıktısının üzerinde çalıştırmak gerekiyor. Dependency Rule'u Clean Architecture'a özgü kurallarla korumanın ayrıntısı, ilerideki Clean, Onion ve Hexagonal Architecture serisinin "Clean Architecture'ı Test Etmek ve Korumak" yazısında; module sınırlarını korumak ise Modular Monolith serisinin konusu.

Sık Düşülen Tuzaklar

Kuralı hiç kırmızıya düşürmeden güvenmek. Top-level Program örneğinde gördüğümüz gibi araç bazı dependency'leri görmeyebilir ya da kural yanlış seçiciyle yazılmış olabilir. Her kural için bilinçli bir ihlal dene.

Gerekçesiz kural. Because olmayan ya da hiçbir karara bağlanmayan kural, ilk can sıkıcı kırmızıda silinir. Kural, bir ADR'a ya da en azından bir cümlelik gerekçeye bağlı olmalı.

Milisaniye eşikleriyle CI'ı kırmak. Gürültülü ölçümler "flaky" test üretir ve ekip kırmızıyı görmezden gelmeyi öğrenir. CI'da deterministik vekiller (sorgu sayısı, allocation sayısı, payload boyutu) kullan; zamanı production'a yakın ortamda ölç.

Çok fazla, çok ayrıntılı kural. Her class adını, her klasörü test eden yüzlerce kural refactoring'i cezalandırır. Kural, değiştirmesi pahalı kararları korumalı: layer yönü, module sınırı, dış dünyaya açılan dependency'ler.

Ratchet'i hiç sıkmamak. Tavan düşmüyorsa ratchet yalnız bir "şimdilik" listesidir. Sayı her düştüğünde sabiti de güncelle.

Kırmızı test'i Skip ile susturmak. Kural artık geçerli değilse kararı değiştir, ADR'ı güncelle ve test'i bilinçli olarak sil. Atlanan test, kimsenin sahiplenmediği bir karardır.

Kategori Yol Haritası: Hangi Soruyla Hangi Seriye?

Mimariye Giriş serisi burada bitiyor. Dokuz yazı boyunca mimarinin ne olduğunu, characteristic'leri nasıl tanımlayıp ölçeceğini, trade-off'ları, ilk stilleri, ekip yapısını ve kararları korumayı konuştuk. Kategorinin geri kalanı, bu serinin açtığı soruların her birini ayrı ayrı derinleştiriyor:

  • "Bu kararı neden verdik, nasıl yazıya dökerim?" → Architecture Decision'lar serisi (ADR, trade-off analizi, C4 (Context, Container, Component, Code) diyagramları ve arc42).
  • "Layered yapıda dependency yönü beni tıkıyor." → Clean, Onion ve Hexagonal Architecture serisi.
  • "Kodu layer'a değil feature'a göre organize etmek istiyorum." → Vertical Slice Architecture serisi.
  • "Monolith'i module'lere nasıl bölerim, sınırları nasıl korurum?" → Modular Monolith serisi.
  • "Domain'i nasıl modellerim, sınırlar nereden geçmeli?" → Domain-Driven Design serisi.
  • "Okuma ve yazma tarafları farklı ihtiyaçlara sahip." → CQRS (Command Query Responsibility Segregation) ve Event Sourcing serisi.
  • "Parçalar birbirini beklemeden konuşsun." → Messaging ve Event-Driven Architecture serisi.
  • "Service'lere bölmenin zamanı geldi mi, nasıl bölerim?" → Microservices serisi.
  • "Ağ, saat, replication ve consistency neden bu kadar zor?" → Distributed Systems Temelleri serisi.
  • "Bu yükü nasıl karşılarım, kapasiteyi nasıl hesaplarım?" → System Design serisi.
  • "Angular tarafında MVC (Model-View-Controller) mi, MVVM (Model-View-ViewModel) mi, state nerede durmalı?" → UI (User Interface) Mimari Desenleri serisi.

Küçük Bir Deneme

★☆☆ Bu yazıdaki LayerTests'i kendi layered projene uyarla ve çalıştır. Kaç ihlal çıktı? Her biri için "bu bir karar mı, bir kaza mı?" sorusunu sor.

★★☆ Üçüncü yazıdaki modifiability senaryosunu test'e çevir: altıncı yazının modular monolith örneğine bir Payments module'ü ekle ve "Payments, Ordering'e bağımlı olamaz" kuralını ArchUnitNET ile yaz. Sonra bilinçli bir ihlal ekleyip kuralın kırmızıya düştüğünü gör.

★★★ QueryBudgetTests'e ikinci bir fitness function ekle: aynı sayfanın veritabanından okuduğu satır sayısını ya da döndürdüğü JSON (JavaScript Object Notation) payload'ının boyutunu sınırlasın. Sonra composition root'u kurala açık bir istisna olarak ekleyecek şekilde LayerTests'i yeniden yaz ve AddDbContext çağrısını bir extension method'a taşıyarak kuralın bu kez onu gördüğünü doğrula.

Seri burada bitiyor, ama Bookstore'un hikâyesi devam ediyor. Kategoride bir sonraki durak, bu yazıda test'e çevirdiğimiz kararların kendisini kayda geçirmek: Architecture Decision'lar serisinde ADR yazmayı, seçenekleri senaryolarla karşılaştırmayı ve kararları fitness function'larla birlikte yaşatmayı ele alacağız.