Bir banka hesabının bugünkü bakiyesini yalnızca tek bir balance alanında tutmak kolaydır; ancak bu değerin nasıl oluştuğunu anlamak zordur. Event Sourcing, sistemin mevcut durumunu doğrudan saklamak yerine, durumu değiştiren olayları kalıcılaştırır. Böylece uygulama geçmişi silmek yerine kaydeder ve istenen anın durumunu olayları tekrar oynatarak oluşturabilir.
``
Klasik CRUD yaklaşımında kayıt çoğunlukla son hâliyle güncellenir. Örneğin siparişin durumu Kargoda olur; fakat önce ne zaman oluşturulduğu, hangi ödeme denemesinin başarısız olduğu veya kim tarafından iptal edildiği ayrı bir denetim kaydı yoksa belirsizdir. Event Sourcing’de ise OrderCreated, PaymentFailed, PaymentReceived ve OrderShipped gibi alan dilini taşıyan olaylar saklanır.
Temel fikir oldukça nettir: Sistem durumu, sıralı olayların indirgenmiş sonucudur. Matematiksel olarak bunu şöyle ifade edebiliriz:
\[S_n = f(f(f(S_0, E_1), E_2), \dots, E_n)\]Burada $S_0$ başlangıç durumu, $E_i$ bir olay, $f$ ise olayı mevcut duruma uygulayan fonksiyondur. İdeal durumda bu fonksiyon deterministik olmalıdır: Aynı olay dizisi, her yeniden çalıştırmada aynı durumu üretmelidir. Bu özellik hata araştırırken adeta zaman makinesi etkisi yaratır.
| Özellik | Geleneksel CRUD | Event Sourcing |
|---|---|---|
| Kalıcı veri | Güncel durum | Değişmez olay akışı |
| Geçmişi görme | Ek log gerektirir | Doğal olarak mevcuttur |
| Hata analizi | Sınırlı bağlam | Olay sırası incelenir |
| Veri modeli değişimi | Migrasyon ağırlıklı | Yeni projection üretilebilir |
| Okuma performansı | Genellikle doğrudan | Projection gerekebilir |
Örneğin sadeleştirilmiş bir hesap agregası, olayları uygulayarak bakiyesini üretir. Buradaki kritik ayrım şudur: Komutlar niyeti ifade eder (Para yatır), olaylar ise gerçekleşmiş ve değişmez gerçekleri (Para yatırıldı) temsil eder.
from dataclasses import dataclass
@dataclass
class Account:
balance: int = 0
def apply(self, event: dict):
if event["type"] == "MoneyDeposited":
self.balance += event["amount"]
elif event["type"] == "MoneyWithdrawn":
self.balance -= event["amount"]
events = [
{"type": "MoneyDeposited", "amount": 500},
{"type": "MoneyWithdrawn", "amount": 120},
{"type": "MoneyDeposited", "amount": 80}
]
account = Account()
for event in events:
account.apply(event)
print(account.balance) # 460
Gerçek projelerde olaylar bir event store içinde agregaya ait sürüm numarasıyla tutulur. Sürüm numarası, iki kullanıcının aynı anda aynı hesabı değiştirmesi gibi yarış durumlarında iyimser eşzamanlılık kontrolü sağlar. Beklenen sürüm $v$ iken depodaki sürüm farklıysa işlem reddedilir; istemci güncel olayları okuyup kararını tekrar vermelidir.
Event Sourcing çoğu zaman CQRS ile birlikte anılır. Yazma tarafı iş kurallarını korur ve olay üretir; okuma tarafı ise bu olaylardan hızlı sorgulanabilir projection’lar oluşturur. Örneğin sipariş olaylarından müşteri paneli için ayrı bir siparis_ozetleri tablosu üretilebilir.
| Kavram | Görevi | Örnek |
|---|---|---|
| Command | Değişiklik isteği | ShipOrder |
| Aggregate | İş kuralı sınırı | Order |
| Event | Gerçekleşmiş kayıt | OrderShipped |
| Projection | Okuma modeli | Günlük satış özeti |
| Snapshot | Hızlandırılmış ara durum | 500. olaydaki hesap durumu |
Her olaydan başlayarak yeniden oynatma, olay sayısı büyüdüğünde pahalı olabilir. Çözüm snapshot’tır: Belirli bir olay numarasındaki durum saklanır, ardından yalnızca sonraki olaylar uygulanır. Yaklaşık maliyet $O(n)$ iken snapshot sonrası $O(n-k)$ olur; burada $k$ snapshot’ın kapsadığı olay sayısıdır.
Bu mimari her tabloya uygulanacak sihirli değnek değildir. Basit yönetim panellerinde gereksiz karmaşıklık yaratabilir. Ancak finans, stok, rezervasyon, lojistik ve denetlenebilir iş süreçlerinde geçmişin birinci sınıf veri olması büyük avantajdır. İyi isimlendirilmiş, değişmez ve iş diline yakın olaylar sayesinde sistem yalnızca ne olduğunu değil, neden bugünkü hâline geldiğini de anlatır.
Yorumlar