Event-Driven Architecture: Servisler Birbirine Sormadan Nasıl Konuşur?

Event-driven architecture, yani olay odaklı mimari, servislerin birbirine doğrudan “bana şu veriyi ver” diye sormak yerine sistemde gerçekleşen değişiklikleri olay olarak yayınladığı bir yaklaşımdır. Bu yazıda event bus, producer, consumer, event log, replay, tutarlılık ve teslim garantileri gibi temel kavramları sade bir sistem tasarımı bakışıyla öğreneceksin.

Event-driven architecture, yani olay odaklı mimari, servislerin birbirine doğrudan “bana şu veriyi ver” diye sormak yerine sistemde gerçekleşen değişiklikleri olay olarak yayınladığı bir yaklaşımdır. Bu yazıda event bus, producer, consumer, event log, replay, tutarlılık ve teslim garantileri gibi temel kavramları sade bir sistem tasarımı bakışıyla öğreneceksin.

Request-response ile event-driven yaklaşım arasındaki fark

Klasik request-response modelinde bir servis başka bir servisten doğrudan cevap bekler. Yani şöyle düşün: bir kullanıcı sipariş oluşturduğunda API Gateway, Order Service’e gider; Order Service de gerekirse Payment Service’e, sonra Inventory Service’e doğrudan istek atar. Akış nettir ama servisler birbirine daha sıkı bağlıdır.

Event-driven architecture tarafında ise servis “sipariş oluşturuldu” gibi bir olayı yayınlar. Bu olayla ilgilenen servisler onu dinler ve kendi işini yapar. Order Service, Payment Service’in ne yapacağını bilmek zorunda değildir; sadece sistemde önemli bir değişiklik olduğunu duyurur.

flowchart LR
    Client["Kullanıcı / Client"] --> Gateway["API Gateway"]
    Gateway --> Order["Order Service"]

    Order -->|"OrderCreated event"| Bus["Event Bus"]

    Bus --> Payment["Payment Service"]
    Bus --> Inventory["Inventory Service"]
    Bus --> Notification["Notification Service"]

    Payment -->|"PaymentCompleted event"| Bus
    Inventory -->|"StockReserved event"| Bus
    Notification --> Email["E-posta / SMS sağlayıcısı"]

Bu diyagramda servisler birbirine doğrudan bağlanmak yerine event bus üzerinden haberleşiyor. Bir servis olay üretirken diğer servisler kendi ilgilerine göre bu olayı tüketiyor.

Producer, consumer ve event bus ne işe yarar?

Producer, yani olay üreten servis, sistemde bir şey değiştiğinde event yayınlar. Bunu şöyle hayal edebilirsin: bir haber kanalı son dakika gelişmesini duyurur; ilgilenen herkes bu duyuruyu kendi bağlamında değerlendirir. Consumer veya subscriber ise bu olayı dinleyen ve gerekirse kendi durumunu güncelleyen servistir.

Event bus, bu olayların taşındığı merkezi altyapıdır. Kafka, RabbitMQ, Amazon SNS/SQS, Google Pub/Sub gibi sistemler bu rolü üstlenebilir. Buradaki önemli nokta şudur: event bus iş mantığını bilmez; genellikle olayları ilgili tüketicilere ulaştırır.

sequenceDiagram
    participant Producer as Producer Service
    participant Bus as Event Bus
    participant ConsumerA as Consumer A
    participant ConsumerB as Consumer B

    Producer->>Bus: Event yayınlar
    Bus-->>ConsumerA: İlgili olayı iletir
    Bus-->>ConsumerB: İlgili olayı iletir
    ConsumerA->>Bus: Yeni event yayınlayabilir
    ConsumerB->>ConsumerB: Kendi lokal durumunu günceller

Bu akışta event bus, servislerin birbirini doğrudan çağırmasını engeller. Her servis yalnızca kendi sorumluluğuna odaklanır.

Her servis kendi verisini neden saklar?

Event-driven mimaride servisler çoğu zaman tükettiği olayların kendisiyle ilgili kısmını kendi veritabanında saklar. Yani şöyle düşün: Billing Service, “sipariş ödendi” olayını alır ve kendi faturalama görünümünü oluşturur. Inventory Service aynı olayı farklı bir amaçla kullanabilir.

Bu yaklaşım servisleri daha bağımsız yapar. Bir servis geçici olarak erişilemez olsa bile diğer servisler kendi yerel verileriyle çalışmaya devam edebilir. Fakat bunun bir bedeli vardır: sistemin her noktasındaki veri aynı anda birebir aynı olmayabilir. Buna eventual consistency, yani nihai tutarlılık denir.

