Event Sourcing: Olaylarla Durum Yönetimi

Sistem durumunu değiştiren olayların kaydedilmesi ve bu olayların yeniden oynatılarak (replay) durumun yeniden oluşturulması, avantajları ve zorlukları anlatılır.

Event Sourcing: Olaylarla Durum Yönetimi

Event Sourcing: Tüm Sistem Durumunu Olaylarla Saklama ve Replay Etme

Geleneksel veri depolama yaklaşımlarında, bir sistemin mevcut durumunu (örneğin, bir banka hesabının bakiyesi) saklarız. Bu yaklaşımda, o duruma nasıl gelindiğine dair geçmiş bilgisi kaybolur. Event Sourcing (Olay Kaynağı) ise bu paradigmayı tersine çevirir: Mevcut durumu değil, durumu değiştiren tüm olayları (events) saklarız. Sistemin mevcut durumu, bu olayların sırasıyla yeniden oynatılması (replay) ile elde edilir.

Event Sourcing, özellikle karmaşık iş mantığı, denetim (audit) gereksinimleri ve geçmişe yönelik analiz ihtiyaçlarının olduğu sistemlerde güçlü bir araçtır. Bu yazıda, Event Sourcing'in temel prensiplerini, avantajlarını, zorluklarını ve .NET ekosisteminde nasıl uygulanacağını derinlemesine ele alacağız.


1. Event Sourcing Nedir? Temel Felsefe

Bir banka hesabı örneğini ele alalım. Geleneksel yaklaşımda, Account tablosunda Balance sütununu güncelleriz.

AccountId Balance LastUpdated
12345 1250.00 2023-10-01

Event Sourcing'de ise her değişiklik bir olay (event) olarak saklanır.

EventId AccountId EventType Amount Timestamp
1 12345 AccountOpened 1000.00 2023-01-01
2 12345 Deposit 500.00 2023-02-01
3 12345 Withdraw 250.00 2023-03-01

Mevcut bakiye (1250 TL), bu olayların toplanmasıyla (1000 + 500 - 250) hesaplanır.

Event Sourcing, append-only (sadece ekleme) bir veri deposudur. Olaylar asla silinmez veya güncellenmez. Bu, tam bir denetim izi (audit trail) sağlar.


2. Temel Yapı Taşları

A. Event (Olay)
Sistemde gerçekleşen ve bir durum değişikliğini temsil eden, değişmez (immutable) bir veri yapısıdır.

csharp

// Event örnekleri
public record AccountOpened(Guid AccountId, decimal InitialBalance, DateTime OpenedAt);
public record DepositMade(Guid AccountId, decimal Amount, DateTime Timestamp);
public record WithdrawalMade(Guid AccountId, decimal Amount, DateTime Timestamp);

B. Aggregate (Küme)
Bir grup ilgili olayı ve bu olayların geçerli durumunu (state) temsil eden domain nesnesidir. Her Aggregate Root (ör. BankAccount) kendi olay akışına (event stream) sahiptir. Olaylar, Aggregate üzerinde işlenir (apply) ve durumu güncellenir.

csharp

public class BankAccount : AggregateRoot
{
    public Guid Id { get; private set; }
    public decimal Balance { get; private set; }
    public bool IsActive { get; private set; }

    // Komut (Command) işleyicisi
    public void MakeDeposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Yatırma miktarı pozitif olmalıdır.");
        if (!IsActive) throw new InvalidOperationException("Hesap aktif değil.");

        // Bir olay oluştur ve uygula
        ApplyEvent(new DepositMade(Id, amount, DateTime.UtcNow));
    }

    // Olay uygulama (Apply) metodu
    private void Apply(DepositMade @event)
    {
        Balance += @event.Amount;
    }

    private void Apply(AccountOpened @event)
    {
        Id = @event.AccountId;
        Balance = @event.InitialBalance;
        IsActive = true;
    }
}

C. Event Store (Olay Deposu)
Olayların kalıcı olarak saklandığı veritabanıdır. Event Store'lar genellikle JSON veya binary formatında olayları saklar. Popüler seçenekler: EventStoreDB, Marten (PostgreSQL üzerinde), veya özel bir SQL tablosu.

D. Projection (Yansıtma) / Read Model (Okuma Modeli)
Olay akışından türetilen, belirli bir amaca yönelik (ör. raporlama, UI) veri modelidir. CQRS ile birlikte sıkça kullanılır.


