Mikroservis Mimarisi & API Sözleşme Yönetimi 7 dk okuma 3 okunma

Microservices'te Schema Evolution ve Contract Testing

Mikroservislerde servisler arası iletişim sözleşmelere (contract) dayanır. Schema Evolution, bu sözleşmelerin geriye dönük uyumlu şekilde evrilmesini; Contract Testing ise tüketici ve sağlayıcı arasındaki uyumun otomatik testlerle doğrulanmasını sağlar. Bu yazıda, backward/forward compatibility, tolerant reader, Pact, Spring Cloud Contract ve .NET ekosistemindeki uygulamalar ele alınır.

Microservices'te Schema Evolution ve Contract Testing

Microservices'te Schema Evolution ve Contract Testing

Mikroservis mimarisinde, servisler birbirleriyle sözleşmeler (contracts) üzerinden konuşur. Bu sözleşmeler; REST API'ler, gRPC protobuf tanımları, message queue event'leri veya veritabanı şemaları olabilir. Servisler bağımsız olarak evrildiği için, bir servisin sözleşmesini değiştirmesi, onu tüketen diğer servisleri kırabilir. İşte Schema Evolution (Şema Evrimi) ve Contract Testing (Sözleşme Testi), bu sorunu yönetmek için kullanılan iki tamamlayıcı disiplindir.


1. Schema Evolution Nedir?

Schema Evolution, bir servisin veri şemasını (API yanıtı, event payload, veritabanı tablosu) mevcut tüketicileri bozmadan değiştirme sanatıdır. Amaç, servislerin bağımsız olarak evrilebilmesini sağlarken, sistem genelinde uyumluluğu korumaktır.

Temel Prensipler:

Prensip Açıklama
Backward Compatibility (Geriye Dönük Uyumluluk) Yeni şema, eski tüketiciler tarafından okunabilmeli.
Forward Compatibility (İleriye Dönük Uyumluluk) Eski şema, yeni tüketiciler tarafından okunabilmeli.
Full Compatibility (Tam Uyumluluk) Hem backward hem forward uyumluluk.
Additive Change (Ekleme Prensibi) Mevcut yapıya ekleme yap, çıkarma yapma.
Tolerant Reader (Hoşgörülü Okuyucu) Bilinmeyen alanları görmezden gel, eksik alanlara dayanıklı ol.

2. Uyumluluk Türleri

A. Backward Compatibility (Geriye Dönük Uyumlu)

Yeni şema, eski tüketiciler tarafından okunabilir.

json

// V1 (Eski)
{
  "id": 123,
  "name": "Laptop",
  "price": 1500
}

// V2 (Backward Compatible)
{
  "id": 123,
  "name": "Laptop",
  "price": 1500,
  "currency": "TRY"   // Yeni alan - eski tüketici görmezden gelir
}

Güvenli Değişiklikler:

  • Yeni opsiyonel alan ekleme

  • Yeni endpoint ekleme

  • Yeni enum değeri ekleme (tolerant reader varsa)

B. Forward Compatibility (İleriye Dönük Uyumlu)

Eski şema, yeni tüketiciler tarafından okunabilir.

json

// V1 (Eski şema)
{
  "id": 123,
  "name": "Laptop"
}

// V2 tüketicisi, currency alanı yoksa varsayılan değer kullanır

Güvenli Değişiklikler:

  • Opsiyonel alanı kaldırma (tüketici varsayılan değer kullanıyorsa)

  • Alanı zorunludan opsiyonele çevirme

C. Full Compatibility (Tam Uyumlu)

Hem eski hem yeni tüketiciler, hem eski hem yeni şemayı okuyabilir. En güvenli, ancak en kısıtlayıcı yaklaşımdır.


3. Breaking vs. Non-Breaking Değişiklikler

Değişiklik Breaking mi? Açıklama
Yeni opsiyonel alan ekleme ❌ Hayır Tolerant reader görmezden gelir
Yeni zorunlu alan ekleme (istek) ✅ Evet Eski istemciler bu alanı göndermez
Alan kaldırma (yanıt) ✅ Evet İstemciler null/undefined alır
Alan tipi değiştirme ✅ Evet int → string dönüşümü bozar
Alan adı değiştirme ✅ Evet İstemciler eski adı arar
Enum'a değer ekleme ⚠️ Duruma bağlı Tolerant reader varsa güvenli
Varsayılan değer değiştirme ✅ Evet Sessiz davranış değişikliği
HTTP durum kodu değiştirme ✅ Evet İstemci mantığını bozar

Altın Kural: Emin değilseniz, breaking olduğunu varsayın.


4. Schema Evolution Stratejileri

A. Additive Change (Ekleme Prensibi)

Mevcut yapıya ekleme yapın, çıkarma yapmayın.

json

// Adım 1: Yeni alanı ekle (opsiyonel)
{
  "id": 123,
  "name": "Laptop",
  "price": 1500,
  "currency": "TRY"  // Yeni
}

// Adım 2: Tüm tüketiciler yeni alanı kullanmaya başlasın (geçiş süresi)

// Adım 3: Eski alanı kaldır (breaking - yeni sürümde)

