Event Driven Architecture Nedir ve Nasıl Uygulanır?
Yazılım dünyasında bir şeyler değişti. Artık “şu işi yap, bitir, devam et” mantığıyla çalışan monolitik sistemler yerini, birbirleriyle konuşan, birbirini tetikleyen, bağımsız ama uyumlu parçalardan oluşan mimarilere bırakıyor. Event Driven Architecture (EDA), yani Olay Güdümlü Mimari, tam da bu dönüşümün kalbinde yer alıyor. Ben bu mimariyi ilk kez büyük bir e-ticaret projesinde, bir sipariş işleme sistemini yeniden tasarlarken yakından tanıdım. O günden beri EDA’nın hem gücünü hem de tuzaklarını yerinde gördüm.
Event Driven Architecture Nedir?
Temel fikir şu: sistemdeki bileşenler birbirini doğrudan çağırmak yerine, olaylar aracılığıyla haberleşir. Bir servis bir şey olduğunda “bu olay gerçekleşti” diye duyurur, ilgilenen diğer servisler bu duyuruyu alır ve kendi işlemlerini yapar. Birincisi ikincisinin varlığından habersiz olabilir. Bu gevşek bağlılık (loose coupling), sistemi son derece esnek kılar.
Geleneksel istek-yanıt mimarisinde A servisi B servisini çağırır, yanıt bekler. B çöktüyse A da çöker ya da bekler. EDA’da ise A servisi bir event yayınlar (publish), B servisi bu eventi kendi zamanında tüketir (consume). B geçici olarak devre dışıysa, event kuyruğunda bekler.
Bu üç temel kavram etrafında döner:
- Event (Olay): Sistemde gerçekleşen bir şeyin kaydı. “Sipariş verildi”, “kullanıcı kayıt oldu”, “ödeme başarısız oldu” gibi.
- Producer (Üretici): Olayı yayınlayan servis.
- Consumer (Tüketici): Olayı dinleyen ve işleyen servis.
Neden EDA Kullanmalısınız?
Klasik senkron mimarilerde bir sorun var: sistemin en yavaş ya da en kırılgan parçası tüm sistemi etkiler. Bir ödeme API’si yavaşladığında sipariş servisi bloklanır. E-posta gönderimi geciktiğinde kullanıcı kayıt akışı durur. Bu tür bağımlılıklar zamanla teknik borç haline gelir.
EDA’nın getirdikleri şunlar:
- Ölçeklenebilirlik: Her consumer bağımsız ölçeklenir. Sipariş işleme yoğunlaşınca sadece o servisi scale edersiniz.
- Dayanıklılık: Bir servis çöktüğünde eventler kaybolmaz, kuyrukta bekler.
- Bağımsız geliştirme: Ekipler birbirlerinden bağımsız çalışabilir. Yeni bir özellik eklemek için mevcut kodu değiştirmenize gerek kalmaz, sadece ilgili eventi dinleyen yeni bir consumer yazarsınız.
- Audit trail: Her olay kayıt altında olduğundan, ne zaman ne olduğunu geriye dönük takip edebilirsiniz.
Temel Mimari Desenler
1. Event Notification Pattern
En basit form. Bir şey olduğunda bildirim gönderilir, detay taşımaz.
# RabbitMQ ile basit bir event yayınlama örneği
# Producer tarafı - Python benzeri pseudocode ile gösteriyoruz
# Önce RabbitMQ container'ı ayağa kaldıralım
docker run -d
--name rabbitmq
-p 5672:5672
-p 15672:15672
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=gizlisifre
rabbitmq:3-management
Bu desende event mesajı minimal tutar: “sipariş 12345 oluşturuldu” bilgisi yeter, detayları consumer kendi gidip çeker.
2. Event-Carried State Transfer Pattern
Event içinde tüm bilgiyi taşırsınız. Consumer, başka bir servise sorgu atmadan işini yapabilir. Büyük sistemlerde ağ trafiğini azaltır ama mesaj boyutunu büyütür.
3. Event Sourcing Pattern
Sistemin durumunu eventlerin sıralı listesi olarak saklarsınız. “Sipariş oluşturuldu -> Ödeme alındı -> Kargoya verildi” gibi. Mevcut durum bu eventlerin replay’iyle hesaplanır. Bu konu başlı başına ayrı bir yazı ister, ama temel mantığı bu.
4. CQRS (Command Query Responsibility Segregation)
Okuma ve yazma operasyonlarını ayırırsınız. Yazma eventleri ayrı bir akışta gider, okuma modeli bu eventlerden güncellenir. EDA ile çok iyi çalışır.
Kafka ile Pratik Uygulama
Gerçek dünyada Kafka, EDA için en yaygın tercih. Neden? Yüksek throughput, kalıcı log yapısı, consumer grupları ile esnek tüketim modeli. Şimdi elimizi kirletelim.
# Kafka ve Zookeeper'ı Docker Compose ile ayağa kaldırma
# docker-compose.yml
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:7.4.0
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
# Compose'u başlatın
docker-compose up -d
# Kafka container'ına bağlanın
docker exec -it <kafka_container_id> bash
# Topic oluşturun
kafka-topics --create
--bootstrap-server localhost:9092
--topic siparis-olaylari
--partitions 3
--replication-factor 1
# Topic listesini görün
kafka-topics --list --bootstrap-server localhost:9092
# Test amaçlı console producer ile mesaj gönderin
kafka-console-producer
--bootstrap-server localhost:9092
--topic siparis-olaylari
--property "key.separator=:"
--property "parse.key=true"
# Şu şekilde mesaj girin (key:value formatında):
# siparis-001:{"event":"SiparisOlusturuldu","siparis_id":"001","musteri_id":"usr-42","tutar":250.00}
# Başka bir terminalde consumer açın
kafka-console-consumer
--bootstrap-server localhost:9092
--topic siparis-olaylari
--group siparis-isleme-grubu
--from-beginning
--property "print.key=true"
--property "key.separator=:"
Burada dikkat edilmesi gereken nokta: --group parametresi. Aynı consumer group içindeki instancelar bir topic’in partitionlarını aralarında paylaşır. Farklı consumer grouplar ise aynı mesajları bağımsız olarak tüketir. Yani sipariş işleme servisi ve e-posta servisi aynı eventi ayrı ayrı, kendi hızlarında okur.
Gerçek Dünya Senaryosu: E-Ticaret Sipariş Akışı
Şöyle bir senaryo düşünelim. Kullanıcı sipariş verdi. Bu tek bir eylem aslında bir düzine işlemi tetikliyor:
- Stok güncellenmeli
- Ödeme işlemi başlatılmalı
- E-posta bildirimi gönderilmeli
- Kargo sistemi bilgilendirilmeli
- Analitik servisi güncellenmeli
- Sadakat puanları hesaplanmalı
Geleneksel yaklaşımda sipariş servisi bunların hepsini sırayla çağırır. Biri takılırsa hepsi takılır. EDA ile sipariş servisi tek bir event yayınlar: SiparisOlusturuldu. Diğer servisler bunu dinler ve kendi işlerini yapar.
# Kafka topic yapısını production benzeri kuralım
# Her domain için ayrı topic
kafka-topics --create
--bootstrap-server localhost:9092
--topic siparis.olusturuldu
--partitions 6
--replication-factor 1
kafka-topics --create
--bootstrap-server localhost:9092
--topic odeme.islendi
--partitions 3
--replication-factor 1
kafka-topics --create
--bootstrap-server localhost:9092
--topic stok.guncellendi
--partitions 3
--replication-factor 1
# Topic detaylarını inceleyin
kafka-topics --describe
--bootstrap-server localhost:9092
--topic siparis.olusturuldu
Topic isimlendirmesinde bir convention belirlemek kritik. Ben domain.olay formatını tercih ediyorum. Bazı ekipler domain-subdomain-event formatını kullanıyor. Önemli olan tutarlılık.
Schema Registry ve Mesaj Kontratı
EDA’nın en sık göz ardı edilen kısmı bu. Producer bir şema değiştirdiğinde consumer çöker. Bu gerçekten başınıza gelecek, söz. Schema Registry bu sorunu çözer.
# Confluent Schema Registry ekleyin
# docker-compose.yml'e eklenecek servis
schema-registry:
image: confluentinc/cp-schema-registry:7.4.0
depends_on:
- kafka
ports:
- "8081:8081"
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka:9092
# Schema Registry'ye Avro şeması kaydedin
curl -X POST
http://localhost:8081/subjects/siparis.olusturuldu-value/versions
-H "Content-Type: application/vnd.schemaregistry.v1+json"
-d '{
"schema": "{"type":"record","name":"SiparisOlusturuldu","fields":[{"name":"siparis_id","type":"string"},{"name":"musteri_id","type":"string"},{"name":"tutar","type":"double"},{"name":"timestamp","type":"long"}]}"
}'
Schema Registry sayesinde şema versiyonlama yapabilir, geriye dönük uyumluluğu zorunlu kılabilirsiniz. Producer yeni bir alan eklediğinde, consumer eski mesajları hala okuyabilir.
Hata Yönetimi: Dead Letter Queue
EDA’da en önemli konulardan biri başarısız mesaj yönetimi. Consumer bir mesajı işleyemediğinde ne olur? Sonsuz döngüye girip tekrar tekrar denemek çözüm değil.
# Dead Letter Topic oluşturun
kafka-topics --create
--bootstrap-server localhost:9092
--topic siparis.olusturuldu.DLT
--partitions 1
--replication-factor 1
# Consumer lag'ı izlemek için
kafka-consumer-groups
--bootstrap-server localhost:9092
--describe
--group siparis-isleme-grubu
# Output şuna benzer bir şey gösterir:
# GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# siparis-isleme-grubu siparis.olusturuldu 0 145 145 0
# siparis-isleme-grubu siparis.olusturuldu 1 203 210 7
Consumer lag, sisteminizin sağlık göstergesidir. Lag artıyorsa consumer’larınız yetişemiyor demektir. Scale etme zamanı gelmiştir ya da işlem sürenizi optimize etmeniz gerekiyordur.
Dead Letter Queue (DLQ) mantığı şu şekilde çalışır: consumer bir mesajı 3 kez (ya da belirlediğiniz sayıda) işlemeye çalışır, başarısız olursa mesajı DLT topic’ine yazar ve ana akışa devam eder. DLT’deki mesajlar ayrı bir süreçle incelenir, hata giderildikten sonra yeniden işleme alınır.
Idempotency: Aynı Mesajı İki Kez İşlemek
Dağıtık sistemlerde mesajlar tekrar teslim edilebilir. Consumer crashleşirse, son işlediği mesajı tekrar alabilir. Bu durumda aynı siparişi iki kez oluşturmak, aynı ödemeyi iki kez çekmek felaket olur.
Idempotency anahtarı bu problemi çözer. Her mesajın benzersiz bir ID’si olur. Consumer, bu ID’yi işlemeden önce bir yerde (Redis, DB) kontrol eder. Daha önce işlendiyse geçer.
# Redis ile idempotency kontrolü için örnek yapı
# Redis'i ayağa kaldırın
docker run -d
--name redis
-p 6379:6379
redis:7-alpine
# Idempotency key'i Redis'e set edin (TTL ile)
# Bu komutu uygulamanızda consumer işlemeden önce çalıştırırsınız
docker exec redis redis-cli SET
"processed:siparis-001"
"1"
EX 86400
NX
# NX flag'i: sadece key yoksa yaz
# EX 86400: 24 saat TTL
# Dönen değer OK ise ilk kez işleniyor, nil ise daha önce işlenmiş
Monitoring ve Observability
EDA’da bir şeyler yanlış gittiğinde nerede yanlış gittiğini bulmak zor olabilir. Mesaj A’dan B’ye, B’den C’ye geçti, C’de bir şey yanlış oldu ama siz A’da duruyorsunuz. Distributed tracing burada hayat kurtarır.
Her event’e bir correlation_id ve causation_id ekleyin. correlation_id başlangıçta oluşturulur ve tüm event zincirine taşınır. causation_id ise bu eventi tetikleyen eventi gösterir. Bu iki field sayesinde bir siparişin başından sonuna tüm yolculuğunu takip edebilirsiniz.
# Kafka topic metriklerini izlemek için
# JMX exporter ile Prometheus entegrasyonu
# Temel metrikler:
# Topic'e gelen mesaj sayısı
kafka_topic_partition_current_offset
# Consumer group lag
kafka_consumergroup_lag
# Broker'daki mesaj üretim hızı
kafka_server_brokertopicmetrics_messagesinpersec_rate
# Bu metrikleri Grafana'da dashboard'a ekleyin
# Kritik alarm thresholdları:
# - Consumer lag > 10000: uyarı
# - Consumer lag > 50000: kritik
# - Producer error rate > %1: uyarı
Prometheus ve Grafana entegrasyonu için JMX Exporter’ı Kafka broker’larınıza eklemek gerekir. Bu kurulumu atlarsanız, sistemin içinde ne döndüğünü göremezsiniz.
EDA’nın Zorlukları ve Tuzakları
Dürüst olmak gerekirse, EDA her şeyin çözümü değil. Dikkat etmeniz gereken noktalar var:
- Eventual consistency: Sistemin her parçası anlık olarak tutarlı olmayabilir. Sipariş verildi ama stok servisi henüz güncellemedi. Bu durumu tasarımınızda kabul etmeniz gerekir.
- Event sıralaması: Partition sayısı arttıkça global sıra garantisi yoktur. Aynı partition içinde sıra garantisi vardır, bu yüzden aynı siparişe ait eventleri aynı partition’a yönlendirmeniz (key kullanarak) önemlidir.
- Şema evrimiyonu: Tüm consumer’lar update edilmeden producer şemasını değiştiremezsiniz. Bunu atlayan ekipler gece yarısı production olaylarıyla karşılaşır.
- Debugging zorluğu: Senkron sistemlere göre hata takibi daha karmaşıktır. Distributed tracing olmadan EDA’yı production’da çalıştırmayın.
- İşlem yönetimi: Birden fazla serviste atomik işlem yapmak istediğinizde Saga Pattern devreye girer. Bu da ayrı bir yazı konusu.
Küçük takımlar veya basit iş akışları için EDA aşırı mühendislik olabilir. Karmaşıklığın getirdiği operasyonel yükü de hesaba katın. Kafka cluster’ını yönetmek, Schema Registry’yi tutmak, consumer gruplarını izlemek operasyonel efor gerektirir.
Ne Zaman EDA Tercih Etmeli?
- Birden fazla servisin aynı olayı işlemesi gerekiyorsa
- Servisler arası bağımlılığı azaltmak istiyorsanız
- Yüksek throughput ve asenkron işlem gerektiren senaryolarda
- Audit log ve event replay ihtiyacı varsa
- Farklı hızlarda çalışan servisler arasında tampon gerekiyorsa
EDA’ya geçmeyin eğer:
- Küçük, tek servisli bir uygulamanız varsa
- İşlem tutarlılığı (ACID) kritik ve her adımda gerekli ise
- Ekibiniz dağıtık sistem deneyiminden yoksunsa
- Operasyonel kapasitiniz sınırlıysa
Sonuç
Event Driven Architecture, doğru kullanıldığında sistemleri gerçekten dönüştürüyor. Servisler arasındaki o sıkı bağı koparıyor, her birinin bağımsız büyümesine imkan tanıyor. Ama bu özgürlüğün bir bedeli var: operasyonel karmaşıklık, eventual consistency ile yaşamayı öğrenmek ve distributed sistemlerin doğasında olan belirsizliği yönetmek.
Başlangıç için bir öneri: önce tek bir kritik akışı EDA ile tasarlayın. Sipariş bildirimleri iyi bir başlangıç noktasıdır. Kafka’yı kurun, bir producer bir consumer yazın, consumer lag’ı izleyin. Sistemin nasıl davrandığını, mesajların nasıl aktığını anlayın. Sonra DLQ ekleyin, idempotency ekleyin, monitoring ekleyin. Her adımda öğrendikleriniz sizi bir sonraki adıma hazırlar.
EDA bir hedef değil, bir araç. Probleminizi çözüyor mu? Getirdiği karmaşıklığı taşıyabilecek misiniz? Bu soruların cevabı evet ise, doğru yoldasınız.
