Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Microservices — #03

Service Boundary'lerini Çizmek: Business Capability, Bounded Context ve Coupling Türleri

Mert Özen 8 Kas 2026 12 dk 4 görüntülenme
Service Boundary'lerini Çizmek: Business Capability, Bounded Context ve Coupling Türleri

Service sınırı tablolara ya da entity'lere göre değil, birlikte değişen iş kurallarına göre çizilir: bir service en fazla bir bounded context içerir ve tek bir ekibin sahipliğindedir. Business capability ile subdomain yaklaşımı, Microsoft'un sınır rehberi, Newman'ın domain, pass-through, common ve content coupling türleri, entity service anti-pattern'i ve Customer'ın üç context'teki üç ayrı modeli.

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

Önce Tahmin Et

Bookstore'un ilk service bölme toplantısında beyaz tahtaya şu liste yazılıyor: BookService, CustomerService, OrderService, StockService. Her biri bir tabloya karşılık geliyor ve dışarıya CRUD (Create, Read, Update, Delete) endpoint'leri açıyor. Kulağa düzenli geliyor: her şeyin bir yeri var.

Şimdi ilk gerçek iş isteği geliyor: "Okul açılışı kampanyasında, aynı müşteri kampanyalı kitaptan en fazla 3 adet alabilsin." Bu değişiklik bu dört service'ten kaçına dokunur? Checkout sırasında kaç network çağrısı yapılır? Ve bu kuralı kim, hangi service'te sahiplenir? Tahminini yaz; yazının sonunda aynı isteği capability'ye göre çizilmiş sınırlarla tekrar soracağız.

Business Capability mi, Subdomain mı?

Chris Richardson'ın microservices.io'daki pattern kataloğu service sınırı için iki ana yol veriyor. Decompose by business capability: işletmenin değer üretmek için yaptığı şeyleri (katalog yönetimi, sipariş yönetimi, stok yönetimi, teslimat) bulup her birini bir service adayı yapmak. Capability kavramı business architecture'dan geliyor; çoğu zaman bir business object'e karşılık geliyor ve iç içe hiyerarşiler kurabiliyor. Decompose by subdomain: DDD'nin (Domain-Driven Design) subdomain'lerini ve bounded context'lerini temel almak. Richardson ikisini birbirinin alternatifi olarak sunuyor; pratikte iyi yapılmış bir capability analizi ile iyi yapılmış bir subdomain analizi çoğu zaman aynı haritaya varıyor.

İkisinin ortak noktası, sınırı teknik rollere göre değil, işin kendisine göre çizmek. Bookstore'da "kitapları göster ve aranabilir kıl", "sipariş al ve yaşam döngüsünü yönet", "stoğu takip et ve ayır", "parayı tahsil et", "paketi gönder", "müşteriye haber ver" altı capability. Bunlar ilk yazıdaki altı adayla ve DDD serisinin "Subdomain'ler: Core, Supporting ve Generic Ayrımıyla Yatırımı Doğru Yere Yapmak" yazısındaki haritayla örtüşüyor. Fark, bakış açısında: capability "işletme ne yapıyor?" sorusundan, subdomain ise "bu işin hangi kısmı bizi farklı kılıyor, hangisi satın alınabilir?" sorusundan başlıyor. Bookstore'da Ordering'in kampanya fiyatlandırması core, Payment generic; bu ayrım hangi service'e ne kadar mühendislik yatırımı yapılacağını da söylüyor.

Bir Service, En Fazla Bir Bounded Context