B. Expand/Contract Pattern (Genişlet/Sıkıştır)

Alan yeniden adlandırma veya tip değişikliği için kullanılır.

text

Adım 1 (Expand): Yeni alanı ekle, eski alanı koru
Adım 2 (Migrate): Kodu yeni alana geçir
Adım 3 (Contract): Eski alanı kaldır

Örnek: price → priceAmount + currency

json

// Adım 1: Her iki alanı da sun
{
  "price": 1500,
  "priceAmount": 1500,
  "currency": "TRY"
}

// Adım 2: Tüketiciler yeni alanlara geçer

// Adım 3: Eski alanı kaldır
{
  "priceAmount": 1500,
  "currency": "TRY"
}

C. Tolerant Reader Pattern

Tüketiciler, tanımadıkları alanları görmezden gelmeli ve eksik alanlara karşı varsayılan değer kullanmalıdır.

csharp

public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal? Price { get; set; }           // Nullable
    public string Currency { get; set; } = "TRY"; // Varsayılan
}

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

5. Message Queue'larda Schema Evolution

Event-driven mimarilerde, event şemaları da evrilir. Kafka, RabbitMQ gibi sistemlerde Schema Registry kullanmak standarttır.

A. Schema Registry (Confluent/Apicurio)

Schema Registry, tüm event şemalarını merkezi olarak saklar ve versiyonlar. Producer, event göndermeden önce şemasını registry'ye kaydeder; Consumer, registry'den şemayı çeker.

csharp

// Producer - Avro şeması ile event gönder
var schema = @"
{
  ""type"": ""record"",
  ""name"": ""OrderCreated"",
  ""fields"": [
    { ""name"": ""orderId"", ""type"": ""string"" },
    { ""name"": ""amount"", ""type"": ""double"" },
    { ""name"": ""currency"", ""type"": ""string"", ""default"": ""TRY"" }
  ]
}";

await producer.ProduceAsync("orders", new OrderCreated { ... });

B. Compatibility Modları

Mod Açıklama
BACKWARD Yeni şema, eski verilerle okunabilir (varsayılan)
FORWARD Eski şema, yeni verilerle okunabilir
FULL Hem backward hem forward
NONE Uyumluluk kontrolü yok (tehlikeli)

C. Protobuf ile Schema Evolution

Protobuf, alan numaraları ile uyumluluğu garanti eder.

protobuf

message OrderCreated {
  string order_id = 1;
  double amount = 2;
  string currency = 3;      // Yeni alan (opsiyonel)
  // Alan numaralarını asla yeniden kullanmayın!
  reserved 4, 5;            // Kaldırılan alanlar
}

Kurallar:

  • Alan numaralarını asla değiştirmeyin.

  • Kaldırılan alanların numaralarını reserved ile işaretleyin.

  • Yeni alanlar opsiyonel olmalı (varsayılan değer ile).


6. Contract Testing Nedir?

Contract Testing, bir tüketici (consumer) ile bir sağlayıcı (provider) arasındaki sözleşmenin (contract) her iki taraf tarafından karşılandığını doğrulayan test türüdür. Integration test'ten farkı, her iki tarafın da ayrı ayrı test edilmesi ve sözleşmenin merkezi olarak doğrulanmasıdır.

Klasik Integration Test Sorunu:

  • Tüm servisleri aynı anda ayağa kaldırmak gerekir.

  • Test ortamı karmaşıktır.

  • Yavaş ve kırılgandır.

Contract Testing Çözümü:

  • Tüketici, beklediği sözleşmeyi tanımlar (Pact dosyası).

  • Sağlayıcı, bu sözleşmeyi karşıladığını doğrular.

  • İki taraf da bağımsız test edilir.


7. Pact ile Contract Testing

Pact, en yaygın kullanılan consumer-driven contract testing aracıdır.

A. Tüketici Tarafı (Consumer Test)

Tüketici, sağlayıcıdan beklediği davranışı tanımlar.

csharp

public class OrderServiceConsumerTests
{
    private readonly IPactBuilderV3 _pact;

    [Fact]
    public async Task GetOrder_ReturnsOrderDetails()
    {
        // 1. Beklenen sözleşmeyi tanımla
        _pact
            .UponReceiving("Get order by ID")
            .Given("Order 123 exists")
            .WithRequest(HttpMethod.Get, "/api/orders/123")
            .WillRespond()
            .WithStatus(HttpStatusCode.OK)
            .WithJsonBody(new
            {
                id = 123,
                customerName = "Ahmet",
                total = 1500.00m,
                currency = "TRY"
            });

        // 2. Test'i çalıştır
        await _pact.VerifyAsync(async ctx =>
        {
            var client = new OrderServiceClient(ctx.MockServerUri);
            var order = await client.GetOrderAsync(123);

            Assert.Equal(123, order.Id);
            Assert.Equal("Ahmet", order.CustomerName);
            Assert.Equal(1500.00m, order.Total);
        });

        // 3. Pact dosyası oluşturulur: pacts/OrderService-OrderApi.json
    }
}

B. Sağlayıcı Tarafı (Provider Verification)

