Mert Özen Her Satırda Daha İleri
Backend .NET ile Mikroservis Uygulaması — #01

Bookstore'u Microservice'lere Bölmek: Hedef Mimari, Service Listesi ve Bu Serinin Kuralları

Mert Özen 14 Kas 2026 14 dk 3 görüntülenme
Bookstore'u Microservice'lere Bölmek: Hedef Mimari, Service Listesi ve Bu Serinin Kuralları

Bookstore'u beş service'e bölüyoruz: Catalog, Ordering, Inventory, Payments ve Notifications; her biri kendi veritabanına sahip, service'ler arasında kimlik ISBN ve Guid, paylaşılan kod yalnız contract'lar ve ServiceDefaults. Yazı hedef mimariyi, hangi kenarın sync hangisinin async olduğunu, Aspire, YARP, RabbitMQ, WolverineFx ve Keycloak kararlarının gerekçesini ve bu kuralı CI'da koruyan architecture test'ini gerçek çıktılarla gösteriyor.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ve xUnit.net v3 4.0 ile derlenip denenmiştir; seri boyunca kullanılan diğer sürümler (Aspire 13.6, RabbitMQ 4.3, WolverineFx 6, Keycloak 26.6, SQL Server 2025) kararlar tablosunda.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) bugün tek bir ASP.NET Core uygulaması. Sipariş oluşturan OrderService, kitapların fiyatını şu satırla okuyor:

IReadOnlyDictionary<int, BookInfo> books =
    await catalog.GetByIdsAsync([.. cart.Items.Select(i => i.BookId)], ct);

catalog burada aynı process'teki Catalog module'ünün interface'i; çağrı bir method çağrısı, veri aynı veritabanından geliyor. Ordering ve Catalog'u iki ayrı service'e ayırdığımızda bu satır neye dönüşmeli? Üç seçenek var: Ordering her siparişte Catalog'a HTTP ile sorar; Ordering'e Catalog veritabanının connection string'ini de veririz; ya da Ordering kitapların ihtiyaç duyduğu kısmını kendi veritabanında tutar. Bir de ikinci soru: BookId bir int, Catalog veritabanındaki identity kolonu. Service'ler ayrıldıktan sonra kitabı bu sayıyla tanımaya devam edebilir miyiz? Tahminini bir kenara yaz; cevap iletişim haritasında.

Bu Seri Ne Yapıyor ve Nereden Başlıyor?

Microservices serisinde service boundary'lerini, data ownership'i, iletişim stillerini, gateway'i, güvenliği ve monolith'ten çıkış yolunu kavram olarak konuştuk. Bu seri aynı kararları Bookstore üzerinde çalışan koda döküyor: Catalog, Ordering, Inventory, Payments ve Notifications service'leri, gRPC ile sync ve RabbitMQ ile async iletişim, outbox ve checkout saga'sı, YARP (Yet Another Reverse Proxy; Microsoft'un .NET için reverse proxy library'si) ile BFF (Backend for Frontend; belirli bir istemci için yazılmış ara backend), Keycloak, Angular, OpenTelemetry, testler ve Kubernetes. Son yazıda kurduğumuz sistemi distributed monolith belirtilerine karşı denetleyip "neyi tekrar birleştirirdik" sorusunu soracağız.

Seri, bir kavramın gerekçesini yeniden anlatmıyor. Saga'nın neden gerektiği Messaging ve Event-Driven Architecture serisinde, YARP'ın ayrıntıları API Gateway ve YARP serisinde, resilience pipeline'ları External Service Integration ve Resilience serisinde, Dockerfile temelleri Docker serisinde duruyor. Her yazı ilgili yazıyı adıyla anıyor ve yalnız "Bookstore'da nasıl birleşti" kısmını gösteriyor. Okumadan önce Microservices serisinin özellikle Service Boundary, Data Ownership, İletişim Stilleri ve API Gateway yazılarına bakmanı öneririm.