Microsoft'un Azure Architecture Center'daki "Identify microservice boundaries" rehberi, domain modelinden service'e giden yolu dört adımda tarif ediyor. Önce bounded context'ten başla: bir microservice'in işlevselliği genel olarak birden fazla bounded context'e yayılmamalı; yayılıyorsa farklı domain model'leri karışıyordur ve domain analizine geri dönmek gerekir. Sonra aggregate'lere bak: iyi tasarlanmış bir aggregate, iyi bir service'in birçok özelliğini taşır; teknik değil iş gereksinimlerinden çıkar, yüksek fonksiyonel cohesion'a sahiptir, bir persistence sınırıdır ve diğerlerine gevşek bağlıdır. Üçüncü olarak domain service'ler de aday olabilir; birden fazla aggregate'i koordine eden bir iş akışı gibi. Son olarak non-functional gereksinimler (ekip büyüklüğü, ölçeklenme, availability, güvenlik) bir service'i bölmeye ya da birkaçını birleştirmeye karar verdirebilir.

Bu kuralın yönüne dikkat: bir bounded context birden fazla service'e bölünebilir, ama bir service birden fazla bounded context'i içine almamalı. DDD serisinin "Bounded Context: Aynı Kelimenin Farklı Anlamları ve Model Sınırları" yazısında Bookstore'daki "kitap" kelimesinin Catalog, Inventory, Ordering ve Shipping'de dört ayrı modele dönüştüğünü görmüştük. Catalog ile Inventory'yi tek bir "BookService"te birleştirmek, bu iki modeli tekrar tek bir class'a sıkıştırmanın service ölçeğindeki hâli. "Aggregate service adayıdır" fikrini de dikkatli okumak gerekiyor: aday olması, her aggregate'in ayrı service olması gerektiği anlamına gelmiyor. Ordering bounded context'inin içinde Order, Cart ve Promotion ayrı aggregate'ler, ama sürekli birlikte değişiyorlar ve aynı ekibin elindeler; üçünü ayrı service yapmak bu yazının sonraki bölümlerindeki bütün coupling sorunlarını davet eder.

Rehberin en pratik cümlesi sonda: emin değilsen daha kaba taneli (coarse-grained) service'lerle başla. Bir service'i ikiye bölmek, işlevselliği birkaç mevcut service arasında yeniden dağıtmaktan çok daha kolay.

"Customer"ın Üç Hâli

Açılıştaki CustomerService önerisi, bounded context ayrımının neden önemli olduğunu en iyi gösteren örnek. "Müşteri" kelimesi Bookstore'un her yerinde geçiyor, ama her context ondan başka bir şey istiyor. Ordering siparişi kime ve nereye göndereceğini bilmek istiyor. Payment kimden, hangi ülkenin kurallarıyla para alacağını ve ödeme sağlayıcısındaki karşılığını bilmek istiyor. Notification kime, hangi dilde ve hangi kanaldan yazacağını bilmek istiyor. Tek bir Customer tablosunu ve onu yöneten tek bir service'i bu üç ihtiyacın toplamı olarak kurarsan, her biri diğerlerinin field'larını taşır ve birinin değişikliği hepsini etkiler.

Bunun yerine hesap açıldığında bir integration event yayınlanıyor ve her context ondan yalnız kendi işine yarayanı alıp kendi modelini kuruyor:

namespace Bookstore.Accounts.Contracts;

// Hesap açılınca yayınlanan integration event: herkes yalnız işine yarayan kısmı alır
public sealed record CustomerRegistered(
    Guid CustomerId,
    string FullName,
    string Email,
    string PreferredLanguage,
    string Street,
    string City,
    string Country);
namespace Bookstore.Ordering
{
    // Ordering için müşteri: siparişi kime ve nereye göndereceğim?
    public sealed record Customer(Guid Id, string Name, Address DefaultShippingAddress);
    public sealed record Address(string Street, string City, string Country);
}

namespace Bookstore.Payments
{
    // Payments için müşteri: kimden, hangi ülkenin kurallarıyla para alacağım?
    public sealed record Payer(Guid CustomerId, string BillingCountry, string? ProviderCustomerRef);
}

namespace Bookstore.Notifications
{
    // Notification için müşteri: kime, hangi dilde, hangi kanaldan yazacağım?
    public sealed record Recipient(Guid CustomerId, string Email, string Language, bool SmsOptIn);
}
using System.Text.Json;
using Bookstore.Accounts.Contracts;
using Bookstore.Notifications;
using Bookstore.Ordering;
using Bookstore.Payments;

