Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

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.
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, 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üncellerBu akışta event bus, servislerin birbirini doğrudan çağırmasını engeller. Her servis yalnızca kendi sorumluluğuna odaklanır.
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 --> AnalyticsDBBu diyagramda her servis kendi ihtiyacına göre veri saklıyor. Aynı olay farklı servislerde farklı veri modellerine dönüşebiliyor.
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.
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 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.
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.
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.
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.
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?
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.