Fundamentele EDA: evenimente de domeniu, comenzi și magistrală de mesaje
Imaginați-vă un sistem de comerț electronic: atunci când un client finalizează o comandă, trebuie să se întâmple multe lucruri în paralel — inventarul trebuie să fie redus, notificarea prin e-mail trebuie trimisă, echipa de realizare trebuie anunțată, sistemul de fidelizare trebuie actualizat. Da coordonează aceste servicii? O abordare clasică este ca serviciul Comenzi să sune toate celelalte servicii direct: cuplare strânsă, fragilitate, imposibilitate să urce independent.
L'Arhitectură bazată pe evenimente (EDA) răstoarnă această paradigmă: serviciul
Orders publică un eveniment OrderPlaced pe o magistrală de mesaje și toate serviciile
părțile interesate îl consumă în mod independent. Serviciul Comenzi nu știe cine îi ascultă, nu
așteaptă răspunsuri, nu depinde de disponibilitatea acestora. Această decuplare și
principiu fundamental pe care sunt construite sistemele distribuite scalabile și rezistente.
Ce vei învăța
- Diferența dintre evenimente de domeniu, comenzi și interogări în EDA
- Model Publicare-Abonare: producători și consumatori decuplați
- Message Bus, Event Bus și Message Queue: când să folosiți ce
- Schema de evenimente: structură, versiunea și CloudEvents standard
- Beneficiile și compromisurile EDA față de REST sincron
- Implementarea unui sistem EDA simplu în TypeScript
- Cum să decizi când să folosești EDA și când să nu-l folosești
Cele trei tipuri de mesaje în EDA
Nu toate mesajele dintr-un sistem bazat pe evenimente sunt la fel. Înțelegeți distincția între evenimente, comenzi și interogări și primul pas pentru a proiecta sisteme EDA corecte:
| Tip | Descriere | Direcţie | Răspuns | Exemplu |
|---|---|---|---|---|
| Eveniment de domeniu | Ceva care s-a întâmplat în domeniu | 1 → N (difuzare) | No | OrderPlaced, PaymentReceived |
| Comanda | O cerere de a efectua o acțiune | 1 → 1 (punct la punct) | Opțional (ACK asincron) | PlaceOrder, SendEmail |
| Interogări | O singură solicitare de date (EDA asincron) | 1 → 1 cu răspuns | Da (coada de răspunsuri) | GetOrderStatus prin coada de răspunsuri |
Evenimente de domeniu: inima EDA
Un Eveniment de domeniu descrie ceva ce sa întâmplat deja în domeniul afacerilor. Proprietățile cheie:
- Imuabil: descrie trecutul, nu se schimbă după publicare
- Numit în trecut:
OrderPlaced, NuPlaceOrder - autonom: conține toate datele necesare consumatorilor
- Tastat: fiecare tip de eveniment are un model definit
// 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(),
},
};
}
Model Publicare-Abonare
Modelul Publicare-Abonare și fundația EDA: editorul (producător) trimite evenimente către magistrala de mesaje fără a ști cine le primește; abonatii (consumatorii) se înregistrează pentru a primi anumite tipuri de evenimente fără a ști cine le publică.
// 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));
Message Bus, Event Bus și Message Queue: Diferențele
Termenii sunt adesea folosiți interschimbabil, dar au semnificații specifice:
- Coada de mesaje: Coadă punct la punct. Un mesaj este livrat către unul singur consumator. Exemplu: SQS Standard Queue
- Autobuz de evenimente: Difuzați către toată lumea abonatii. Fiecare abonat primește o copie a evenimentului. Exemplu: AWS EventBridge, subiect SNS
- Autobuz de mesaje: Termen general care include atât coada, cât și subiectul. În practică: un broker care gestionează rutarea mesajelor (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: Standardul pentru schemele de evenimente
CloudEvents și o specificație CNCF care standardizează structura evenimente între diferite sisteme. Adoptarea acestuia facilitează interoperabilitatea și simplifică instrumentele monitorizare și depanare:
// 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");
}
Versiunea evenimentelor
Evenimentele sunt consumate de mai multe servicii în mod independent. Schimbați modelul a unui eveniment fără o strategie de versiuni distruge consumatorii. Principalele modele:
// 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
Beneficiile și compromisurile EDA
Când să utilizați EDA
- Decuplarea necesară: Când doriți să adăugați noi consumatori fără a schimba editorii
- Scalabilitate independentă: Consumatori diferiți cu sarcini diferite care se scalează separat
- Piste de audit: Evenimentele imuabile sunt un jurnal natural al tot ceea ce s-a întâmplat în sistem
- Rezistenta la esec: Dacă un consumator este oprit, magistrala de mesaje reține mesajele până când revine
- Integrare între sisteme: Sisteme eterogene care comunică prin evenimente standard
Când să NU folosiți EDA
- Se cere răspuns imediat: Dacă utilizatorul trebuie să aștepte rezultatul sincron, EDA adaugă latență și complexitate inutile
- Sisteme simple: Un monolit cu puține caracteristici nu beneficiază de cheltuielile generale ale unui broker de mesaje
- Tranzacții simple distribuite: Pentru operațiunile care trebuie să fie atomice în mai multe servicii, EDA necesită modelul Saga (complexitate ridicată)
- Echipă mică fără experiență EDA: Curba de învățare este semnificativă. Începeți cu REST și adăugați EDA acolo unde este necesar
Fluxul complet: exemplu de comerț electronic
// 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
Concluzii și pașii următori
EDA este o schimbare de paradigmă: de la un sistem la care serviciile „se apelează” reciproc un sistem în care serviciile „comunică prin evenimente”. Câștigul de decuplare, scalabilitatea și reziliența sunt reale, dar necesită înfruntarea unor noi provocări: managementul de erori devine asincron, depanarea necesită ID de corelare și urmărire distribuită, consistența trebuie să devină „eventuală”.
Următoarele articole din această serie abordează tiparele avansate care fac EDA viabil în producție: Event Sourcing pentru stare imuabilă, CQRS pentru separare citire/scriere, Saga pentru tranzacții distribuite și instrumente AWS (EventBridge, SQS, SNS) pentru a le implementa în medii cloud.
Articole viitoare din seria Event-Driven Architecture
- Aprovizionarea evenimentelor: starea ca o secvență imuabilă de evenimente
- CQRS: Citire și scriere separate pentru scalare independentă
- Saga Pattern: Tranzacții distribuite cu coregrafie și orchestrație
- AWS EventBridge: Autobuz de evenimente fără server și rutare bazată pe conținut
- Coada de scrisori moarte și rezistență în sistemele asincrone
Serii înrudite
- Apache Kafka și Stream Processing — Kafka ca coloană vertebrală pentru sistemele EDA de mare volum
- Kubernetes la scară — orchestrați microservicii EDA pe Kubernetes
- Arhitectură software practică — când EDA vs REST, monolit vs microservicii