CustomerRegistered registered = new(
    Guid.Parse("0199f0c4-5c1e-7000-8000-0000000000ad"), "Ada Lovelace", "ada@example.com", "en",
    "1 Queen Street", "London", "GB");

// Her context aynı event'ten kendi modelini kurar ve kendi veritabanına yazar
Customer customer = new(registered.CustomerId, registered.FullName,
    new Address(registered.Street, registered.City, registered.Country));
Payer payer = new(registered.CustomerId, registered.Country, ProviderCustomerRef: null);
Recipient recipient = new(registered.CustomerId, registered.Email, registered.PreferredLanguage, SmsOptIn: false);

JsonSerializerOptions options = new(JsonSerializerDefaults.Web);
Console.WriteLine($"Ordering      : {JsonSerializer.Serialize(customer, options)}");
Console.WriteLine($"Payments      : {JsonSerializer.Serialize(payer, options)}");
Console.WriteLine($"Notifications : {JsonSerializer.Serialize(recipient, options)}");
Ordering      : {"id":"0199f0c4-5c1e-7000-8000-0000000000ad","name":"Ada Lovelace","defaultShippingAddress":{"street":"1 Queen Street","city":"London","country":"GB"}}
Payments      : {"customerId":"0199f0c4-5c1e-7000-8000-0000000000ad","billingCountry":"GB","providerCustomerRef":null}
Notifications : {"customerId":"0199f0c4-5c1e-7000-8000-0000000000ad","email":"ada@example.com","language":"en","smsOptIn":false}

Çıktıda üç ayrı model görünüyor ve hiçbiri diğerinin property'lerini taşımıyor. Notification adresi bilmiyor, Ordering e-postayı bilmiyor, Payment yalnız fatura ülkesini ve sağlayıcıdaki referansı tutuyor. Notification yarın SMS (Short Message Service) tercihine bir "sessiz saatler" ayarı eklemek isterse, bu değişiklik yalnız Notification'ın kodunda ve veritabanında oluyor. Ordering'in deploy edilmesi gerekmiyor. Bu, data minimization açısından da bir kazanç: her service yalnız ihtiyaç duyduğu kişisel veriyi tutuyor ve bir sızıntının etki alanı daralıyor.

Bir bedeli de var: aynı müşterinin adı üç yerde duruyor ve müşteri adını değiştirdiğinde üç kopyanın da güncellenmesi gerekiyor. Bu, eventual consistency'nin ilk görünüşü; hesap service'i bir CustomerDetailsChanged event'i yayınlar, her context kendi kopyasını günceller. Kopyaların nasıl tutulacağı ve ne kadar süre tutarsız kalabileceği bir sonraki yazının konusu.

Newman'ın Coupling Türleri

Sam Newman, Building Microservices'in ikinci baskısında (O'Reilly, 2021) service'ler arası coupling'i gevşekten sıkıya doğru dört türe ayırıyor. Bu sınıflandırma, bir sınırın iyi olup olmadığını konuşmak için ortak bir dil veriyor.

Domain coupling: bir service, başka bir service'in sunduğu işlevselliğe ihtiyaç duyduğu için onu çağırıyor. Ordering'in stok ayırmak için Inventory'ye gitmesi bunun örneği. Tamamen kaçınılmaz ve kabul edilebilir; yapılması gereken, paylaşılan bilgiyi gerektiği kadarla sınırlamak.

Pass-through coupling: bir service, kendisinin kullanmadığı bir veriyi yalnız başka bir service'e iletmek için alıyor. Bookstore'da Ordering'in kargo adresini Inventory'ye verdiğini, Inventory'nin de stoğu ayırdıktan sonra bu adresi Shipping'e taşıdığını düşün:

namespace Bookstore.Inventory.Before
{
    // Pass-through coupling: Inventory, kullanmadığı kargo bilgisini Shipping'e taşımak için alıyor
    public sealed record ShippingManifest(string RecipientName, string Street, string City, string Country, string Carrier);