Bookstore'un referans uygulamasında Catalog, Ordering ve Inventory birer module: kendi klasörleri, kendi public interface'leri (ICatalogService, IInventoryService) ve kendi tabloları var. Modular Monolith serisinde bu ayrımı schema per module, architecture test'leri ve in-memory event bus ile sıkılaştırdık. Aynı serinin "Module'ü Service'e Çıkarmak" yazısı, bir module'ü ayırmanın geçerli sebeplerini ve dört adımlık çıkış yolunu anlattı. Bu seri o yolun sonundan başlıyor: module sınırları belli, veri zaten module başına ayrılmış, şimdi her birini ayrı process, ayrı veritabanı ve ayrı deploy birimi yapıyoruz.

Gerçek hayatta bu dönüşümü bir seferde yapmazsın; Microservices serisinin Strangler Fig yazısında anlattığımız gibi en kolay ve en az riskli module'den başlar, her adımda monolith'i biraz küçültürsün. Bu seride öğretme amacıyla bütün service'leri yan yana kuruyoruz, ama sırayı yine o yazının mantığıyla seçtik: veri sahipliği en net olan Catalog önce, en çok bağımlılığı olan checkout akışı en sonda.

Service Listesi ve Veri Sahipliği

Her service'in bir sahibi, bir veritabanı ve dışarıya açtığı bir contract'ı var. Ekip sütunu Mimariye Giriş serisinin Conway's Law yazısında planladığımız dört stream-aligned ekipten geliyor.

ServiceProjeSahip olduğu veriVeri deposuYayınladığı event'ler (plan)Ekip
CatalogBookstore.Catalog.ApiKitap, yazar, kategori, fiyatcatalogdb (SQL Server)BookPriceChanged, BookRenamedCatalog & Search
OrderingBookstore.Ordering.ApiSipariş, sipariş satırları, kitapların yerel kopyası, checkout saga state'iorderingdb (SQL Server)OrderPlaced, OrderConfirmed, OrderCancelledOrdering & Payment
InventoryBookstore.Inventory.GrpcStok, rezervasyoninventorydb (SQL Server)StockReserved, OutOfStockFulfillment
PaymentsBookstore.Payments.ApiÖdeme denemeleri, sağlayıcı referanslarıpaymentsdb (SQL Server)PaymentSucceeded, PaymentFailedOrdering & Payment
NotificationsBookstore.Notifications.ApiKalıcı veri yokRedis (bağlantı ve tekrar kontrolü)YokCustomer Experience

Tabloda iki kural gizli. Birincisi, bir veritabanının connection string'ini yalnız sahibi alıyor. Lokalde dört veritabanı tek bir SQL Server container'ında duruyor, çünkü dizüstü bilgisayarda dört SQL Server çalıştırmanın bir faydası yok; ama her service yalnız kendi veritabanını görüyor. Production'da bu ayrım fiziksel de olabilir, kullanıcı ve yetki seviyesinde de; Microservices serisinin Data Ownership yazısındaki "private tables, schema or database" seçeneklerinden en güçlüsünü seçiyoruz. İkincisi, event isimleri şimdilik plan. Contract'ları gerçekten yazdığımız yer 03 (Catalog) ve 06 (integration event'ler); isim değişirse orada değişecek.

Neyi Bilerek Service Yapmıyoruz?

Bookstore'da altı module var ama beş service var. Shipping, Fulfillment ekibinin Inventory service'i içinde ayrı bir module olarak kalıyor. Sebebi basit: kargo etiketi ve takip numarası stokla aynı ekibin işi, aynı anda değişiyor ve Shipping'i ayırmak için Modular Monolith serisinde saydığımız ölçülebilir sebeplerden (farklı ölçeklenme, farklı ekip, farklı release hızı) hiçbiri yok. "Her module bir service olmalı" düşüncesi, Microservices serisinde anlattığımız entity service anti-pattern'inin bir üst seviyedeki hâli. Bu seride Shipping'in kodunu yazmıyoruz; mesaj şu: service sayısı bir hedef değil, bir maliyet.

Identity de ayrı bir service değil. Kullanıcı, rol ve oturum işini Keycloak üstleniyor; 10. yazıda BFF bir OIDC (OpenID Connect; OAuth 2.0 üzerine kurulu kimlik katmanı) client'ı, diğer service'ler token doğrulayan resource server'lar olacak. Kurumsal Sipariş (Institutional) bu serinin kapsamında değil; aynı mimariye bir service daha eklemek, ilk beşini doğru kurduktan sonra mekanik bir iş.

İletişim Haritası: Hangi Kenar Sync, Hangisi Async?

Hedef mimarinin bütün kenarları aşağıdaki şekilde. Düz oklar cevabı beklenen çağrılar, kesikli oklar RabbitMQ üzerinden giden mesajlar.

01-target-architecture-tr.png

  • Angular yalnız Bookstore.Bff ile konuşuyor. BFF katalog ve sipariş isteklerini Catalog'a ve Ordering'e REST (Representational State Transfer; HTTP kaynakları ve method'ları üzerine kurulu API stili) ile iletiyor, kitap detay sayfası için Catalog ile Inventory'yi paralel çağırıp tek cevap üretiyor (09).
  • Ordering, sipariş sırasında stok rezervasyonu için Inventory'yi gRPC ile çağırıyor (05). Bunu bilerek senkron kuruyoruz ve Inventory yavaşladığında checkout'a ne olduğunu ölçüyoruz; 07'de rezervasyon checkout saga'sının mesajla ilerleyen bir adımına dönüşüyor. gRPC, hedef mimaride stok sorgusu için kalıyor.
  • Catalog fiyat ve isim değişikliklerini event olarak yayınlıyor; Ordering bunlarla kendi kitap kopyasını güncelliyor (06).
  • Checkout saga'sının sahibi Ordering. Inventory ve Payments ile command ve event alışverişi yapıyor, hata yolunda compensation çalıştırıyor (07). Payments dış ödeme sağlayıcısına HTTP ile gidiyor ve sonucu webhook ile alıyor (08).
  • Notifications hiçbir service'i çağırmıyor; event'leri dinleyip kullanıcıya SignalR ile push yapıyor ve e-posta gönderiyor (11).

Şimdi Önce Tahmin Et'teki sorunun cevabı. Ordering kitapların ihtiyaç duyduğu kısmını (ISBN, başlık, fiyat) kendi veritabanında tutuyor ve bu kopyayı Catalog'un event'leriyle güncelliyor. Her siparişte Catalog'a sormak, Catalog'u checkout'un çalışma şartı yapar: Catalog deploy edilirken ya da yavaşken sipariş de durur. Catalog veritabanına ikinci bir connection string vermek ise ayırdığımız şeyi geri birleştirmek demek; Catalog bir kolonun adını değiştirdiğinde Ordering production'da kırılır. Kopyanın bedeli eventual consistency: fiyat birkaç saniye eski olabilir. Bu yüzden siparişe yazılan fiyat, müşterinin sepette gördüğü fiyat; 04'te bunu sipariş anında donduruyoruz.

İkinci sorunun cevabı da hayır. BookId Catalog'un iç detayı: başka bir ortamda, bir veri taşımasından sonra ya da tablo yeniden oluşturulduğunda aynı kitap başka bir sayı alabilir. Service'ler arasında kitabın kimliği ISBN-13. Siparişin kimliği de veritabanının ürettiği bir sayı değil, Ordering'in ürettiği bir Guid (version 7, zamana göre sıralı); böylece sipariş daha veritabanına yazılmadan Inventory'ye ve Payments'a aynı kimlikle gidebiliyor. Contract'lara hiçbir service'in identity kolonu girmiyor.

Teknik Kararlar ve Alternatifleri

Her satır bir ADR (Architecture Decision Record; bir mimari kararın gerekçesini ve alternatiflerini kaydeden kısa doküman) adayı. Burada yalnız kararı ve tek cümlelik gerekçeyi yazıyoruz; ayrıntı ilgili yazıda.

KonuSeçimAlternatifNeden bu seçim?
Lokal ortamAspire 13.6 AppHostDocker ComposeService'ler, container'lar, bağlantılar ve dashboard tek C# dosyasından; service discovery ve OpenTelemetry ek ayar gerekmeden geliyor (02).
Dış APIREST + OpenAPIGraphQLTek istemci (Angular, BFF arkasında) ve basit okuma ihtiyacı; GraphQL'in esnekliği burada maliyetini karşılamıyor.
Service'ler arası sync çağrıgRPCREST.proto ile sıkı contract, HTTP/2 ve deadline propagation (05).
Async iletişimRabbitMQ 4.3 + WolverineFx 6Azure Service Bus, Kafka; MassTransitLokalde container olarak çalışan bir broker; outbox, inbox ve saga'yı tek bir MIT (ticari kullanıma izin veren serbest bir açık kaynak lisansı) lisanslı library'de veren bir messaging katmanı (06, 07).
Gateway ve BFFKendi projemiz içinde YARP 2.3Nginx, managed API gatewayBFF'de oturum, token ve aggregation kodu var; bunlar config dosyasına değil koda ait (09).
KimlikKeycloak 26.6Microsoft Entra ID, Duende IdentityServer, OpenIddictLokalde container, standart OIDC; buluta geçerken client tarafı değişmiyor (10).
ObservabilityOpenTelemetry + Aspire dashboardVendor SDK'larıVendor'dan bağımsız; aynı telemetri production'da başka bir backend'e gidiyor (12).
VeritabanıSQL Server 2025, service başına databasePostgreSQLBookstore'un ortak kuralı; veritabanı motoru bu serinin konusu değil.
DeployKubernetesAzure Container Apps, App ServiceKubernetes serisindeki altyapının üzerine; Aspire'ın Kubernetes publish'i hâlâ preview (14).

Messaging library seçimi bir paragrafı hak ediyor, çünkü yayındaki Message Queue'larla Çalışmak serisi MassTransit kullandı. MassTransit v9 Ocak 2026'da ticari lisansla çıktı; v8 açık kaynak kaldı ama sahibinin duyurusuna göre resmi bakımı 2026'nın sonunda bitiyor. 2027'de de yaşayacak bir referans uygulama için bu, ya lisans maliyeti ya da bakım dışı bir sürüm demek. Wolverine MIT lisanslı; RabbitMQ transport'u, Entity Framework Core (EF Core) ile transactional outbox'ı ve persist edilen saga'yı aynı pakette veriyor. Serinin planı yazılırken 5.x sürümü hedeflenmişti; bugün NuGet'teki son kararlı sürüm 6.64 ve 6.x .NET 10'u destekliyor, biz de 6.x kullanacağız. Mesajlaşma mekanizmalarının kendisi (outbox, idempotent consumer, retry) library'den bağımsız; Message Queue'larla Çalışmak serisinde anlatılanlar aynen geçerli.

Repo Yapısı: Mono-repo ve Central Package Management

Bütün service'ler tek bir Git reposunda duruyor. Mono-repo, service'leri birbirine bağlamaz; bağlayan şey ortak deploy, ortak veritabanı ve paylaşılan business kodudur. Tek repo bize atomik commit, tek arama ve tek CI tanımı veriyor; bağımsız deploy'u 14. yazıda path filter'larla, service başına ayrı pipeline'la sağlayacağız. İskeleti şu komutlarla kurduk:

mkdir bookstore
cd bookstore
dotnet new globaljson --sdk-version 10.0.100 --roll-forward latestFeature
dotnet new sln --name Bookstore
dotnet new buildprops
dotnet new packagesprops
dotnet new gitignore

dotnet new web -o src/Services/Catalog/Bookstore.Catalog.Api
dotnet new web -o src/Services/Ordering/Bookstore.Ordering.Api
dotnet new web -o src/Services/Inventory/Bookstore.Inventory.Grpc
dotnet new web -o src/Services/Payments/Bookstore.Payments.Api
dotnet new web -o src/Services/Notifications/Bookstore.Notifications.Api
dotnet new classlib -o src/BuildingBlocks/Bookstore.Contracts
dotnet sln add (Get-ChildItem -Recurse -Filter *.csproj)

Son satır PowerShell; bash'te dotnet sln add $(find . -name "*.csproj") aynı işi yapıyor. Test projesi (tests/Bookstore.ArchitectureTests) xUnit.net v3 paketini kullanan sıradan bir console projesi; global.json'daki "test": { "runner": "Microsoft.Testing.Platform" } satırı dotnet test'in onu Microsoft.Testing.Platform ile çalıştırmasını sağlıyor. .NET 10'da dotnet new sln varsayılan olarak .slnx formatında, okunabilir bir XML dosyası üretiyor. dotnet sln add projeyi klasör yoluna göre solution klasörüne koyduğu için her service kendi solution klasöründe duruyor:

<Solution>
  <Folder Name="/src/" />
  <Folder Name="/src/BuildingBlocks/">
    <Project Path="src/BuildingBlocks/Bookstore.Contracts/Bookstore.Contracts.csproj" />
  </Folder>
  <Folder Name="/src/Services/" />
  <Folder Name="/src/Services/Catalog/">
    <Project Path="src/Services/Catalog/Bookstore.Catalog.Api/Bookstore.Catalog.Api.csproj" />
  </Folder>
  <Folder Name="/src/Services/Inventory/">
    <Project Path="src/Services/Inventory/Bookstore.Inventory.Grpc/Bookstore.Inventory.Grpc.csproj" />
  </Folder>
  <Folder Name="/src/Services/Notifications/">
    <Project Path="src/Services/Notifications/Bookstore.Notifications.Api/Bookstore.Notifications.Api.csproj" />
  </Folder>
  <Folder Name="/src/Services/Ordering/">
    <Project Path="src/Services/Ordering/Bookstore.Ordering.Api/Bookstore.Ordering.Api.csproj" />
  </Folder>
  <Folder Name="/src/Services/Payments/">
    <Project Path="src/Services/Payments/Bookstore.Payments.Api/Bookstore.Payments.Api.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/Bookstore.ArchitectureTests/Bookstore.ArchitectureTests.csproj" />
  </Folder>
</Solution>

Ortak build ayarları Directory.Build.props'ta, paket sürümleri Directory.Packages.props'ta. Central Package Management (CPM) açıkken projeler PackageReference'a sürüm yazmıyor; sürüm tek yerde. Bu, service'lerin aynı paketi farklı sürümlerle kullanmasını engelliyor ve "Inventory hâlâ eski gRPC paketinde" sürprizini ortadan kaldırıyor:

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup Label="Tests">
    <PackageVersion Include="xunit.v3" Version="4.0.3" />
  </ItemGroup>
</Project>

Tek sürüm politikası bir tercih, kural değil. Bir service gerçekten farklı bir sürüme ihtiyaç duyarsa kendi .csproj'unda VersionOverride kullanabilir; bunu istisna olarak görüp code review'da gerekçesini sormak yeterli. Service'ler henüz boş: her biri tek bir endpoint dönen bir web projesi. 02'de hepsini Aspire ile ayağa kaldıracağız.

Paylaşılan Kod Politikası ve Onu Koruyan Test

Microservice projelerinde en sık görülen kayma şu: önce bir Bookstore.Common projesi açılıyor, içine birkaç extension method konuyor, sonra bir base entity, sonra bir "ortak" validation kuralı. Bir yıl sonra her service bu projeye bağlı ve bir değişiklik bütün service'lerin aynı anda deploy edilmesini gerektiriyor. Bu, Microservices serisinin Strangler Fig yazısında anlattığımız distributed monolith'in en sinsi yolu.

Bu seride paylaşılan kod iki projeyle sınırlı: Bookstore.Contracts (integration event'lerin ve mesajların tanımı) ve Bookstore.ServiceDefaults (02'de ekleyeceğimiz telemetri, health check ve resilience ayarları). Bir service başka bir service'in implementasyon projesine referans veremiyor; yalnız sahibinin yayınladığı .Contracts projelerine verebiliyor. Kuralı bir doküman yerine test olarak yazdık, böylece CI'da kırılıyor. Mimariye Giriş serisinin Architecture Erosion yazısında fitness function dediğimiz şeyin küçük bir örneği bu. Test, .csproj dosyalarını XML olarak okuyor:

public sealed record ProjectFile(string Name, string FullPath, string Folder)
{
    public IEnumerable<string> ProjectReferences =>
        XDocument.Load(FullPath).Descendants("ProjectReference")
            .Select(r => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(FullPath)!,
                r.Attribute("Include")!.Value.Replace('\\', Path.DirectorySeparatorChar))));

    public IEnumerable<string> PackageReferences =>
        XDocument.Load(FullPath).Descendants("PackageReference").Select(p => p.Attribute("Include")!.Value);

    public override string ToString() => Name;
}
public sealed class SharedCodeRules
{
    // Paylaşılan kod yalnız bu ikisi: "Common" ya da "Shared" adında bir business library yok
    private static readonly string[] AllowedBuildingBlocks = ["Bookstore.Contracts", "Bookstore.ServiceDefaults"];