flowchart TD
    Bus["Event Bus"]

    subgraph Services["Servisler"]
        Order["Order Service"]
        Billing["Billing Service"]
        Search["Search Service"]
        Analytics["Analytics Service"]
    end

    subgraph Databases["Lokal Veri Görünümleri"]
        OrderDB[("Order DB")]
        BillingDB[("Billing DB")]
        SearchDB[("Search Index")]
        AnalyticsDB[("Analytics Store")]
    end

    Order -->|event yayınlar| Bus
    Bus --> Billing
    Bus --> Search
    Bus --> Analytics

    Order --> OrderDB
    Billing --> BillingDB
    Search --> SearchDB
    Analytics --> AnalyticsDB

Bu diyagramda her servis kendi ihtiyacına göre veri saklıyor. Aynı olay farklı servislerde farklı veri modellerine dönüşebiliyor.

Event log ve replay neden güçlüdür?

Event log, sistemde yaşanan olayların sıralı kaydıdır. Bunu şöyle hayal edebilirsin: son durumu doğrudan saklamak yerine, o duruma nasıl gelindiğini de saklıyorsun. Bu sayede geçmişteki bir ana geri dönmek, hatalı bir dönemi analiz etmek veya yeni bir servisi eski olaylarla beslemek mümkün olur.

Örneğin yeni bir Recommendation Service eklediğini düşün. Bu servis bugünden itibaren gelen olayları dinleyebilir; ama geçmiş siparişleri de bilmesi gerekiyorsa event log baştan oynatılarak servisin kendi görünümü oluşturulabilir.

flowchart LR
    Log["Event Log"]
    E1["Event 1"]
    E2["Event 2"]
    E3["Event 3"]
    E4["Event 4"]

    Log --> E1 --> E2 --> E3 --> E4

    E1 --> NewService["Yeni Servis"]
    E2 --> NewService
    E3 --> NewService
    E4 --> NewService

    NewService --> NewDB[("Yeni Servis Veritabanı")]

Bu yapı özellikle servis değiştirme, veri yeniden oluşturma ve üretim hatalarını analiz etme gibi durumlarda işe yarar. Git’in commit geçmişi de bu fikre yakın bir zihinsel model sunar: son durumu anlamak için değişiklik geçmişinden yararlanırsın.

Teslim garantileri: at most once ve at least once

Event-driven sistemlerde mesajın nasıl teslim edileceği kritik bir karardır. At most once, yani en fazla bir kez teslim, olayın kaybolmasını kabul edebilir ama tekrar işlenmesini istemez. At least once, yani en az bir kez teslim, olayın mutlaka ulaşmasını hedefler; fakat aynı olay birden fazla kez gelebilir.

Yani şöyle düşün: önemsiz bir bilgilendirme bildirimi kaybolursa sistem çökmez. Ama ödeme, fatura veya stok güncellemesi gibi işlemlerde olayın mutlaka işlenmesi gerekir. Bu durumda consumer tarafında idempotency gerekir. Idempotency, aynı olay tekrar işlense bile sonucun bozulmaması demektir.

flowchart TD
    Event["Event oluşur"] --> Choice{"Teslim stratejisi"}

    Choice -->|At most once| SendOnce["Bir kez gönder"]
    SendOnce --> Maybe["Ulaşmazsa tekrar denenmeyebilir"]

    Choice -->|At least once| Retry["Başarılı olana kadar tekrar dene"]
    Retry --> Consumer["Consumer işler"]
    Consumer --> Idempotent{"Daha önce işlendi mi?"}
    Idempotent -->|Evet| Skip["Tekrar etkisiz bırak"]
    Idempotent -->|Hayır| Apply["İşlemi uygula"]

Bu diyagram teslim stratejisinin iş mantığını nasıl etkilediğini gösteriyor. Özellikle at least once kullanıyorsan consumer tarafında tekrar gelen event’lere hazırlıklı olmalısın.

Event-driven mimarinin güçlü tarafları

Event-driven architecture servisleri gevşek bağlı hale getirir. Bir servis olay yayınlar, diğer servislerin kim olduğunu bilmek zorunda kalmaz. Bu durum sistemin büyümesini kolaylaştırabilir çünkü yeni servisler eski servisleri değiştirmeden event bus’a abone olabilir.

Ayrıca geçmiş olayları saklamak, hata analizi ve servis migrasyonu için çok değerlidir. Yeni bir servis yazdığında eski olayları replay ederek kendi verisini oluşturabilir, sonra canlı olayları tüketmeye devam edebilirsin. React, Node.js ve oyun sistemlerinde event fikrinin sık görülmesi de tesadüf değildir; kullanıcı etkileşimleri, state değişimleri ve asenkron akışlar bu modele doğal olarak uyar.

Zor tarafı: akışı anlamak kolay değildir

Bu mimarinin bedeli görünmezliktir. Request-response modelinde kodu okuduğunda bir servisin nereye istek attığını daha kolay görürsün. Event-driven sistemde ise bir event yayınlandığında onu kimin tükettiğini anlamak için event bus yapılandırmasına, topic’lere, consumer gruplarına ve servislerin aboneliklerine bakman gerekir.

