Podstawy EDA: zdarzenia domeny, polecenia i magistrala komunikatów
Wyobraź sobie system e-commerce: kiedy klient realizuje zamówienie, musi to nastąpić wiele rzeczy równolegle — należy zmniejszyć stan zapasów, wysłać powiadomienie e-mailem, należy powiadomić zespół ds. realizacji zamówień i zaktualizować system lojalnościowy. Tak czy koordynują te usługi? Klasyczne podejście polega na wywołaniu usługi Zamówienia wszystkie inne usługi bezpośrednio: ścisłe powiązanie, kruchość, niemożliwość wspinać się samodzielnie.
L'Architektura sterowana zdarzeniami (EDA) obala ten paradygmat: służba
Orders publikuje wydarzenie OrderPlaced na magistrali komunikatów i wszystkich usługach
zainteresowane strony konsumują je niezależnie. Służba Zamówień nie wie, kto ich słucha, nie
oczekuje odpowiedzi, nie jest to uzależnione od ich dostępności. To oddzielenie i
podstawowa zasada, na której budowane są skalowalne i odporne systemy rozproszone.
Czego się nauczysz
- Różnica między zdarzeniami domeny, poleceniami i zapytaniami w EDA
- Wzorzec publikowania i subskrybowania: oddzieleni producenci i konsumenci
- Magistrala komunikatów, magistrala zdarzeń i kolejka komunikatów: kiedy czego używać
- Schemat zdarzeń: struktura, wersjonowanie i standardowe CloudEvents
- Korzyści i kompromisy EDA w porównaniu z synchronicznym REST
- Implementacja prostego systemu EDA w TypeScript
- Jak zdecydować, kiedy stosować EDA, a kiedy nie
Trzy typy wiadomości w EDA
Nie wszystkie komunikaty w systemie sterowanym zdarzeniami są takie same. Zrozum różnicę pomiędzy zdarzeniami, poleceniami i zapytaniami oraz pierwszym krokiem do zaprojektowania poprawnych systemów EDA:
| Typ | Opis | Kierunek | Odpowiedź | Przykład |
|---|---|---|---|---|
| Zdarzenie domeny | Coś, co wydarzyło się w domenie | 1 → N (transmisja) | No | OrderPlaced, PaymentReceived |
| Rozkaz | Prośba o wykonanie akcji | 1 → 1 (punkt do punktu) | Opcjonalne (asynchroniczne potwierdzenie) | PlaceOrder, SendEmail |
| Zapytania | Jedno żądanie danych (asynchroniczne EDA) | 1 → 1 z odpowiedzią | Tak (kolejka odpowiedzi) | GetOrderStatus poprzez kolejkę odpowiedzi |
Wydarzenia domeny: serce EDA
Un Zdarzenie domeny opisuje coś, co już wydarzyło się w domenie biznesowej. Kluczowe właściwości:
- Niezmienny: opisuje przeszłość, która nie zmienia się po publikacji
- Nazwany w przeszłości:
OrderPlaced, NiePlaceOrder - Samodzielny: zawiera wszystkie dane niezbędne konsumentom
- Wpisano: każdy typ zdarzenia ma określony wzorzec
// 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(),
},
};
}
Wzór publikowania i subskrybowania
Wzór Publikuj-Subskrybuj oraz fundament EDA: wydawca (producent) wysyła zdarzenia na szynę komunikatów, nie wiedząc, kto je odbiera; abonenci (konsumenci) rejestrują się, aby otrzymywać określone rodzaje wydarzeń, nie wiedząc, kto je publikuje.
// 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));
Magistrala komunikatów, magistrala zdarzeń i kolejka komunikatów: różnice
Terminy te są często używane zamiennie, ale mają określone znaczenie:
- Kolejka wiadomości: Kolejka punkt-punkt. Wiadomość jest dostarczana do tylko jeden konsument. Przykład: Standardowa kolejka SQS
- Autobus eventowy: Transmisja do wszyscy abonenci. Każdy subskrybent otrzymuje kopię wydarzenia. Przykład: AWS EventBridge, temat SNS
- Autobus wiadomości: Termin ogólny obejmujący zarówno kolejkę, jak i temat. W praktyce: broker zarządzający routingiem wiadomości (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 schematów zdarzeń
Wydarzenia w chmurze oraz specyfikacja CNCF, która standaryzuje strukturę zdarzeń pomiędzy różnymi systemami. Przyjęcie go ułatwia interoperacyjność i upraszcza narzędzia monitorowanie i debugowanie:
// 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");
}
Wersjonowanie wydarzeń
Zdarzenia są wykorzystywane niezależnie przez wiele usług. Zmień wzór wydarzenia bez strategii wersjonowania psuje konsumentów. Główne wzory:
// 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
Korzyści i kompromisy EDA
Kiedy stosować EDA
- Niezbędne oddzielenie: Gdy chcesz dodać nowych odbiorców bez zmiany wydawcy
- Niezależna skalowalność: Różni odbiorcy z różnymi obciążeniami skalują się osobno
- Ścieżki audytu: Zdarzenia niezmienne są naturalnym zapisem wszystkiego, co wydarzyło się w systemie
- Odporność na awarie: Jeśli odbiorca nie działa, szyna komunikatów wstrzymuje komunikaty do czasu ich ponownego uruchomienia
- Integracja międzysystemowa: Systemy heterogeniczne komunikujące się za pośrednictwem standardowych zdarzeń
Kiedy NIE stosować EDA
- Wymagana natychmiastowa reakcja: Jeśli użytkownik musi czekać na wynik synchroniczny, EDA dodaje niepotrzebne opóźnienia i złożoność
- Proste systemy: Monolit z kilkoma funkcjami nie korzysta z narzutu brokera komunikatów
- Proste transakcje rozproszone: W przypadku operacji, które muszą być niepodzielne w wielu usługach, EDA wymaga wzorca Saga (duża złożoność)
- Mały zespół bez doświadczenia w EDA: Krzywa uczenia się jest znacząca. Zacznij od REST i w razie potrzeby dodaj EDA
Kompletny przepływ: przykład handlu elektronicznego
// 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
Wnioski i dalsze kroki
EDA to zmiana paradygmatu: z systemu, w którym usługi „dzwonią” do siebie nawzajem system, w którym usługi „komunikują się poprzez zdarzenia”. Zysk odsprzęgania, Skalowalność i odporność są realne, ale wymagają stawienia czoła nowym wyzwaniom: zarządzaniu błędów staje się asynchroniczne, debugowanie wymaga identyfikatora korelacji i śledzenia rozproszonego, spójność musi stać się „ostateczna”.
Kolejne artykuły z tej serii dotyczą zaawansowanych wzorców tworzących EDA opłacalne w produkcji: Event Sourcing dla niezmiennego stanu, CQRS dla separacji odczyt/zapis, Saga dla transakcji rozproszonych i narzędzia AWS (EventBridge, SQS, SNS) w celu wdrożenia ich w środowiskach chmurowych.
Nadchodzące artykuły z serii Architektura sterowana zdarzeniami
- Źródło zdarzeń: stan jako niezmienna sekwencja zdarzeń
- CQRS: Oddzielny odczyt i zapis dla niezależnego skalowania
- Wzór Saga: transakcje rozproszone z choreografią i orkiestracją
- AWS EventBridge: bezserwerowa magistrala zdarzeń i routing oparty na treści
- Kolejka niedostarczonych listów i odporność w systemach asynchronicznych
Powiązane serie
- Apache Kafka i przetwarzanie strumieniowe — Kafka jako szkielet dla masowych systemów EDA
- Kubernetes na dużą skalę — koordynować mikrousługi EDA na platformie Kubernetes
- Praktyczna architektura oprogramowania — gdy EDA vs REST, monolit vs mikrousługi