Sağlayıcı, tüketicinin tanımladığı sözleşmeyi karşıladığını doğrular.

csharp

public class OrderApiProviderTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task VerifyPactWithOrderService()
    {
        var factory = new WebApplicationFactory<Program>();

        await PactVerifier
            .ProviderVerifier()
            .ProviderState("Order 123 exists", setUp: async () =>
            {
                // Test verisini hazırla
                await SeedOrderAsync(123, "Ahmet", 1500.00m, "TRY");
            })
            .VerifyAsync("pacts/OrderService-OrderApi.json", factory.CreateClient());
    }
}

C. Pact Broker

Pact Broker, tüketici ve sağlayıcı arasındaki sözleşmeleri merkezi olarak saklar ve versiyonlar. CI/CD pipeline'ında "can I deploy?" sorusuna yanıt verir.

text

1. Consumer test çalışır → Pact dosyası broker'a yüklenir
2. Provider, broker'dan Pact dosyalarını çeker ve doğrular
3. Broker, sonucu kaydeder ve "webhook" ile ilgili taraflara bildirir

8. .NET Ekosisteminde Contract Testing Araçları

Araç Açıklama
Pact .NET Consumer-driven contract testing (en yaygın)
Pact Broker Merkezi sözleşme deposu
WireMock.NET HTTP mock server; provider simülasyonu
Mountebank Çoklu protokol mock server
SpecFlow BDD tarzı contract testing
Verify Snapshot testing ile API yanıtlarını doğrulama

WireMock.NET ile Basit Mock:

csharp

var server = WireMockServer.Start();
server
    .Given(Request.Create()
        .WithPath("/api/orders/123")
        .UsingGet())
    .RespondWith(Response.Create()
        .WithStatusCode(200)
        .WithBodyAsJson(new { id = 123, total = 1500.00m }));

var client = new HttpClient { BaseAddress = new Uri(server.Url) };
var response = await client.GetAsync("/api/orders/123");

9. Gerçek Dünya Senaryosu: Sipariş Servisi Evrimi

Senaryo: OrderService, OrderApi'ye sipariş oluşturuyor. OrderApi, amount alanını total olarak yeniden adlandırmak istiyor.

Problem: Doğrudan amount'u total yapmak, tüm tüketicileri bozar.

Çözüm: Expand/Contract Pattern + Contract Testing.

Adım 1 (Expand): OrderApi, her iki alanı da sunar.

json

{
  "id": 123,
  "amount": 1500.00,   // Eski
  "total": 1500.00     // Yeni
}

Adım 2 (Migrate): OrderService yeni alanı kullanmaya başlar. Contract test güncellenir.

Adım 3 (Contract): Tüm tüketiciler yeni alana geçtikten sonra amount kaldırılır.

Contract Test ile Doğrulama:

  • OrderService'in pact dosyası, total alanını beklediğini tanımlar.

  • OrderApi, bu pact'i doğrular.

  • Eğer OrderApi yanlışlıkla amount'u kaldırırsa, provider verification başarısız olur ve CI/CD pipeline'ı durur.


10. En İyi Pratikler

Pratik Açıklama
Additive Change Yapın Mevcut yapıya ekleme yapın, çıkarma yapmayın.
Tolerant Reader Olun Bilinmeyen alanları görmezden gelin, eksik alanlara varsayılan değer kullanın.
Expand/Contract Kullanın Yeniden adlandırma/tip değişikliği için 3 adımlı geçiş.
Schema Registry Kullanın Mesaj kuyruklarında merkezi şema yönetimi.
Contract Testing Yapın Her tüketici-sağlayıcı çifti için pact dosyası.
Pact Broker Kullanın Sözleşmeleri merkezi olarak saklayın ve versiyonlayın.
CI/CD'ye Entegre Edin Contract test'leri pipeline'a ekleyin; kırılırsa deploy'u durdurun.
Breaking Değişiklikleri Sürümleyin Kaçınılmaz breaking değişiklikler için yeni API sürümü.
Dokümante Edin Her şema değişikliğini ve uyumluluk modunu belgeleyin.
Monitoring Kurun Production'da şema uyumsuzluklarını izleyin.

Sonuç

Mikroservis mimarisinde, servisler bağımsız olarak evrilir; ancak bu evrim, tüketicileri bozmamalıdır. Schema Evolution, sözleşmelerin geriye dönük uyumlu şekilde değişmesini sağlar; Additive Change, Expand/Contract ve Tolerant Reader pattern'ları bu sürecin temel araçlarıdır. Contract Testing ise tüketici ve sağlayıcı arasındaki uyumun otomatik testlerle doğrulanmasını sağlar; Pact ve Pact Broker, bu disiplinin endüstri standardıdır. .NET ekosisteminde Pact .NET, WireMock.NET ve Verify gibi araçlarla bu süreçleri kolayca uygulayabilirsiniz. Unutmayın: Dağıtık sistemlerde, sözleşmeler bozulursa, sistem çöker. Schema Evolution ve Contract Testing, bu sözleşmeleri korumanın temelidir.

Tüm yazılar Soru ya da yorumun mu var? Yaz