3. Event Sourcing + CQRS Mimarisi

Event Sourcing ve CQRS birlikte kullanıldığında çok güçlü bir mimari oluşturur:

  1. Command (Komut): İş mantığını tetikler, Aggregate üzerinde değişiklik yapar ve olaylar üretir.

  2. Event Store: Olayları append-only (sadece ekleme) olarak saklar.

  3. Event Handler: Olayları yakalar ve read model'leri (okuma modelleri) günceller.

  4. Read Model: Sorgu (query) işlemleri için optimize edilmiş, denormalize veri yapılarıdır.

  5. Projection: Olayları read model'lere dönüştüren işlemdir.

Bu mimari, yazma (write) ve okuma (read) iş yüklerini ayırarak ölçeklenebilirliği artırır.


4. Event Sourcing'in Avantajları

  • Tam Denetim İzi (Audit Trail): Sistemdeki her değişiklik, kim, ne zaman, hangi veri ile yaptı, tam olarak kaydedilir. Bu, finansal sistemler, sağlık uygulamaları ve regülasyon gereksinimleri olan sistemler için çok değerlidir.

  • Geçmişe Dönük Analiz (Temporal Queries): Sistemin herhangi bir anındaki durumunu sorgulayabilirsiniz. Belirli bir tarihte hesap bakiyesi neydi? Hata ayıklama ve analiz için çok güçlüdür.

  • Esneklik (Flexibility): Yeni bir read model veya raporlama ihtiyacı doğduğunda, mevcut olayları yeniden oynatarak (replay) yeni bir projeksiyon oluşturabilirsiniz. Bu, veri göçlerini (migration) çok daha kolay hale getirir.

  • Event Replay (Yeniden Oynatma): Hata durumunda veya sistem yeniden başlatıldığında, olaylar yeniden oynatılarak sistem durumu güvenle yeniden oluşturulabilir.

  • Zaman Yolculuğu (Time Travel): Hata ayıklama sırasında, belirli bir olayın öncesindeki duruma dönüp inceleme yapabilirsiniz.


5. Event Sourcing'in Zorlukları ve Riskleri

  • Karmaşıklık (Complexity): Event Sourcing, geleneksel CRUD'dan çok daha karmaşıktır. Olay versiyonlama, snapshot yönetimi, eventual consistency gibi ek zorluklar getirir.

  • Event Versioning (Olay Versiyonlama): Zamanla event şemaları değişir. Yeni event sürümleri eklenirken eski event'lerin nasıl okunacağı yönetilmelidir.

  • Depolama Maliyeti: Her değişiklik bir event olarak saklandığı için, zamanla depolama maliyeti artar.

  • Snapshot (Anlık Görüntü) Gereksinimi: Çok uzun event akışlarının (ör. 10.000+ event) yeniden oynatılması performansı düşürür. Bu nedenle, belirli aralıklarla snapshot alınması gerekir.

  • Eventual Consistency (Nihai Tutarlılık): Read modelleri, event işleme süresi kadar gecikmeli (lag) olabilir. Bu, bazı senaryolarda kabul edilemez.


6. .NET'te Event Sourcing Uygulama Yöntemleri

A. EventStoreDB (Özel Olay Deposu)
Event Sourcing için özel olarak tasarlanmış, açık kaynak, olgun bir veritabanıdır. .NET istemcisi (EventStore.Client) mevcuttur.

csharp

// EventStoreDB ile bağlantı ve olay yazma
using var connection = EventStoreConnection.Create("ConnectTo=tcp://admin:changeit@localhost:1113");
await connection.ConnectAsync();

var eventData = new EventData(
    eventId: Uuid.NewUuid(),
    type: "DepositMade",
    data: JsonSerializer.SerializeToUtf8Bytes(new DepositMade(accountId, amount, DateTime.UtcNow)),
    metadata: JsonSerializer.SerializeToUtf8Bytes(new { UserId = currentUserId })
);

await connection.AppendToStreamAsync($"account-{accountId}", StreamState.Any, new[] { eventData });

B. Marten (PostgreSQL ile)
Marten, PostgreSQL üzerinde Event Sourcing ve CQRS uygulamanızı sağlayan bir .NET kütüphanesidir. Document DB (JSONB) ve Event Store yeteneklerini birleştirir.

csharp