    public interface IInventoryApi
    {
        Task ReserveAndForwardAsync(Guid orderId, IReadOnlyList<(string Isbn, int Quantity)> items, ShippingManifest manifest);
    }
}

namespace Bookstore.Inventory.After
{
    // Inventory yalnız stok ayırıyor; adresi Shipping, Ordering'in OrderPlaced event'inden alıyor
    public interface IInventoryApi
    {
        Task<Guid> ReserveAsync(Guid orderId, IReadOnlyList<(string Isbn, int Quantity)> items);
    }
}

İlk interface'te Inventory'nin ShippingManifest ile hiçbir işi yok, ama Shipping kargo firması için yeni bir field istediğinde (örneğin teslimat notu) Inventory'nin contract'ı da değişmek zorunda. İkinci interface'te Inventory yalnız kendi işini yapıyor; Shipping adresi Ordering'in yayınladığı OrderPlaced event'inden kendisi alıyor. Değişiklik artık Ordering ile Shipping arasında kalıyor ve araya giren üçüncü bir taraf yok.

Common coupling: birden fazla service aynı veriyi paylaşıyor; ortak bir veritabanı, ortak bir dosya sistemi ya da ortak bir cache. Shipping ve Notification'ın sipariş durumunu aynı Orders tablosundan okuması bunun örneği. Tablonun yapısı değiştiğinde hepsi etkileniyor. Newman'ın önerisi, ortak durumu tek bir service'in sahiplenmesi ve değişikliklerin yalnız onun üzerinden yapılması; bunun veri tarafını bir sonraki yazıda database per service olarak ele alıyoruz.

Content coupling: bir service, başka bir service'in içine uzanıp onun iç durumunu doğrudan değiştiriyor. Shipping'in kargo teslim edildiğinde Orders tablosundaki Status kolonunu doğrudan Delivered yapması bunun en tipik hâli. Ordering artık kendi siparişinin hangi durumda olduğunu kontrol etmiyor; iptal edilmiş bir sipariş "teslim edildi"ye dönebilir, çünkü bu kuralı uygulayan kod devre dışı kaldı. Bu, sınırın fiilen ortadan kalkması demek. Doğrusu, Shipping'in OrderDelivered event'ini yayınlaması ve Ordering'in kendi kuralına göre kendi durumunu değiştirmesi.

Bu dört türün yanında bir de temporal coupling var: iki service'in aynı anda ayakta olması gerekiyorsa aralarında zaman üzerinden bir bağ vardır. Bunu iletişim stillerini anlattığımız altıncı yazıda ele alıyoruz.

Entity Service Anti-Pattern'i

Açılıştaki liste, Michael Nygard'ın Aralık 2017'de yazdığı "The Entity Service Antipattern" yazısında anlattığı yapının ta kendisi: her biri bir business object üzerinde CRUD işlemleri sunan service'ler. Nygard'ın itirazı iki yönlü. Birincisi semantic coupling: bir iş kuralı neredeyse hiçbir zaman tek bir entity'ye ait değil; "kampanyalı kitaptan en fazla 3 adet" kuralı kitabı, müşteriyi, siparişi ve kampanyayı birlikte ilgilendiriyor ve bu kural hangi entity service'e konsa diğer üçünü çağırmak zorunda kalıyor. İkincisi operational coupling: entity service'ler neredeyse her isteğin yolunda olduğu için hem yük hem de kesinti her şeye yayılıyor; CustomerService yavaşsa sipariş, ödeme ve bildirim aynı anda yavaşlıyor.

Açılıştaki soruya dönelim. Entity service tasarımında kural için bir sahip bulmak zor: OrderService kuralı uygular, ama bunun için BookService'ten kitabın kampanyalı olup olmadığını, CustomerService'ten müşteriyi ve kendi tablosundan geçmiş siparişleri ister; checkout'a en az iki yeni network çağrısı eklenir ve kampanya bilgisinin nerede tutulacağı bir tartışmaya döner. Capability'ye göre çizilmiş sınırlarda cevap tek cümle: kampanya ve fiyatlandırma kuralları Ordering bounded context'inin core subdomain'i, kural Ordering'de yaşar, değişiklik tek bir service'e dokunur. Kitabın adı ve fiyatı gibi Catalog'dan gelen bilgiyi Ordering kendi kopyasında tutar.

Sınırı Doğrulamak

Bir sınır çizdikten sonra onu sınamanın en kullanışlı yolu birkaç soru sormak. Microsoft'un rehberi bir liste veriyor; Bookstore için en işe yarayan üçü şunlar.

Tek bir sorumluluğu var mı? Service'in ne yaptığını tek cümlede, "ve" kullanmadan söyleyebiliyorsan iyi bir işaret. "Inventory stoğu takip eder ve ayırır" tek bir iş yeteneği; "Customer service müşteriyi yönetir, adres doğrular ve e-posta gönderir" üç ayrı yetenek.

Bağımsız değişebiliyor mu? Son birkaç ayın değişiklik isteklerini al ve her birinin hangi service'lere dokunacağını işaretle. İsteklerin çoğu tek bir service'te kalıyorsa sınır doğru yerde. Sık sık iki service birlikte değişiyorsa ya aralarındaki contract fazla geniş ya da aslında tek bir service olmaları gerekiyor. Mimariye Giriş serisinin "Conway's Law: Ekip Yapısı Mimariyi Nasıl Şekillendirir?" yazısında commit geçmişinden birlikte değişen dosyaları bulmanın yolunu göstermiştik; aynı analiz service adaylarına da uygulanabilir.

Chatty çağrı var mı? Bir kullanıcı isteği iki service arasında çok sayıda gidip gelme gerektiriyorsa, rehberin ifadesiyle bu, iki işlevin aynı service'e ait olduğunun bir belirtisi olabilir. Sepet sayfasında her kitap için Catalog'a ayrı çağrı yapan bir Ordering, ya kopyaya ya da bir toplu endpoint'e ihtiyaç duyuyor.

Rehberin listesinde üç madde daha var: service küçük bir ekibin bağımsız çalışarak kurabileceği kadar olmalı, iki service'in birlikte deploy edilmesini gerektiren bir bağımlılık olmamalı ve sınırlar veri tutarlılığı sorunlarından kaçınacak şekilde çizilmeli. Sonuncusu, tutarlılık gerektiren kuralları tek bir service'te tutmak demek; beşinci yazının konusu.

Service Ownership: You Build It, You Run It

Bir sınırı korumanın en güçlü yolu teknik değil, organizasyonel: her service'in tek bir sahibi ekip olmalı. Amazon CTO'su (Chief Technology Officer) Werner Vogels'in 2006'da ACM (Association for Computing Machinery) dergisi Queue için Jim Gray'le yaptığı söyleşideki "you build it, you run it" cümlesi bu fikrin özeti oldu: service'i yazan ekip onu production'da da işletir, alarm geldiğinde o uyanır ve müşteriyle doğrudan temas eder. Lewis ve Fowler'ın makalesindeki "Products not Projects" maddesi de aynı yere çıkıyor.

Sahiplik, sınır kararını doğrudan etkiliyor. İki service'i iki ayrı ekibe veremiyorsan, ayrı service olmalarının ana gerekçesi zayıflıyor. Tersine, bir service'in birden fazla sahibi varsa kimse onun contract'ını korumaz; "ortak" service'ler zamanla herkesin bir şey eklediği, kimsenin bir şey çıkaramadığı yapılara dönüşür. Matthew Skelton ve Manuel Pais'in Team Topologies (IT Revolution, 2019) kitabındaki stream-aligned ekip kavramı burada doğrudan uygulanıyor: bir ekip bir değer akışının sahibidir ve sahip olduğu service'ler o akışın sınırlarıyla örtüşür. Kavramın kendisini Mimariye Giriş serisinin Conway's Law yazısında anlattık; burada etkisine bakalım.

Bookstore'un 12–18 aylık ekip hedefi dört stream-aligned ekip ve bir platform ekibi. Service'lerle mapping'i şöyle olur: Catalog & Search ekibi Catalog'un, Ordering & Payment ekibi Ordering ile Payment'ın, Fulfillment ekibi Inventory ile Shipping'in, Customer Experience ekibi Notification ile hesap tarafının sahibi. Bir ekibin birden fazla service'i olabilir, ama her service'in tek bir ekibi var. Bu mapping bir soruyu da gündeme getiriyor: Ordering ve Payment aynı ekipteyse ve DDD serisindeki context map'te aralarında bir Partnership ilişkisi varsa, ikisini başlangıçta ayrı service yapmak gerçekten gerekli mi? Cevap, Payment'ın ayrı deploy edilme ihtiyacına bağlı; ödeme sağlayıcısı entegrasyonu sık değişiyorsa ya da ayrı ölçeklenmesi gerekiyorsa evet, yoksa iki bounded context aynı service içinde iki ayrı module olarak başlayabilir. Bu, "bir service en fazla bir bounded context" kuralını esnetmek değil; daha kaba taneli başlamanın bilinçli bir hâli ve ileride bölmeyi kolay tutan bir karar.

Sık Düşülen Tuzaklar

Tabloları service yapmak. Veritabanı şemasındaki her tablo için bir service, entity service anti-pattern'inin ta kendisi. Sınır, verinin şekline değil, birlikte değişen kurallara göre çizilmeli.

Aynı kelimeyi tek modele zorlamak. "Customer", "Book", "Order" gibi kelimeler bütün sistemde geçiyor diye tek bir service'e toplanmaz; her context'in kendi modeli olur ve aralarındaki mapping açık bir contract'la yapılır.

Çok erken, çok ince bölmek. Domain'i henüz tanımıyorken çizilen ince sınırlar neredeyse her zaman yanlış yerden geçer. Kaba taneli başla, gerçek değişiklik verisi biriktikçe böl.

Sahipsiz service. "Platform ekibi bakar", "herkes katkı verir" denen service'ler, contract'ı en hızlı bozulan service'lerdir. Her service'in adı konmuş tek bir sahibi olmalı.

Ekip yapısını hesaba katmamak. Sınırı çizip ekipleri eski hâliyle bırakmak, sınırı organizasyonun iletişim yapısına karşı savaşa sokar. Conway's Law bu savaşı genelde kazanır.

Küçük Bir Deneme

★☆☆ Bu yazıdaki örneğe Bookstore.Shipping için dördüncü bir müşteri modeli ekle: Consignee. Kargo için müşteriden hangi bilgiler gerekiyor, hangileri gereksiz? CustomerRegistered event'i bu bilgileri taşıyor mu, taşımıyorsa ne yaparsın?

★★☆ Çalıştığın sistemdeki service'lerden birini seç ve Newman'ın dört coupling türüne göre dışa doğru bağımlılıklarını listele. Common ya da content coupling bulduğun her yer için, sahipliği tek bir service'e taşıyan bir düzeltme öner.

★★★ Bookstore için son altı ayda gelmiş olabilecek on değişiklik isteği yaz (kupon, kısmi iade, yeni kargo firması, SMS bildirimi…). Her isteğin hangi service'lere dokunduğunu iki tasarım için işaretle: açılıştaki entity service'ler ve capability'ye göre çizilmiş altı service. İki tasarımda kaç istek tek bir service'te kaldı?

Sınırları çizdik; sıradaki soru verinin kime ait olduğu. Bir sonraki yazıda database per service'i, shared database'in yarattığı gizli coupling'i ve bir service başka bir service'in verisine ihtiyaç duyduğunda kullanılabilecek üç yolu, Ordering'in kitap adını Catalog'a sormadan nasıl bildiği üzerinden ele alıyoruz.