EDA Temelleri: Etki Alanı Olayları, Komutlar ve Mesaj Veriyolu
Bir e-ticaret sistemi hayal edin: Bir müşteri bir siparişi tamamladığında bunun gerçekleşmesi gerekir Pek çok şey paralel olarak yapılıyor; envanterin azaltılması gerekiyor, e-posta bildiriminin gönderilmesi gerekiyor, yerine getirme ekibine bilgi verilmeli, sadakat sistemi güncellenmelidir. Evet bu hizmetleri koordine ediyorlar mı? Siparişler hizmetinin aranması için klasik bir yaklaşım diğer tüm hizmetler doğrudan: sıkı bağlantı, kırılganlık, imkansızlık bağımsız olarak tırmanmak.
L'Olay Odaklı Mimari (EDA) bu paradigmayı alt üst ediyor: hizmet
Siparişler bir etkinlik yayınlıyor OrderPlaced bir mesaj veriyolunda ve tüm servislerde
İlgili taraflar bunu bağımsız olarak tüketirler. Siparişler hizmeti onları kimin dinlediğini bilmiyor,
cevapları bekler, onların müsaitlik durumuna bağlı değildir. Bu ayrıştırma ve
ölçeklenebilir ve esnek dağıtılmış sistemlerin üzerine inşa edildiği temel prensip.
Ne Öğreneceksiniz
- EDA'da Etki Alanı Olayları, Komutları ve Sorguları arasındaki fark
- Yayınla-Abone Ol Modeli: ayrıştırılmış üreticiler ve tüketiciler
- Mesaj Veriyolu, Olay Veriyolu ve Mesaj Kuyruğu: ne zaman kullanılmalı, ne kullanılmalı
- Olay şeması: yapı, sürüm oluşturma ve standart CloudEvents
- EDA'nın eşzamanlı REST'e göre avantajları ve avantajları
- TypeScript'te basit bir EDA sistemi uygulama
- EDA'nın ne zaman kullanılacağına ve ne zaman kullanılmayacağına nasıl karar verilir?
EDA'daki Üç Mesaj Türü
Olay odaklı bir sistemdeki tüm mesajlar aynı değildir. Ayrımı anlayın olaylar, komutlar ve sorgular arasında ve doğru EDA sistemlerini tasarlamanın ilk adımı:
| Tip | Tanım | Yön | Cevap | Örnek |
|---|---|---|---|---|
| Etki Alanı Etkinliği | Etki alanında meydana gelen bir şey | 1 → N (yayın) | No | OrderPlaced, PaymentReceived |
| Emretmek | Bir eylemi gerçekleştirme isteği | 1 → 1 (noktadan noktaya) | İsteğe bağlı (zaman uyumsuz ACK) | PlaceOrder, SendEmail |
| Sorgular | Veriler için bir istek (asenkron EDA) | 1 → 1 yanıtla | Evet (yanıt kuyruğu) | GetOrderStatus yanıt kuyruğu aracılığıyla |
Etki Alanı Etkinlikleri: EDA'nın Kalbi
Un Etki Alanı Etkinliği iş alanında zaten olmuş bir şeyi anlatır. Anahtar özellikler:
- Değişmez: geçmişi anlatır, yayınlandıktan sonra değişmez
- Geçmişte adı geçenler:
OrderPlaced, OlumsuzPlaceOrder - Kendi kendine yeten: tüketiciler için gerekli tüm verileri içerir
- Yazıldı: her olay türünün tanımlanmış bir modeli vardır
// TypeScript: struttura di un Domain Event
interface DomainEvent {
eventId: string; // ID unico dell'evento (UUID)
eventType: string; // nome del tipo evento
occurredAt: string; // timestamp ISO 8601 (immutabile)
aggregateId: string; // ID dell'aggregato che ha generato l'evento
aggregateType: string; // tipo dell'aggregato (es. "Order")
version: number; // versione dello schema evento (per evoluzione)
payload: unknown; // dati specifici dell'evento
metadata?: {
correlationId?: string; // ID per tracciare la catena di eventi
causationId?: string; // ID del messaggio che ha causato questo evento
userId?: string; // utente che ha innescato l'azione
};
}
// Evento concreto: OrderPlaced
interface OrderPlacedEvent extends DomainEvent {
eventType: 'OrderPlaced';
aggregateType: 'Order';
payload: {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
unitPrice: number;
}>;
totalAmount: number;
currency: string;
shippingAddress: {
street: string;
city: string;
country: string;
};
};
}
// Creare un OrderPlaced event
function createOrderPlacedEvent(order: Order): OrderPlacedEvent {
return {
eventId: crypto.randomUUID(),
eventType: 'OrderPlaced',
occurredAt: new Date().toISOString(),
aggregateId: order.id,
aggregateType: 'Order',
version: 1,
payload: {
orderId: order.id,
customerId: order.customerId,
items: order.items,
totalAmount: order.totalAmount,
currency: order.currency,
shippingAddress: order.shippingAddress,
},
metadata: {
correlationId: crypto.randomUUID(),
},
};
}
Yayınla-Abone Ol Modeli
Desen Yayınla-Abone Ol ve EDA'nın temeli: yayıncı (yapımcı) olayları kimin aldığını bilmeden mesaj veriyoluna gönderir; aboneler (tüketiciler) kimin yayınladığını bilmeden belirli türdeki etkinlikleri almak için kaydolurlar.
// Implementazione semplice di un Event Bus in memoria (per test/sviluppo)
type EventHandler<T extends DomainEvent> = (event: T) => Promise<void>;
class InMemoryEventBus {
private handlers = new Map<string, EventHandler<DomainEvent>[]>();
subscribe<T extends DomainEvent>(eventType: string, handler: EventHandler<T>): void {
const existing = this.handlers.get(eventType) ?? [];
this.handlers.set(eventType, [...existing, handler as EventHandler<DomainEvent>]);
}
async publish(event: DomainEvent): Promise<void> {
const eventHandlers = this.handlers.get(event.eventType) ?? [];
// Pubblica in parallelo a tutti i subscriber
await Promise.allSettled(
eventHandlers.map((handler) => handler(event))
);
}
async publishAll(events: DomainEvent[]): Promise<void> {
for (const event of events) {
await this.publish(event);
}
}
}
// Utilizzo:
const eventBus = new InMemoryEventBus();
// Inventory Service si registra per OrderPlaced
eventBus.subscribe<OrderPlacedEvent>('OrderPlaced', async (event) => {
console.log(`Decrementing inventory for order ${event.payload.orderId}`);
for (const item of event.payload.items) {
await inventoryService.decrement(item.productId, item.quantity);
}
});
// Email Service si registra per OrderPlaced
eventBus.subscribe<OrderPlacedEvent>('OrderPlaced', async (event) => {
await emailService.sendOrderConfirmation(
event.payload.customerId,
event.payload.orderId
);
});
// Order Service pubblica l'evento (non conosce i subscriber)
await eventBus.publish(createOrderPlacedEvent(placedOrder));
Mesaj Veriyolu, Olay Veriyolu ve Mesaj Kuyruğu: Farklar
Terimler sıklıkla birbirinin yerine kullanılır ancak belirli anlamları vardır:
- Mesaj Sırası: Noktadan noktaya kuyruk. adresine bir mesaj iletildi sadece bir tane tüketici. Örnek: SQS Standart Kuyruğu
- Olay Otobüsü: Yayınla herkes aboneler. Her abone etkinliğin bir kopyasını alır. Örnek: AWS EventBridge, SNS Konusu
- Mesaj Veriyolu: Hem kuyruğu hem de konuyu içeren genel terim. Uygulamada: mesaj yönlendirmeyi yöneten bir komisyoncu (RabbitMQ, Kafka)
// Esempio: stessa logica su AWS SQS + SNS (architettura fan-out comune)
// Pattern fan-out: SNS Topic + SQS Queue per ogni consumer
// 1. Pubblica su SNS Topic
// 2. SNS consegna a tutte le SQS Queue sottoscritte
// 3. Ogni servizio legge dalla propria SQS Queue indipendentemente
// Terraform per il fan-out pattern:
resource "aws_sns_topic" "order_events" {
name = "order-events"
}
resource "aws_sqs_queue" "inventory_queue" {
name = "inventory-order-events"
}
resource "aws_sqs_queue" "email_queue" {
name = "email-order-events"
}
resource "aws_sns_topic_subscription" "inventory" {
topic_arn = aws_sns_topic.order_events.arn
protocol = "sqs"
endpoint = aws_sqs_queue.inventory_queue.arn
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.order_events.arn
protocol = "sqs"
endpoint = aws_sqs_queue.email_queue.arn
}
CloudEvents: Etkinlik Şemaları Standardı
Bulut Etkinlikleri ve yapısını standartlaştıran bir CNCF spesifikasyonu Farklı sistemler arasındaki olaylar. Bunu benimsemek birlikte çalışabilirliği kolaylaştırır ve araçları basitleştirir izleme ve hata ayıklama:
// CloudEvents v1.0 - struttura standard
{
"specversion": "1.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "com.company.order.placed", // Reverse DNS + evento
"source": "/orders-service/v1", // URI del servizio sorgente
"subject": "order-789", // identificativo della risorsa
"time": "2026-03-20T10:30:00Z", // timestamp ISO 8601
"datacontenttype": "application/json",
"dataschema": "https://schemas.company.com/order/placed/v1.json",
"data": {
"orderId": "order-789",
"customerId": "cust-123",
"totalAmount": 150.00,
"currency": "EUR"
}
}
// TypeScript: creare un CloudEvent con la SDK ufficiale
import { CloudEvent } from "cloudevents";
const event = new CloudEvent({
specversion: "1.0",
type: "com.company.order.placed",
source: "/orders-service/v1",
subject: `order-${orderId}`,
datacontenttype: "application/json",
dataschema: "https://schemas.company.com/order/placed/v1.json",
data: {
orderId: order.id,
customerId: order.customerId,
totalAmount: order.totalAmount,
currency: order.currency,
},
});
// Valida il CloudEvent prima di pubblicarlo
if (!event.source || !event.type) {
throw new Error("CloudEvent validation failed: missing required fields");
}
Olayların Sürümlenmesi
Etkinlikler birden fazla hizmet tarafından bağımsız olarak tüketilir. Deseni değiştir Sürüm oluşturma stratejisi olmayan bir olayın tüketicilere zarar vermesi. Ana desenler:
// Pattern 1: Versioning nel tipo evento
// Vecchi consumer continuano a ricevere v1, nuovi consumer si registrano per v2
eventBus.subscribe('OrderPlaced.v1', handleOrderPlacedV1);
eventBus.subscribe('OrderPlaced.v2', handleOrderPlacedV2);
// Pattern 2: Backward-compatible changes (aggiunta di campi opzionali)
// SAFE: aggiungere nuovi campi opzionali (consumer ignorano i campi sconosciuti)
interface OrderPlacedEventV1 {
orderId: string;
customerId: string;
totalAmount: number;
}
interface OrderPlacedEventV2 extends OrderPlacedEventV1 {
// Aggiunto in V2: opzionale, backward-compatible
estimatedDeliveryDate?: string;
loyaltyPointsEarned?: number;
}
// Pattern 3: Parallel publishing (per breaking changes)
// Pubblica sia v1 che v2 per un periodo di transizione
async function publishOrderPlaced(order: Order): Promise<void> {
const v1Event = createOrderPlacedV1(order);
const v2Event = createOrderPlacedV2(order);
await Promise.all([
eventBus.publish(v1Event), // per consumer legacy
eventBus.publish(v2Event), // per consumer aggiornati
]);
}
// NEVER: rimuovere campi, cambiare tipi, rinominare campi obbligatori
// -> breaking change: migra prima tutti i consumer poi rimuovi v1
EDA'nın Faydaları ve Avantajları
EDA Ne Zaman Kullanılır?
- Gerekli ayırma: Yayıncıyı değiştirmeden yeni tüketiciler eklemek istediğinizde
- Bağımsız ölçeklenebilirlik: Farklı yüklere sahip farklı tüketiciler ayrı ayrı ölçeklendirilir
- Denetim izleri: Değişmez olaylar sistemde olup biten her şeyin doğal kaydıdır
- Başarısızlığa karşı dayanıklılık: Bir tüketici kapalıysa, mesaj veri yolu mesajları tekrar gelene kadar tutar
- Sistemler arası entegrasyon: Standart olaylar aracılığıyla iletişim kuran heterojen sistemler
EDA ne zaman kullanılmamalıdır?
- Acil yanıt gerekli: Kullanıcının eşzamanlı sonucu beklemesi gerekiyorsa EDA gereksiz gecikme ve karmaşıklık ekler
- Basit sistemler: Az sayıda özelliğe sahip bir monolit, mesaj komisyoncusunun ek yükünden faydalanmaz
- Basit dağıtılmış işlemler: Birden fazla hizmette atomik olması gereken işlemler için EDA, Saga modelini gerektirir (yüksek karmaşıklık)
- EDA deneyimi olmayan küçük ekip: Öğrenme eğrisi önemlidir. REST ile başlayın ve gerektiğinde EDA'yı ekleyin
Akışın Tamamı: e-Ticaret Örneği
// Flusso completo EDA per un ordine e-commerce
// 1. Order Service: riceve HTTP POST /orders
// 2. Valida, persiste, pubblica evento
class OrderService {
constructor(
private readonly orderRepo: OrderRepository,
private readonly eventBus: EventBus
) {}
async placeOrder(dto: PlaceOrderDto): Promise<Order> {
// Logica business: crea l'ordine
const order = Order.create(dto);
// Persisti nel database
await this.orderRepo.save(order);
// Pubblica gli eventi generati dall'aggregato
const events = order.getUncommittedEvents();
await this.eventBus.publishAll(events);
order.clearEvents();
return order;
}
}
// 3. Inventory Service: ascolta OrderPlaced
// - Scala indipendentemente con 5 consumer paralleli
// - Se giu, i messaggi si accumulano nella queue
// 4. Email Service: ascolta OrderPlaced
// - Invia email di conferma
// - Se fallisce, il messaggio va in DLQ per retry
// 5. Loyalty Service: ascolta OrderPlaced
// - Calcola e aggiunge punti fedeltà
// - Pubblica LoyaltyPointsEarned
// 6. Analytics Service: ascolta OrderPlaced + LoyaltyPointsEarned
// - Aggiorna le metriche in tempo reale
// Il servizio Order non sa niente di tutto questo!
// Aggiungere un nuovo consumer = zero modifiche al publisher
Sonuçlar ve Sonraki Adımlar
EDA bir paradigma değişimidir: hizmetlerin birbirini "çağırdığı" bir sistemden hizmetlerin "olaylar aracılığıyla iletişim kurduğu" bir sistem. Ayrışma kazancı, Ölçeklenebilirlik ve esneklik gerçektir ancak yeni zorluklarla yüzleşmeyi gerektirir: yönetim Hataların çoğu eşzamansız hale gelir, hata ayıklama için korelasyon kimliği ve dağıtılmış izleme gerekir, tutarlılık "nihai" hale gelmelidir.
Bu serideki sonraki makaleler EDA'yı oluşturan gelişmiş modelleri ele almaktadır. üretimde uygulanabilir: Değişmez durum için Event Sourcing, ayırma için CQRS okuma/yazma, dağıtılmış işlemler için Saga ve AWS araçları (EventBridge, SQS, SNS) bunları bulut ortamlarında uygulamak.
Olay Odaklı Mimari Serisinde Gelecek Makaleler
- Olay Kaynak Kullanımı: Değişmez Bir Olaylar Dizisi Olarak Durum
- CQRS: Bağımsız Ölçeklendirme için Ayrı Okuma ve Yazma
- Efsane Deseni: Koreografi ve Orkestrasyonla Dağıtılmış İşlemler
- AWS EventBridge: Sunucusuz Olay Veriyolu ve İçerik Tabanlı Yönlendirme
- Asenkron Sistemlerde Teslim Edilmeyen Mektup Kuyruğu ve Dayanıklılık
İlgili Seriler
- Apache Kafka ve Akış İşleme — Yüksek hacimli EDA sistemlerinin omurgası olarak Kafka
- Geniş Ölçekte Kubernet'ler — Kubernetes'te EDA mikro hizmetlerini düzenleyin
- Pratik Yazılım Mimarisi — EDA ve REST, monolit ve mikro hizmetler karşılaştırıldığında