flowchart TD
    Developer["Geliştirici"] --> ServiceCode["Service A kodunu inceler"]
    ServiceCode --> Publish["Event yayınlandığını görür"]
    Publish --> Question{"Bu event'i kim tüketiyor?"}
    Question --> BusConfig["Event bus / topic yapılandırması"]
    BusConfig --> ConsumerList["Consumer listesini bulur"]
    ConsumerList --> ServiceB["Service B"]
    ConsumerList --> ServiceC["Service C"]
    ConsumerList --> ServiceD["Service D"]

Bu yüzden event-driven sistemlerde gözlemlenebilirlik, dokümantasyon ve event şemaları çok önemlidir. Aksi halde sistem çalışır ama ekip için takip etmesi zor bir yapıya dönüşür.

Dış sistemlerle çalışırken dikkatli olmak gerekir

Event replay her zaman masum değildir. Bir servis yalnızca kendi veritabanını güncelliyorsa eski olayları tekrar oynatmak genellikle yönetilebilir. Ama servis dış dünyaya e-posta gönderiyor, ödeme alıyor veya üçüncü parti API çağırıyorsa replay tehlikeli olabilir.

Bunu şöyle hayal edebilirsin: geçmişteki “InvoiceCreated” olaylarını tekrar oynattığında sistem aynı faturaları müşterilere yeniden e-posta olarak göndermemeli. Bu yüzden dış etki oluşturan işlemler ayrı tasarlanmalı, event id’leriyle kontrol edilmeli ve gerekirse side effect kayıtları tutulmalıdır.

flowchart LR
    Log["Event Log"] --> Replay["Replay işlemi"]
    Replay --> Service["Notification Service"]
    Service --> Decision{"Bu event daha önce dış sisteme yansıdı mı?"}
    Decision -->|Evet| Ignore["Tekrar gönderme"]
    Decision -->|Hayır| Provider["E-posta sağlayıcısı"]
    Provider --> SentLog[("Gönderim kaydı")]

Bu diyagram replay sırasında dış sistemlere tekrar tekrar istek atılmasını engelleyen kontrol noktasını gösteriyor. Event-driven mimaride en riskli alanlardan biri tam olarak burasıdır.

Event geçmişi nasıl yönetilir?

Bütün olayları sonsuza kadar ham haliyle işlemek pratik olmayabilir. Çok büyük sistemlerde baştan sona replay yapmak saatler, hatta günler sürebilir. Bu yüzden snapshot, compaction veya diff tabanlı yaklaşımlar kullanılır.

Snapshot, belli bir andaki sistem durumunu kaydetmektir. Yani şöyle düşün: binlerce olayı baştan çalıştırmak yerine, dün geceki hazır durumu alır ve yalnızca bugünkü olayları üzerine uygularsın.

flowchart TD
    Start["Başlangıç"] --> Events1["Eski event'ler"]
    Events1 --> Snapshot["Snapshot / Compact edilmiş durum"]
    Snapshot --> Events2["Yeni event'ler"]
    Events2 --> Current["Güncel durum"]

Bu yaklaşım replay maliyetini azaltır. Ancak hangi olayların saklanacağı, hangilerinin sıkıştırılacağı ve hangi durumların geri alınabilir olduğu mimari karar gerektirir.

Ne zaman event-driven architecture seçilmeli?

Event-driven architecture her probleme uygun değildir. Aslında bu çok basit: servislerin birbirinden bağımsız büyümesi, geçmiş olayların saklanması, asenkron işleme ve yeni consumer’ların kolay eklenmesi önemliyse güçlü bir seçenektir. Ama sıkı zaman kontrolü, anlık tutarlılık ve kolay izlenebilir çağrı zinciri gerekiyorsa request-response daha sade olabilir.

System design görüşmelerinde de bu mimariyi sırf güçlü göründüğü için kullanmak iyi fikir değildir. Önce problemin doğasına bakmak gerekir: olaylar gerçekten sistemin merkezi kavramı mı, yoksa yalnızca servisler arası basit veri alışverişi mi yapılıyor?

Özet

Event-driven architecture, servislerin birbirinden doğrudan veri istemek yerine değişiklikleri event olarak yayınladığı bir mimari yaklaşımdır. Bu yapı servisleri gevşek bağlı hale getirir, yeni consumer eklemeyi kolaylaştırır, event log sayesinde geçmişi yeniden oynatmaya izin verir ve büyük sistemlerde esneklik sağlayabilir. Buna karşılık veri tutarlılığı, akışı takip etme, dış sistemlerle replay güvenliği ve teslim garantileri dikkatli tasarlanmalıdır. Kısacası event-driven mimari güçlüdür, ama yalnızca sistemin doğal dili gerçekten “olaylar” olduğunda doğru seçimdir.

Bu yazı What’s an Event Driven System? videosundan ilham alınarak yazılmıştır.


Kaynakça

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir