Mimari ve Dağıtık Sistemler
Production'ı Bozmadan Breaking Change: Event Upcasting ve Schema Evolution
Breaking Changes Without Breaking Production: Event Upcasting and Schema Evolution
Event sourcing'de geçmiş değiştirilemez; şema değişikliklerini eski event'leri okurken dönüştüren upcaster'lar ve disiplinli versiyonlamayla yönetirsiniz, 2026'da yaygınlaşan Dynamic Consistency Boundary yaklaşımında da bu kural değişmiyor.
Event sourcing'de geçmiş yeniden yazılmaz; kırıcı şema değişikliği, eski event'i okuma anında güncel şemaya çeviren versiyonlu upcaster'larla yönetilir.
Axon, Marten ve saha çalışmaları upcasting'i varsayılan yol sayıyor; kopyala-dönüştür yanlıları uzun zincirlerde ve yasal silmede istisna istiyor.
Zihinsel model
Event deposu noterdeki tutanak defteridir: sayfayı yırtmazsın, okurken güncel dile çeviren bir tercüman eklersin.
Neden
- Denetim izi, replay ve projeksiyon yeniden kurulumu event'lerin değişmezliğine dayanır; depodaki veriyi değiştirmek bunları bozar.
- Upcaster zinciri her eski biçimi tek güncel biçime çevirir; uygulama kodu yalnızca son şemayı bilir.
- Adı aynı kalıp anlamı değişen alan sessizce yanlış hesap üretir; versiyon numarası bu değişikliği görünür kılar.
Kanıt
Sanayiden 19 event sourced sistem üzerine yapılan saha çalışması, ekiplerin versiyonlu event, upcasting ve kopyala-dönüştür arasında seçim yaptığını gösteriyor.
An Empirical Characterization of Event Sourced Systems and Their Schema Evolution (arXiv), 2021Axon Framework 5 yeni tutarlılık modeline geçerken upcaster'ları geçici kaldırdı, sonra 'message transformation' adıyla geri getirdi.
Axon Framework 5 Reference: Event Versioning, 2025Marten upcast'i ham JSON üzerinde yaparak eski event sınıflarını kod tabanında tutma zorunluluğunu kaldırıyor.
Marten Docs: Events Versioning, 2026Karşı görüş
Zincir uzadıkça okuma gecikmesi ve bakım yükü büyür. Kişisel veri silinmesi gerektiğinde upcasting yetmez; kopyala-dönüştür ya da crypto-shredding gerekir.
Kavramlar
Pratikte
- Event sınıfını sözleşme say; IDE ile rename yerine yeni versiyon ve upcaster yaz.
- Her event versiyonu için gerçek fixture sakla, CI'da zincirin tamamını test et.
- Servisler arası integration event'leri iç domain event'lerinden ayır, schema registry ile uyumluluk kuralı koy.
Event sourcing'de sistemin durumu, yalnızca eklenen (append-only) bir event akışından türetilir; dün yazılmış bir event yıllar sonra da yeniden okunur. Bu yüzden 'alanın adını değiştirelim' gibi sıradan bir değişiklik, ilişkisel veritabanındaki bir migration'dan çok daha zor bir soruya dönüşür: milyonlarca eski event'i ne yapacağız? En yaygın yanıt upcasting: eski event'i depoda olduğu gibi bırakıp okuma anında güncel şemaya çevirmek. Ekim 2026 itibarıyla araç ekosistemi de hareketli: Axon Framework 5 serisi aggregate yerine Dynamic Consistency Boundary (DCB) modelini öne çıkarıyor ve upcasting'i 'message transformation' adıyla yeniden kurdu, Marten 9.x ham JSON üzerinde upcast'i destekliyor. Mikroservislerin event'leri birbirine yayınladığı ve AI ajanlarının kodu hızla değiştirdiği bir ortamda, event şemasındaki kırıcı değişiklikler üretim kesintilerinin sessiz kaynağı olmaya devam ediyor.
Çıkarımlar
- Event sınıfı bir sözleşmedir, domain nesnesi değil; IDE ile rename etmek geçmişi bozar.
- Alan adı aynı kalıp anlamı değişiyorsa (KDV dahil/hariç gibi) bu kırıcı değişikliktir; yeni alan ya da yeni versiyon gerekir.
- Upcaster saf ve deterministik olmalı, dış servise gitmemeli; replay her seferinde aynı sonucu vermeli.
- Her event versiyonu için gerçek örnek fixture saklayın ve CI'da zincirin tamamını test edin.
- Servisler arası yayınlanan event'ler API'dir; iç domain event'i ile dış integration event'ini ayırın.
- DCB'ye geçen ekipler için etiketler de şemanın parçasıdır; etiket adını değiştirmek tutarlılık sorgularını sessizce bozabilir.
Alt başlıklar
Event sourcing'de şema değişikliği neden zor
İlişkisel bir tabloda `customer_name` kolonunu `full_name` yapmak için bir migration yazıp tüm satırları güncellersiniz; eski şema bir daha görünmez. Event sourcing'de durum farklı. Event'ler değişmez (immutable) kabul edilir, çünkü denetim izi, yeniden oynatma (replay) ve projeksiyon yeniden üretimi bu değişmezliğe dayanır. 2022'de yazılmış bir `OrderPlaced` event'i, 2026'da yeni bir okuma modeli kurarken yeniden okunacak.
Sonuç: kodunuz aynı event tipinin birden fazla tarihsel biçimiyle karşılaşır. Aggregate'i yeniden kurarken v1, v2 ve v3 biçimleri aynı akışta yan yana durabilir. Bunu yönetmezseniz deserializasyon hataları ya da daha kötüsü, sessizce `null` gelen alanlar yüzünden yanlış hesaplanan bakiyeler alırsınız. Anti-pattern: event sınıfını doğrudan domain nesnesi olarak kullanıp üzerinde refactor yapmak; IDE'nin 'rename' tuşu eski event'leri bozar.
Ekim 2026 itibarıyla durum
Son on iki ayda event sourcing araçlarında en büyük hareket, tutarlılık sınırının yeniden tanımlanması oldu. Axon Framework 5.0, Ekim 2025'te Dynamic Consistency Boundary (DCB) desteğiyle çıktı; DCB'de event'ler tek bir stream'e değil etiketlere bağlanıyor ve her işlem kendi tutarlılık kapsamını bir sorguyla tanımlıyor. Axoniq Mayıs 2026'da 'aggregate'in devri bitti' iddiasını açıkça savundu, Ağustos 2026'daki Axon Server 2026.1.0 DCB bağlamları için çok katmanlı depolama getirdi, 6 Ekim 2026'daki Axon Framework 5.4.0 ise event sourcing üzerine kurulu dayanıklı workflow'larla Saga'lardan çıkış yolu sundu. dcb.events topluluk listesinde artık Java, C#, Go, Python ve Rust dahil birçok dilde DCB uygulaması var.
Şema evrimi açısından önemli ayrıntı: Axon 5.0'da upcaster'lar geçici olarak kaldırılmıştı; dokümantasyon bunların 5.2 ile döneceğini söylüyordu ve 5.2 sürüm notları 'message transformation / upcasting' çalışmasını içeriyor. Marten 9.x dokümantasyonu CLR tipleri ya da ham JSON üzerinden upcast'i anlatıyor. Kleppmann ve Riccomini'nin Designing Data-Intensive Applications kitabının Şubat 2026'da çıkan ikinci baskısı, şema evrimi ve kodlama konusunda güncel temel referans.
Kırıcı ve kırıcı olmayan değişiklikler
Her değişiklik eşit değil. Kırıcı olmayan (non-breaking) değişiklikler: varsayılan değeri olan opsiyonel bir alan eklemek, okuyucuların görmezden gelebileceği yeni bir alan eklemek. Kırıcı (breaking) değişiklikler: alan adını değiştirmek, alanın tipini değiştirmek (string fiyatı integer kuruşa çevirmek gibi), bir alanı ikiye bölmek (`address` yerine `street` ve `city`), bir alanın anlamını değiştirmek ve event'i yeniden adlandırmak.
En sinsi olan anlam değişikliğidir. `amount` alanı v1'de KDV dahil, v2'de KDV hariç tutuluyorsa şema aynı görünür ama her eski event yanlış yorumlanır. Pratik kural: alan adı aynı kalıp anlamı değişiyorsa bunu kırıcı değişiklik sayın ve ya yeni bir alan adı kullanın ya da versiyon numarasını artırıp upcaster yazın. Kırıcı olmayan değişiklikler için de 'tolerant reader' (hoşgörülü okuyucu) ilkesini uygulayın: bilinmeyen alanları yok sayın, eksik alanlara açık varsayılan verin.
Upcasting nasıl çalışır
Upcaster, eski versiyondaki bir event'i bir sonraki versiyona çeviren saf bir fonksiyondur. Depodaki veri değişmez; dönüşüm event okunurken, deserializasyondan önce veya hemen sonra uygulanır. Zincir halinde çalışır: v1 → v2 → v3. Böylece uygulama kodu yalnızca en güncel biçimi bilir.
Örnek: `PriceChanged` v1'de `price: "129.90"` string olarak, v2'de `priceMinor: 12990` ve `currency: "TRY"` olarak tutuluyor. Upcaster v1 event'i okuyunca fiyatı kuruşa çevirir, para birimi için o dönemde geçerli olan sabit değeri ekler, versiyonu 2 yapar. Axon Framework 4'te bunun için upcaster zinciri vardı; 5.0'da geçici olarak kaldırılan upcaster'lar 5.2 ile 'message transformation' çatısı altında geri geldi, bu yüzden Axon 5'e geçen ekiplerin sürüm notlarını dikkatle okuması gerekiyor. Marten 9.x'te upcaster'lar fonksiyon ya da sınıf olarak tanımlanabiliyor ve ham JSON üzerinde dönüşüme izin veriliyor; böylece eski CLR sınıflarını kod tabanında tutmak zorunda kalmazsınız. Marten dokümantasyonu async upcaster'larda N+1 sorgu riskine karşı da uyarıyor. Kural: upcaster deterministik olmalı ve dış servise gitmemeli; replay'in her seferinde aynı sonucu üretmesi buna bağlı.
Alternatif stratejiler: yeni event tipi, kopyala-dönüştür, weak schema
Upcasting tek seçenek değil. Yeni event tipi: değişiklik kavramsal olarak farklıysa (ör. `OrderShipped` yerine `OrderPartiallyShipped` ve `OrderFullyShipped`) eskisini bırakıp yeni tip tanımlamak daha dürüsttür; okuyucular her iki tipi de işler. Weak schema: event'i katı bir sınıfa bağlamadan, alan bazında eşleyerek okumak; ekleme türü değişikliklerde migration ihtiyacını azaltır ama tip güvenliğini zayıflatır.
Kopyala-dönüştür (copy-transform): tüm akışı yeni bir depoya, dönüştürülmüş haliyle kopyalayıp trafiği oraya çevirmek. Bu yöntem upcaster zinciri çok uzadığında (ör. 6-7 versiyon) veya kişisel verinin silinmesi gerektiğinde mantıklıdır, ama operasyonel olarak ağırdır ve 'event'ler değişmez' ilkesini bilinçli olarak esnetir. Greg Young'ın 'Versioning in an Event Sourced System' kitabı bu seçenekleri karşılaştıran temel referans. Pratik sıralama: önce kırıcı olmayan değişiklik, sonra upcaster, son çare kopyala-dönüştür. DCB kullanan sistemlerde bir ek nokta var: event'ler etiketlerle sorgulandığı için bir alanı ya da etiketi yeniden adlandırmak yalnızca okumayı değil, tutarlılık sorgularını da etkiler; etiket şeması da versiyonlanması gereken bir sözleşmedir.
Servisler arası event'ler: sözleşme olarak şema
Bir event yalnızca kendi servisinizin içinde okunuyorsa upcaster yeterli olabilir. Ama event Kafka, RabbitMQ veya benzeri bir broker üzerinden başka ekiplerin servislerine yayınlanıyorsa, o event artık bir API sözleşmesidir. Burada iç event'ler (domain event) ile dışa açılan event'leri (integration event) ayırmak önemli; iç event'i özgürce evriltip dış event'i daha yavaş ve kontrollü değiştirirsiniz.
Schema registry (şema kayıt sistemi) kullanan ekipler Avro veya Protobuf şemalarına uyumluluk kuralı koyar: geriye dönük uyumluluk (yeni okuyucu eski veriyi okuyabilir) ya da ileriye dönük uyumluluk (eski okuyucu yeni veriyi okuyabilir). Kırıcı bir değişiklik gerekiyorsa genelde yeni bir topic veya yeni bir event versiyonu açılır ve iki versiyon belli bir süre (ör. bir çeyrek) birlikte yayınlanır. Anti-pattern: tüketicilerin kim olduğunu bilmeden alanı silmek ve sorunu bir başka ekibin gece alarmından öğrenmek.
Test ve operasyon: üretimi bozmadan değiştirmek
Upcaster'ların en büyük riski, yalnızca yazıldıkları gün test edilmeleri. Sağlam bir pratik: her event versiyonu için gerçek üretimden alınmış (anonimleştirilmiş) örnek JSON'ları `fixtures/events/OrderPlaced/v1.json` gibi dosyalarda saklamak ve CI'da her fixture'ı en güncel versiyona upcast edip aggregate'i kurmak. Yeni bir versiyon eklendiğinde eski fixture'lar silinmez; zincirin tamamı her build'de test edilir.
Operasyon tarafında iki gösterge işe yarar: akışlarda hâlâ hangi versiyonların bulunduğu (ör. `OrderPlaced` event'lerinin yüzde kaçı v1) ve upcasting'in okuma süresine eklediği gecikme. Eski bir versiyon artık hiçbir aktif akışta yoksa veya arşivlenmiş akışlarda kalmışsa, o upcaster emekliye ayrılabilir. AI ajanıyla kod yazan ekipler için ekstra kural: event sınıflarına dokunan her değişiklik, ajan talimat dosyasında 'yeni versiyon + upcaster + fixture' üçlüsünü zorunlu kılmalı; yoksa ajan bir rename ile geçmişi bozabilir.
Son gelişmeler
Ekim 2026 itibarıyla
Önce kendi cevabını düşün, sonra aç.
Neden eski event'leri bir migration script'iyle güncelleyip işi bitirmiyoruz?
Yapılabilir, buna kopyala-dönüştür veya yerinde migration denir. Ama denetim izinin değişmezliğini bozar, büyük akışlarda saatler sürebilir ve aynı event'i okuyan başka servisleri etkiler. Upcasting önce denenir; zincir çok uzarsa veya yasal silme gerekiyorsa migration bilinçli bir karar olarak yapılır.
KVKK/GDPR kapsamında kişisel verinin silinmesi gerekiyorsa değişmez event'lerle ne yapacağız?
Yaygın çözüm crypto-shredding: kişisel alanları kullanıcıya özel bir anahtarla şifreleyip silme talebinde anahtarı yok etmek. Böylece event yerinde kalır ama içerik okunamaz hale gelir. Diğer seçenek kişisel veriyi event dışında ayrı bir depoda tutup event'te yalnızca referans saklamak.
Upcasting okuma performansını düşürmez mi?
Her okumada dönüşüm maliyeti var, ama çoğu dönüşüm basit JSON işlemi olduğu için aggregate yükleme süresine etkisi genelde küçüktür. Asıl risk uzun zincirlerde ve sıcak yollarda; snapshot kullanmak ve upcasting gecikmesini ölçmek bunu yönetir. Gecikme ölçülebilir hale geldiğinde kopyala-dönüştür seçeneği masaya gelir.
Event sourcing kullanmıyoruz, bu konu bizi ilgilendiriyor mu?
Webhook, Kafka mesajı, mobil uygulamaya giden push payload'u veya bir kuyrukta bekleyen iş gibi 'kaydedilip sonra okunan' her veri aynı sorunu yaşar. Eski mobil uygulama sürümlerinin yıllarca yaşaması da buna benzer. Tolerant reader ve versiyonlu payload ilkeleri event sourcing dışında da geçerli.
Dynamic Consistency Boundary'ye geçersek upcasting ihtiyacı ortadan kalkar mı?
Hayır. DCB tutarlılık sınırını stream yerine etiket ve sorgu üzerinden tanımlıyor, ama event'ler yine değişmez ve yıllar sonra yeniden okunuyor. Şema değişikliği yine versiyon ve dönüşüm ister; üstüne bir de etiket şemasının uyumluluğunu korumanız gerekir. Axon 5'te upcasting'in 5.2 ile 'message transformation' adıyla geri gelmesi de bu ihtiyacın sürdüğünü gösteriyor.
Notun kaynakları
- Event versioning with Marten (Oskar Dudycz, event-driven.io)
- Axon Framework Reference: Event Versioning
- Versioning in an Event Sourced System (Greg Young)
- The Release of Axon Framework 5.0 (Axoniq, Ekim 2025)
- Axon Framework 5.4.0 and Axoniq Framework 5.4.0: Durable Workflows and a Path Off Sagas (Axoniq, Ekim 2026)
- Axon Server 2026.1.0: Multi-Tier DCB Storage and Self-Healing Topology (Axoniq, Ağustos 2026)
- The Aggregate Had Its Day. Dynamic Consistency Boundary is Next. (Axoniq, Mayıs 2026)
- DCB implementasyonları listesi (dcb.events)
Sunum stili
12 slayt. Tıklarsan sunum o slayttan başlar.
Slayt notları
İlişkisel veritabanında kolon rename etmek bir migration. Event Sourcing'de aynı cümle 'milyonlarca eski Event'i ne yapacağız?' sorusuna dönüşür.
Event'ler değişmez; denetim izi, replay ve Projection yeniden kurulumu buna dayanır. Event sınıfını refactor etmek geçmişle sözleşmeyi bozmak demek.
Bunu yönetmezsen deserializasyon hatası alırsın; daha kötüsü, sessizce null gelen alanlarla yanlış hesaplanan bakiyeler.
En sinsisi sol üst: şema aynı görünür ama her eski Event yanlış yorumlanır. Ad aynı, anlam farklıysa yeni alan ya da yeni versiyon.
Marten 9.x'te upcaster fonksiyon ya da sınıf olabilir, ham JSON üzerinde de çalışır; eski sınıfları kod tabanında tutmak gerekmez. Axon 5'te upcaster'lar 5.2 ile 'message transformation' adıyla döndü.
Replay deterministik değilse Aggregate durumu çalıştırmadan çalıştırmaya değişir. Her versiyon için anonimleştirilmiş gerçek JSON fixture saklayın, eskileri silmeyin.
Greg Young'ın 'Versioning in an Event Sourced System' kitabı bu seçenekleri karşılaştıran temel referans. Copy-transform değişmezlik ilkesini bilinçli olarak esnetir.
İç Domain Event'i özgürce evriltin, dışa açılan Integration Event'i yavaş ve kontrollü değiştirin. Kırıcı değişiklikte yeni topic ya da versiyon açılır, ikisi bir süre birlikte yayınlanır.
Tutarlılık modeli değişiyor ama event'ler hâlâ değişmez. DCB'de etiketler de sözleşmeye giriyor; etiket rename'i tutarlılık sorgusunu bozabilir.
Akışlarda hâlâ yüzde kaç v1 var, ölçün. Eski versiyon hiçbir aktif akışta kalmadığında o upcaster emekliye ayrılabilir.
Agent ile kod yazan ekiplerde risk büyüyor: talimat dosyasında açık kural yoksa Agent Event sınıfını sıradan bir sınıf gibi refactor eder.
Soru olarak bırak: şemalarınızı nasıl versiyonluyorsunuz, Schema Registry mi, upcaster mı, 'dikkatli olalım' mı?