Základy EDA: Doménové události, příkazy a sběrnice zpráv
Představte si systém elektronického obchodu: když zákazník dokončí objednávku, musí se tak stát mnoho věcí souběžně – je třeba snížit zásoby, je třeba odeslat e-mailové upozornění, musí být informován realizační tým, musí být aktualizován věrnostní systém. Ano koordinují tyto služby? Klasickým přístupem je volání služby Objednávky všechny ostatní služby přímo: těsné spojení, křehkost, nemožnost samostatně lézt.
L'Event-Driven Architecture (EDA) převrací toto paradigma: služba
Objednávky zveřejňují událost OrderPlaced na sběrnici zpráv a všechny služby
zájemci jej spotřebují samostatně. Služba Objednávky neví, kdo je poslouchá, ne
čeká na odpovědi, nezávisí na jejich dostupnosti. Toto oddělení a
základní princip, na kterém jsou vybudovány škálovatelné a odolné distribuované systémy.
Co se naučíte
- Rozdíl mezi doménovými událostmi, příkazy a dotazy v EDA
- Vzor Publish-Subscribe: oddělení producenti a spotřebitelé
- Message Bus, Event Bus a Message Queue: kdy co použít
- Schéma události: struktura, verzování a standardní CloudEvents
- Výhody a kompromisy EDA versus synchronní REST
- Implementace jednoduchého systému EDA v TypeScriptu
- Jak se rozhodnout, kdy EDA používat a kdy ne
Tři typy zpráv v EDA
Ne všechny zprávy v systému řízeném událostmi jsou stejné. Pochopte rozdíl mezi událostmi, příkazy a dotazy a prvním krokem k návrhu správných systémů EDA:
| Typ | Popis | Směr | Odpověď | Příklad |
|---|---|---|---|---|
| Doménová událost | Něco, co se stalo v doméně | 1 → N (vysílání) | No | OrderPlaced, PaymentReceived |
| Příkaz | Požadavek na provedení akce | 1 → 1 (z bodu do bodu) | Volitelné (asynchronní ACK) | PlaceOrder, SendEmail |
| Dotazy | Jeden požadavek na data (asynchronní EDA) | 1 → 1 s odpovědí | Ano (fronta odpovědí) | GetOrderStatus prostřednictvím fronty odpovědí |
Doménové události: Srdce EDA
Un Doménová událost popisuje něco, co se již v obchodní doméně stalo. Klíčové vlastnosti:
- Neměnný: popisuje minulost, po zveřejnění se nemění
- Pojmenováno v minulosti:
OrderPlaced, NePlaceOrder - Samostatný: obsahuje všechny údaje nezbytné pro spotřebitele
- Napsáno: každý typ události má definovaný vzor
// 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(),
},
};
}
Vzor publikování a odběru
Vzor Publikovat-Odebírat a založení EDA: vydavatel (producent) odesílá události do sběrnice zpráv, aniž by věděl, kdo je přijímá; předplatitelé (spotřebitelé) se registrují k odběru určitých typů akcí, aniž by věděli, kdo je zveřejňuje.
// 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));
Sběrnice zpráv, sběrnice událostí a fronta zpráv: Rozdíly
Termíny se často používají zaměnitelně, ale mají konkrétní význam:
- Fronta zpráv: Fronta z bodu do bodu. Zpráva je doručena na pouze jeden spotřebitel. Příklad: Standardní fronta SQS
- Event Bus: Vysílat do každý předplatitelé. Každý předplatitel obdrží kopii události. Příklad: AWS EventBridge, téma SNS
- Sběrnice zpráv: Obecný termín, který zahrnuje jak frontu, tak téma. V praxi: broker, který spravuje směrování zpráv (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: Standard pro schémata událostí
CloudEvents a specifikace CNCF, která standardizuje strukturu události mezi různými systémy. Jeho přijetí usnadňuje interoperabilitu a zjednodušuje nástroje sledování a ladění:
// 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");
}
Verze událostí
Události jsou spotřebovávány více službami nezávisle. Změňte vzor událost bez strategie verzování spotřebitele zlomí. Hlavní vzory:
// 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
Výhody a kompromisy EDA
Kdy použít EDA
- Nutné oddělení: Když chcete přidat nové zákazníky, aniž byste měnili vydavatele
- Nezávislá škálovatelnost: Různí spotřebitelé s různou zátěží se škálují samostatně
- Auditní stopy: Neměnné události jsou přirozeným záznamem všeho, co se v systému stalo
- Odolnost proti selhání: Pokud je spotřebitel mimo provoz, sběrnice zpráv uchovává zprávy, dokud se nevrátí zpět
- Integrace mezi systémy: Heterogenní systémy, které komunikují prostřednictvím standardních událostí
Kdy NEPOUŽÍVAT EDA
- Vyžaduje se okamžitá odpověď: Pokud musí uživatel čekat na synchronní výsledek, EDA přidává zbytečnou latenci a složitost
- Jednoduché systémy: Monolit s několika funkcemi netěží z režie zprostředkovatele zpráv
- Jednoduché distribuované transakce: Pro operace, které musí být atomické napříč více službami, vyžaduje EDA vzor Saga (vysoká složitost)
- Malý tým bez zkušeností EDA: Významná je křivka učení. Začněte s REST a přidejte EDA tam, kde je potřeba
Kompletní tok: Příklad elektronického obchodu
// 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
Závěry a další kroky
EDA je změnou paradigmatu: od systému, do kterého si služby navzájem „telefonují“. systém, kde služby „komunikují prostřednictvím událostí“. zisk z oddělení, škálovatelnost a odolnost je skutečná, ale vyžaduje čelit novým výzvám: řízení chyb se stane asynchronním, ladění vyžaduje ID korelace a distribuované trasování, konzistence se musí stát „nahodnou“.
Další články této série se zabývají pokročilými vzory, které tvoří EDA životaschopné ve výrobě: Event Sourcing pro neměnný stav, CQRS pro separaci čtení/zápis, Saga pro distribuované transakce a nástroje AWS (EventBridge, SQS, SNS) implementovat je v cloudových prostředích.
Připravované články ze série Event-Driven Architecture Series
- Sourcing událostí: Stav jako neměnná sekvence událostí
- CQRS: Samostatné čtení a zápis pro nezávislé škálování
- Saga Pattern: Distribuované transakce s choreografií a orchestrací
- AWS EventBridge: Sběrnice událostí bez serveru a směrování založené na obsahu
- Fronta nedoručených dopisů a odolnost v asynchronních systémech
Související série
- Apache Kafka a zpracování streamu — Kafka jako páteř pro velkoobjemové systémy EDA
- Kubernetes ve společnosti Scale — organizovat mikroslužby EDA na Kubernetes
- Praktická softwarová architektura — když EDA vs REST, monolit vs mikroslužby







