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ı
reservedile 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ı,totalalanını beklediğini tanımlar. -
OrderApi, bu pact'i doğrular. -
Eğer
OrderApiyanlışlıklaamount'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.