    // Contracts projesi mesajları tanımlar; mantık ya da altyapı paketi taşımaz
    private static readonly string[] AllowedContractPackages = [];

    public static TheoryData<string> ServiceProjectNames() => [.. RepositoryLayout.ServiceProjects().Select(p => p.Name)];

    [Theory]
    [MemberData(nameof(ServiceProjectNames))]
    public void A_service_references_only_its_own_projects_building_blocks_and_contracts(string projectName)
    {
        ProjectFile project = RepositoryLayout.ServiceProjects().Single(p => p.Name == projectName);
        string buildingBlocks = RepositoryLayout.Src("BuildingBlocks") + Path.DirectorySeparatorChar;

        string[] violations =
        [
            .. project.ProjectReferences.Where(reference =>
                !reference.StartsWith(project.Folder + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&
                !reference.StartsWith(buildingBlocks, StringComparison.Ordinal) &&
                !Path.GetFileNameWithoutExtension(reference).EndsWith(".Contracts", StringComparison.Ordinal))
            .Select(Path.GetFileNameWithoutExtension)!
        ];

        Assert.True(violations.Length == 0,
            $"{project.Name} references another service's implementation: {string.Join(", ", violations)}");
    }

    [Fact]
    public void Building_blocks_contain_only_contracts_and_service_defaults()
    {
        string[] unexpected = [.. RepositoryLayout.BuildingBlocks().Select(p => p.Name).Except(AllowedBuildingBlocks)];

        Assert.True(unexpected.Length == 0, $"Unexpected shared projects: {string.Join(", ", unexpected)}");
    }

    [Fact]
    public void Contract_projects_have_no_logic_dependencies()
    {
        ProjectFile[] contracts =
        [
            .. RepositoryLayout.BuildingBlocks().Concat(RepositoryLayout.ServiceProjects())
                .Where(p => p.Name.EndsWith(".Contracts", StringComparison.Ordinal))
        ];

        foreach (ProjectFile contract in contracts)
        {
            Assert.Empty(contract.ProjectReferences);
            Assert.All(contract.PackageReferences, package => Assert.Contains(package, AllowedContractPackages));
        }
    }
}

RepositoryLayout test çıktı klasöründen yukarı çıkıp Bookstore.slnx'i bularak repo kökünü belirliyor ve src/Services/<Service>/ altındaki her .csproj'u service klasörüyle birlikte döndürüyor. Kuralı denemek için Ordering'e Catalog'a referans ekleyip testleri çalıştırdık:

dotnet add src/Services/Ordering/Bookstore.Ordering.Api reference src/Services/Catalog/Bookstore.Catalog.Api
dotnet test

failed Bookstore.ArchitectureTests.SharedCodeRules.A_service_references_only_its_own_projects_building_blocks_and_contracts(projectName: "Bookstore.Ordering.Api") (4ms)
  Bookstore.Ordering.Api references another service's implementation: Bookstore.Catalog.Api

Test run summary: Failed!
  total: 7
  failed: 1
  succeeded: 6
  skipped: 0
  duration: 296ms

Referansı geri aldığımızda yedi test de geçiyor. Yedi, çünkü [Theory] beş service projesi için ayrı ayrı çalışıyor ve iki [Fact] var. Yeni bir service eklendiğinde test kendiliğinden onu da kapsıyor; listeyi elle güncellemek gerekmiyor.

Testin göremediği şeyler de var. İki service aynı veritabanına bağlanıyorsa ya da bir service diğerinin tablosunu SQL ile okuyorsa .csproj'da iz bırakmaz. Bunu veritabanı kullanıcı yetkileriyle (her service yalnız kendi veritabanında yetkili) ve 02'de göreceğimiz "connection string'i yalnız sahibine ver" kuralıyla kapatıyoruz.

Üretimde Dikkat

Service sayısını değil, birlikte değişen dosyaları izle. Aynı pull request'te iki service'in kodu sık sık birlikte değişiyorsa ya contract fazla geniş ya da sınır yanlış yerde. Git geçmişinden bunu çıkarmak Mimariye Giriş serisinin Conway's Law yazısındaki analizle birkaç dakika sürüyor; ayda bir bakmak yeterli.

Contract'lara sürüm baştan girsin. Event'lerin adı, alanları ve gRPC paket adı (bookstore.inventory.v1) ilk günden versiyonlu olmalı. Breaking change gerektiğinde yeni bir sürümü eskisinin yanında yayınlamak, bütün consumer'ları aynı gün deploy etmekten her zaman ucuz.

Lokal ortamın maliyetini ölç. Beş service, dört container ve dashboard bir geliştirici bilgisayarında belleğin önemli bir kısmını kullanıyor. 15. yazıda bu sayıları modular monolith sürümüyle karşılaştıracağız; şimdiden not et ki "bilgisayarım yetmiyor" şikâyeti de mimarinin bir maliyeti.

Sık Düşülen Tuzaklar

Her module'ü service yapmak. Module'ler modular monolith'te ucuz, service'ler değil. Shipping örneğinde olduğu gibi ayırmak için ölçülebilir bir sebep yoksa module olarak kalsın.

Veritabanını en sona bırakmak. Kodu ayırıp veritabanını paylaşmaya devam etmek, iki deploy birimi ile tek bir şema demek: bir migration iki service'i birden kırar. Bu seride her service ilk gününden kendi veritabanıyla başlıyor.

Contract'a veritabanı kimliği koymak. BookId = 17 Catalog'un içinde anlamlı, dışında değil. ISBN ve Guid gibi doğal ya da üretilmiş kimlikler service sınırını geçebilir; identity kolonları geçmemeli.

"Ortak" projeyi küçük başlatıp büyümesine izin vermek. Yukarıdaki test bunun için var. Bir yardımcı method iki service'te lazımsa kopyalamak, onu paylaşılan bir projeye koymaktan çoğu zaman daha ucuz.

Küçük Bir Deneme

★☆☆ src/BuildingBlocks altına dotnet new classlib -o src/BuildingBlocks/Bookstore.Common ile bir proje ekle ve testleri çalıştır. Hangi test kırılıyor, mesajı ne diyor?

★★☆ Kendi çalıştığın bir monolith'in module'lerini yukarıdaki service tablosunun sütunlarıyla doldur: sahip olduğu veri, yayınlayacağı event'ler, ekip. Hangi satırda "ekip" sütunu boş kalıyor? O module'ü service yapmak için ölçülebilir bir sebebin var mı?

★★★ SharedCodeRules'a dördüncü bir kural ekle: bir projenin .csproj'unda VersionOverride varsa test bunu yalnız AllowedVersionOverrides listesindeki paketler için kabul etsin. Kuralı önce Catalog'a <PackageReference Include="xunit.v3" VersionOverride="4.0.2" /> ekleyerek kır, mesajı oku, sonra geri al. Bu kural CPM'in "tek sürüm" kararını neden tek başına korumadığını gösteriyor.

Kararlar yazıldı, repo iskeleti ve onu koruyan test hazır, ama service'ler henüz birbirinden habersiz birer web projesi. Bir sonraki yazıda Aspire AppHost ile SQL Server, Redis, RabbitMQ ve Keycloak container'larını, service'leri ve aralarındaki bağlantıları tek bir C# dosyasında tanımlayacak; ServiceDefaults ile telemetriyi ve health check'leri her service'e tek satırla ekleyecek, migration'ları ayrı bir process olarak çalıştırıp dashboard'da her şeyi yeşil göreceğiz.