<h2>O Que é Event-Driven Architecture?</h2>
<p>A Event-Driven Architecture é um padrão arquitetural onde componentes de um sistema se comunicam através de eventos em vez de chamadas diretas. Um evento representa algo que aconteceu no sistema — uma ação concluída, uma mudança de estado ou uma ocorrência significativa. Esse padrão promove desacoplamento entre componentes, permitindo que diferentes partes da aplicação reajam a eventos sem conhecer diretamente quem os dispara.</p>
<p>Em uma aplicação tradicional com arquitetura em camadas, você chamaria métodos diretamente: um serviço chama outro serviço, que chama outro. Isso cria dependências rígidas e torna o sistema difícil de manter e escalar. Na Event-Driven Architecture, quando algo importante acontece, um evento é emitido. Qualquer componente interessado nesse evento se registra como um "ouvinte" (subscriber) e é notificado quando o evento ocorre. Isso permite que você adicione novos comportamentos sem modificar o código existente — princípio conhecido como Open/Closed Principle.</p>
<h2>Construindo um Sistema de Events e Subscribers Tipado</h2>
<h3>A Importância da Tipagem Forte</h3>
<p>TypeScript nos permite criar um sistema de eventos totalmente tipado, eliminando erros em tempo de execução. Quando você define tipos para eventos e seus dados associados, o compilador garante que apenas dados corretos sejam passados e que os subscribers tratem os dados adequadamente. Isso é especialmente crítico em sistemas grandes onde eventos trafegam por múltiplos componentes.</p>
<p>Vamos construir uma infraestrutura robusta. Primeiro, definiremos tipos genéricos que permitem criar qualquer tipo de evento mantendo a segurança de tipos:</p>
<pre><code class="language-typescript">// types/events.ts
export interface EventMap {
[key: string]: any;
}
export interface Event<T = any> {
type: string;
payload: T;
timestamp: number;
}
export type EventHandler<T = any> = (event: Event<T>) => void | Promise<void>;
export interface EventEmitter<T extends EventMap = EventMap> {
on<K extends keyof T>(type: K, handler: EventHandler<T[K]>): void;
off<K extends keyof T>(type: K, handler: EventHandler<T[K]>): void;
emit<K extends keyof T>(type: K, payload: T[K]): Promise<void>;
}</code></pre>
<p>Esses tipos servem como contrato base para qualquer sistema de eventos. O <code>EventMap</code> é um mapa que associa nomes de eventos a seus tipos de dados. Um subscriber sabe exatamente qual tipo de dado esperar, e o compilador avisa se você tentar usar um tipo incorreto.</p>
<h3>Implementação do Event Bus</h3>
<p>O Event Bus é o componente central que gerencia todos os eventos e subscribers. Ele mantém um registro de quem está interessado em cada tipo de evento e é responsável por distribuir os eventos aos subscribers corretos:</p>
<pre><code class="language-typescript">// infrastructure/EventBus.ts
import { Event, EventHandler, EventEmitter, EventMap } from '../types/events';
export class EventBus<T extends EventMap = EventMap> implements EventEmitter<T> {
private handlers: Map<keyof T, Set<EventHandler<any>>> = new Map();
on<K extends keyof T>(type: K, handler: EventHandler<T[K]>): void {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set());
}
this.handlers.get(type)!.add(handler);
}
off<K extends keyof T>(type: K, handler: EventHandler<T[K]>): void {
const handlers = this.handlers.get(type);
if (handlers) {
handlers.delete(handler);
}
}
async emit<K extends keyof T>(type: K, payload: T[K]): Promise<void> {
const event: Event<T[K]> = {
type: String(type),
payload,
timestamp: Date.now(),
};
const handlers = this.handlers.get(type);
if (!handlers) return;
const promises = Array.from(handlers).map(handler =>
Promise.resolve(handler(event)).catch(error => {
console.error(Error in handler for event ${String(type)}:, error);
})
);
await Promise.all(promises);
}
}</code></pre>
<p>Note como <code>emit</code> é assíncrono e aguarda todas as handlers. Isso garante que todos os subscribers processem o evento antes de continuar, permitindo coordenação entre múltiplos componentes.</p>
<h3>Definindo Eventos da Aplicação</h3>
<p>Cada aplicação tem seus próprios eventos. Vamos criar um exemplo de um sistema de e-commerce onde eventos significativos acontecem:</p>
<pre><code class="language-typescript">// domain/events/ApplicationEvents.ts
import { EventMap } from '../../types/events';
export interface UserRegisteredPayload {
userId: string;
email: string;
name: string;
registeredAt: Date;
}
export interface OrderCreatedPayload {
orderId: string;
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
totalAmount: number;
}
export interface OrderShippedPayload {
orderId: string;
trackingNumber: string;
estimatedDelivery: Date;
}
export interface ApplicationEventMap extends EventMap {
'user:registered': UserRegisteredPayload;
'order:created': OrderCreatedPayload;
'order:shipped': OrderShippedPayload;
}</code></pre>
<p>Essa estrutura define exatamente quais eventos sua aplicação suporta e que dados cada um carrega. Se você tentar emitir um evento que não existe ou com dados do tipo errado, TypeScript avisa imediatamente.</p>
<h2>Criando Subscribers Tipados</h2>
<h3>Padrão de Subscriber</h3>
<p>Um subscriber é simplesmente uma classe que se registra no Event Bus e reage a eventos específicos. O padrão que vamos usar permite que cada subscriber seja independente e fácil de testar:</p>
<pre><code class="language-typescript">// domain/subscribers/SendWelcomeEmailSubscriber.ts
import { Event, EventHandler } from '../../types/events';
import { UserRegisteredPayload } from '../events/ApplicationEvents';
export class SendWelcomeEmailSubscriber {
async handle(event: Event<UserRegisteredPayload>): Promise<void> {
const { email, name } = event.payload;
// Simula envio de email
console.log(Sending welcome email to ${email} for ${name});
// Em produção, você chamaria um serviço de email real
await this.sendEmail(email, Welcome, ${name}!);
}
private async sendEmail(to: string, subject: string): Promise<void> {
// Implementação real de envio de email
return Promise.resolve();
}
}</code></pre>
<p>Um subscriber que precisa fazer algo quando uma ordem é criada:</p>
<pre><code class="language-typescript">// domain/subscribers/UpdateInventorySubscriber.ts
import { Event } from '../../types/events';
import { OrderCreatedPayload } from '../events/ApplicationEvents';
export class UpdateInventorySubscriber {
async handle(event: Event<OrderCreatedPayload>): Promise<void> {
const { items } = event.payload;
console.log(Updating inventory for ${items.length} items);
for (const item of items) {
await this.decrementStock(item.productId, item.quantity);
}
}
private async decrementStock(productId: string, quantity: number): Promise<void> {
// Chama banco de dados para decrementar estoque
console.log(Decrementing ${quantity} units of product ${productId});
}
}</code></pre>
<h3>Registrando Subscribers no Event Bus</h3>
<p>A forma como você registra subscribers define como sua aplicação reage a eventos. Vamos criar um bootstrapper que centraliza esse registro:</p>
<pre><code class="language-typescript">// infrastructure/SubscriberRegistry.ts
import { EventBus } from './EventBus';
import { ApplicationEventMap } from '../domain/events/ApplicationEvents';
import { SendWelcomeEmailSubscriber } from '../domain/subscribers/SendWelcomeEmailSubscriber';
import { UpdateInventorySubscriber } from '../domain/subscribers/UpdateInventorySubscriber';
export function registerSubscribers(eventBus: EventBus<ApplicationEventMap>): void {
const sendWelcomeEmailSubscriber = new SendWelcomeEmailSubscriber();
const updateInventorySubscriber = new UpdateInventorySubscriber();
// Registra subscribers para eventos específicos
eventBus.on('user:registered', (event) => sendWelcomeEmailSubscriber.handle(event));
eventBus.on('order:created', (event) => updateInventorySubscriber.handle(event));
}</code></pre>
<p>Quando um evento é emitido, todas as handlers registradas são chamadas. A tipagem garante que você não possa registrar uma handler para um evento inexistente ou tentar registrar uma handler que espera um tipo de dados diferente.</p>
<h2>Integrando Events com a Aplicação Real</h2>
<h3>Exemplo Prático: Fluxo Completo de Pedido</h3>
<p>Vamos ver como tudo funciona junto em um caso de uso real — criar um pedido:</p>
<pre><code class="language-typescript">// application/services/OrderService.ts
import { EventBus } from '../../infrastructure/EventBus';
import { ApplicationEventMap, OrderCreatedPayload } from '../../domain/events/ApplicationEvents';
export class OrderService {
constructor(private eventBus: EventBus<ApplicationEventMap>) {}
async createOrder(
userId: string,
items: Array<{ productId: string; quantity: number; price: number }>
): Promise<string> {
// Validações e lógica de negócio
const orderId = this.generateOrderId();
const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// Persiste a ordem no banco de dados
await this.saveOrderToDatabase({
id: orderId,
userId,
items,
totalAmount,
status: 'created',
});
// Emite o evento para que subscribers reajam
const payload: OrderCreatedPayload = {
orderId,
userId,
items,
totalAmount,
};
await this.eventBus.emit('order:created', payload);
return orderId;
}
private generateOrderId(): string {
return ORD-${Date.now()};
}
private async saveOrderToDatabase(order: any): Promise<void> {
console.log('Saving order to database:', order);
}
}</code></pre>
<p>Agora vamos usar esse serviço:</p>
<pre><code class="language-typescript">// main.ts
import { EventBus } from './infrastructure/EventBus';
import { OrderService } from './application/services/OrderService';
import { registerSubscribers } from './infrastructure/SubscriberRegistry';
import { ApplicationEventMap } from './domain/events/ApplicationEvents';
async function main() {
const eventBus = new EventBus<ApplicationEventMap>();
// Registra todos os subscribers
registerSubscribers(eventBus);
const orderService = new OrderService(eventBus);
// Simula criação de pedido
const orderId = await orderService.createOrder('user-123', [
{ productId: 'prod-1', quantity: 2, price: 29.99 },
{ productId: 'prod-2', quantity: 1, price: 49.99 },
]);
console.log(Order created: ${orderId});
}
main().catch(console.error);</code></pre>
<p>Quando <code>createOrder</code> é executado, ele emite o evento <code>order:created</code>. Automaticamente, sem que o <code>OrderService</code> saiba, o <code>UpdateInventorySubscriber</code> é acionado e decrementa o estoque. Você pode adicionar mais subscribers (como enviar notificação ao cliente, gerar nota fiscal, etc.) sem modificar o <code>OrderService</code>.</p>
<h3>Vantagens Desse Padrão</h3>
<p>Este padrão oferece several benefícios tangíveis. Primeiro, <strong>desacoplamento real</strong>: o <code>OrderService</code> não importa ou depende de nenhum subscriber. Segundo, <strong>facilidade de teste</strong>: você pode testar o <code>OrderService</code> sem precisar de nenhum subscriber registrado, ou testar cada subscriber independentemente. Terceiro, <strong>extensibilidade</strong>: adicionar novos comportamentos é apenas questão de criar um novo subscriber e registrá-lo — nenhuma mudança no código existente.</p>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Tratamento de Erros em Eventos Assíncronos</h3>
<p>Em sistemas reais, handlers podem falhar. É importante não deixar uma falha em um subscriber impedir que outros subscribers executem:</p>
<pre><code class="language-typescript">// infrastructure/EventBus.ts (versão melhorada)
export class EventBus<T extends EventMap = EventMap> implements EventEmitter<T> {
private handlers: Map<keyof T, Set<EventHandler<any>>> = new Map();
private errorHandler?: (error: Error, eventType: string) => void;
setErrorHandler(handler: (error: Error, eventType: string) => void): void {
this.errorHandler = handler;
}
async emit<K extends keyof T>(type: K, payload: T[K]): Promise<void> {
const event: Event<T[K]> = {
type: String(type),
payload,
timestamp: Date.now(),
};
const handlers = this.handlers.get(type);
if (!handlers) return;
// Executa todas as handlers em paralelo, mas captura erros individualmente
const promises = Array.from(handlers).map(handler =>
Promise.resolve(handler(event))
.catch(error => {
if (this.errorHandler) {
this.errorHandler(error, String(type));
} else {
console.error(Error in handler for event ${String(type)}:, error);
}
})
);
await Promise.all(promises);
}
// ... resto do código
}</code></pre>
<h3>Eventos Prioritários</h3>
<p>Às vezes, certos handlers devem executar antes de outras. Vamos adicionar suporte a prioridade:</p>
<pre><code class="language-typescript">// infrastructure/EventBus.ts (com suporte a prioridade)
interface HandlerEntry<T> {
handler: EventHandler<T>;
priority: number; // Maior número = maior prioridade
}
export class EventBus<T extends EventMap = EventMap> implements EventEmitter<T> {
private handlers: Map<keyof T, HandlerEntry<any>[]> = new Map();
on<K extends keyof T>(
type: K,
handler: EventHandler<T[K]>,
priority: number = 0
): void {
if (!this.handlers.has(type)) {
this.handlers.set(type, []);
}
const handlersList = this.handlers.get(type)!;
handlersList.push({ handler, priority });
// Ordena por prioridade (decrescente)
handlersList.sort((a, b) => b.priority - a.priority);
}
async emit<K extends keyof T>(type: K, payload: T[K]): Promise<void> {
const event: Event<T[K]> = {
type: String(type),
payload,
timestamp: Date.now(),
};
const handlersList = this.handlers.get(type);
if (!handlersList) return;
// Executa handlers em ordem de prioridade
for (const { handler } of handlersList) {
try {
await Promise.resolve(handler(event));
} catch (error) {
console.error(Error in handler for event ${String(type)}:, error);
}
}
}
}</code></pre>
<h3>Middleware para Events</h3>
<p>Você pode adicionar camadas de processamento antes e depois dos handlers — útil para logging, autenticação, ou transformação de dados:</p>
<pre><code class="language-typescript">// infrastructure/EventMiddleware.ts
import { Event } from '../types/events';
export interface EventMiddleware {
before?(event: Event): Promise<void>;
after?(event: Event, result: any): Promise<void>;
}
export class EventBusWithMiddleware<T extends EventMap = EventMap> extends EventBus<T> {
private middlewares: EventMiddleware[] = [];
use(middleware: EventMiddleware): void {
this.middlewares.push(middleware);
}
async emit<K extends keyof T>(type: K, payload: T[K]): Promise<void> {
const event: Event<T[K]> = {
type: String(type),
payload,
timestamp: Date.now(),
};
// Executa middlewares "before"
for (const middleware of this.middlewares) {
if (middleware.before) {
await middleware.before(event);
}
}
// Executa handlers normais
await super.emit(type, payload);
// Executa middlewares "after"
for (const middleware of this.middlewares) {
if (middleware.after) {
await middleware.after(event, undefined);
}
}
}
}
// Exemplo de middleware de logging
const loggingMiddleware: EventMiddleware = {
before: async (event) => {
console.log([EVENT] ${event.type} emitted at ${new Date(event.timestamp).toISOString()});
},
after: async (event) => {
console.log([EVENT] ${event.type} processed);
},
};</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu três conceitos fundamentais que transformarão a forma como você arquiteta aplicações TypeScript. Primeiro, <strong>Event-Driven Architecture desacopla componentes</strong>, permitindo que diferentes partes do sistema reajam a eventos sem conhecer diretamente quem os emite — isso torna o código mais flexível, testável e manutenível. Segundo, <strong>tipagem forte em TypeScript garante segurança em tempo de compilação</strong>, impedindo erros onde você tenta passar dados incorretos para um evento ou handler. Terceiro, <strong>padrões como prioridade e middleware adicionam poder ao sistema sem complexidade desnecessária</strong> — comece simples e evolua conforme suas necessidades crescem.</p>
<p>A chave para sucesso com Event-Driven Architecture é começar com uma base sólida de tipos e infraestrutura, depois deixar que a natureza desacoplada do padrão trabalhe a seu favor conforme o sistema cresce.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript Official Documentation</a></li>
<li><a href="https://martinfowler.com/articles/201701-event-driven.html" target="_blank" rel="noopener noreferrer">Event-Driven Architecture - Martin Fowler</a></li>
<li><a href="https://www.domainlanguage.com/ddd/" target="_blank" rel="noopener noreferrer">Domain-Driven Design - Eric Evans</a></li>
<li><a href="https://nodejs.org/api/events.html" target="_blank" rel="noopener noreferrer">Node.js EventEmitter Documentation</a></li>
<li><a href="https://martinfowler.com/books/eaa.html" target="_blank" rel="noopener noreferrer">Patterns of Enterprise Application Architecture - Martin Fowler</a></li>
</ul>
<p><!-- FIM --></p>