// Marten ile Event Sourcing
var store = DocumentStore.For(options =>
{
    options.Connection("Host=localhost;Database=marten_db;Username=postgres;Password=...");
    options.Events.StreamIdentity = StreamIdentity.AsGuid;
});

using var session = store.LightweightSession();
var account = new BankAccount();
account.MakeDeposit(500);

session.Events.Append(account.Id, account.Events.ToArray());
await session.SaveChangesAsync();

C. Özel Implementasyon (SQL Server ile)
Daha basit projelerde, olayları JSON olarak bir SQL tablosunda saklayabilirsiniz.

sql

CREATE TABLE EventStore (
    Id BIGINT IDENTITY PRIMARY KEY,
    AggregateId UNIQUEIDENTIFIER NOT NULL,
    Version INT NOT NULL,
    EventType NVARCHAR(255) NOT NULL,
    EventData NVARCHAR(MAX) NOT NULL,
    CreatedAt DATETIME2 NOT NULL
);

.NET'te Olayların Yeniden Oynatılması (Replay)

csharp

public async Task<BankAccount> ReplayEventsAsync(Guid accountId)
{
    var events = await _eventStore.GetEventsAsync(accountId);
    var account = new BankAccount();
    foreach (var @event in events)
    {
        account.ApplyEvent(@event); // Apply metodu ile durumu güncelle
    }
    return account;
}

7. Snapshot (Anlık Görüntü) Kullanımı

Uzun event akışlarında performansı artırmak için snapshot kullanılır. Her N event'ten sonra aggregate'in mevcut durumu (state) bir snapshot olarak saklanır. Yeniden oynatma (replay) işlemi, en son snapshot'dan başlatılır ve sadece ondan sonraki event'ler işlenir.

csharp

public class AccountSnapshot
{
    public Guid AccountId { get; set; }
    public decimal Balance { get; set; }
    public bool IsActive { get; set; }
    public int Version { get; set; } // Hangi versiyona ait olduğu
    public DateTime SnapshotAt { get; set; }
}

// Snapshot ile replay
public async Task<BankAccount> ReplayWithSnapshotAsync(Guid accountId)
{
    var snapshot = await _snapshotStore.GetLatestSnapshotAsync(accountId);
    var events = await _eventStore.GetEventsAsync(accountId, fromVersion: snapshot.Version + 1);

    var account = new BankAccount();
    account.LoadSnapshot(snapshot); // Snapshot'dan durumu yükle
    foreach (var @event in events)
    {
        account.ApplyEvent(@event);
    }
    return account;
}

8. Event Sourcing Ne Zaman Kullanılmalı?

Uygun Olduğu Senaryolar Uygun Olmadığı Senaryolar
Finansal sistemler (bankacılık, ödeme) Basit CRUD uygulamaları
Denetim (audit) ve regülasyon gereksinimleri olan sistemler Prototipler ve MVP'ler
Karmaşık iş mantığı ve geçmiş analizi gereken sistemler Yüksek performans, düşük gecikme gereksinimleri
Zaman yolculuğu (time travel) ve hata ayıklama ihtiyaçları Ekip deneyimi düşükse veya proje büyük değilse
CQRS ile birlikte kullanılacak sistemler  

Sonuç:

Event Sourcing, sistem durumunuzu bir "olay günlüğü" olarak düşünmenizi sağlayan devrimsel bir veri depolama yaklaşımıdır. Tam denetim izi, geçmişe dönük analiz ve esneklik gibi avantajları, özellikle finans ve regülasyon gerektiren sistemler için vazgeçilmez kılar.

Ancak, getirdiği karmaşıklık (event versiyonlama, eventual consistency, snapshot yönetimi) ve depolama maliyeti, basit projeler için aşırı mühendislik (over-engineering) olabilir. .NET ekosisteminde, EventStoreDB, Marten (PostgreSQL) veya özel SQL tabanlı çözümlerle Event Sourcing uygulanabilir.

Özetle:

  • Event Store: Tüm olayları append-only olarak sakla.

  • Aggregate: Olayları işle ve durumu güncelle.

  • Projection: Olayları read model'lere dönüştür.

  • Snapshot: Uzun event akışlarında performansı artır.

  • CQRS ile birlikte kullan: Yazma ve okuma iş yüklerini ayır.

Bu deseni doğru uyguladığınızda, sisteminiz sadece mevcut durumu değil, geçmişin tamamını da yönetebilir hale gelir.

Tüm yazılar