TypeScript

Event-Driven Architecture com TypeScript: Events e Subscribers Tipados na Prática

15 min de leitura

Event-Driven Architecture com TypeScript: Events e Subscribers Tipados na Prática

O Que é Event-Driven Architecture? 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. 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. Construindo um Sistema de Events e Subscribers Tipado A Importância da Tipagem

<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 &quot;ouvinte&quot; (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&lt;T = any&gt; {

type: string;

payload: T;

timestamp: number;

}

export type EventHandler&lt;T = any&gt; = (event: Event&lt;T&gt;) =&gt; void | Promise&lt;void&gt;;

export interface EventEmitter&lt;T extends EventMap = EventMap&gt; {

on&lt;K extends keyof T&gt;(type: K, handler: EventHandler&lt;T[K]&gt;): void;

off&lt;K extends keyof T&gt;(type: K, handler: EventHandler&lt;T[K]&gt;): void;

emit&lt;K extends keyof T&gt;(type: K, payload: T[K]): Promise&lt;void&gt;;

}</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 &#039;../types/events&#039;;

export class EventBus&lt;T extends EventMap = EventMap&gt; implements EventEmitter&lt;T&gt; {

private handlers: Map&lt;keyof T, Set&lt;EventHandler&lt;any&gt;&gt;&gt; = new Map();

on&lt;K extends keyof T&gt;(type: K, handler: EventHandler&lt;T[K]&gt;): void {

if (!this.handlers.has(type)) {

this.handlers.set(type, new Set());

}

this.handlers.get(type)!.add(handler);

}

off&lt;K extends keyof T&gt;(type: K, handler: EventHandler&lt;T[K]&gt;): void {

const handlers = this.handlers.get(type);

if (handlers) {

handlers.delete(handler);

}

}

async emit&lt;K extends keyof T&gt;(type: K, payload: T[K]): Promise&lt;void&gt; {

const event: Event&lt;T[K]&gt; = {

type: String(type),

payload,

timestamp: Date.now(),

};

const handlers = this.handlers.get(type);

if (!handlers) return;

const promises = Array.from(handlers).map(handler =&gt;

Promise.resolve(handler(event)).catch(error =&gt; {

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 &#039;../../types/events&#039;;

export interface UserRegisteredPayload {

userId: string;

email: string;

name: string;

registeredAt: Date;

}

export interface OrderCreatedPayload {

orderId: string;

userId: string;

items: Array&lt;{ productId: string; quantity: number; price: number }&gt;;

totalAmount: number;

}

export interface OrderShippedPayload {

orderId: string;

trackingNumber: string;

estimatedDelivery: Date;

}

export interface ApplicationEventMap extends EventMap {

&#039;user:registered&#039;: UserRegisteredPayload;

&#039;order:created&#039;: OrderCreatedPayload;

&#039;order:shipped&#039;: 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 &#039;../../types/events&#039;;

import { UserRegisteredPayload } from &#039;../events/ApplicationEvents&#039;;

export class SendWelcomeEmailSubscriber {

async handle(event: Event&lt;UserRegisteredPayload&gt;): Promise&lt;void&gt; {

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&lt;void&gt; {

// 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 &#039;../../types/events&#039;;

import { OrderCreatedPayload } from &#039;../events/ApplicationEvents&#039;;

export class UpdateInventorySubscriber {

async handle(event: Event&lt;OrderCreatedPayload&gt;): Promise&lt;void&gt; {

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&lt;void&gt; {

// 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 &#039;./EventBus&#039;;

import { ApplicationEventMap } from &#039;../domain/events/ApplicationEvents&#039;;

import { SendWelcomeEmailSubscriber } from &#039;../domain/subscribers/SendWelcomeEmailSubscriber&#039;;

import { UpdateInventorySubscriber } from &#039;../domain/subscribers/UpdateInventorySubscriber&#039;;

export function registerSubscribers(eventBus: EventBus&lt;ApplicationEventMap&gt;): void {

const sendWelcomeEmailSubscriber = new SendWelcomeEmailSubscriber();

const updateInventorySubscriber = new UpdateInventorySubscriber();

// Registra subscribers para eventos específicos

eventBus.on(&#039;user:registered&#039;, (event) =&gt; sendWelcomeEmailSubscriber.handle(event));

eventBus.on(&#039;order:created&#039;, (event) =&gt; 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 &#039;../../infrastructure/EventBus&#039;;

import { ApplicationEventMap, OrderCreatedPayload } from &#039;../../domain/events/ApplicationEvents&#039;;

export class OrderService {

constructor(private eventBus: EventBus&lt;ApplicationEventMap&gt;) {}

async createOrder(

userId: string,

items: Array&lt;{ productId: string; quantity: number; price: number }&gt;

): Promise&lt;string&gt; {

// Validações e lógica de negócio

const orderId = this.generateOrderId();

const totalAmount = items.reduce((sum, item) =&gt; sum + item.price * item.quantity, 0);

// Persiste a ordem no banco de dados

await this.saveOrderToDatabase({

id: orderId,

userId,

items,

totalAmount,

status: &#039;created&#039;,

});

// Emite o evento para que subscribers reajam

const payload: OrderCreatedPayload = {

orderId,

userId,

items,

totalAmount,

};

await this.eventBus.emit(&#039;order:created&#039;, payload);

return orderId;

}

private generateOrderId(): string {

return ORD-${Date.now()};

}

private async saveOrderToDatabase(order: any): Promise&lt;void&gt; {

console.log(&#039;Saving order to database:&#039;, order);

}

}</code></pre>

<p>Agora vamos usar esse serviço:</p>

<pre><code class="language-typescript">// main.ts

import { EventBus } from &#039;./infrastructure/EventBus&#039;;

import { OrderService } from &#039;./application/services/OrderService&#039;;

import { registerSubscribers } from &#039;./infrastructure/SubscriberRegistry&#039;;

import { ApplicationEventMap } from &#039;./domain/events/ApplicationEvents&#039;;

async function main() {

const eventBus = new EventBus&lt;ApplicationEventMap&gt;();

// Registra todos os subscribers

registerSubscribers(eventBus);

const orderService = new OrderService(eventBus);

// Simula criação de pedido

const orderId = await orderService.createOrder(&#039;user-123&#039;, [

{ productId: &#039;prod-1&#039;, quantity: 2, price: 29.99 },

{ productId: &#039;prod-2&#039;, 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&lt;T extends EventMap = EventMap&gt; implements EventEmitter&lt;T&gt; {

private handlers: Map&lt;keyof T, Set&lt;EventHandler&lt;any&gt;&gt;&gt; = new Map();

private errorHandler?: (error: Error, eventType: string) =&gt; void;

setErrorHandler(handler: (error: Error, eventType: string) =&gt; void): void {

this.errorHandler = handler;

}

async emit&lt;K extends keyof T&gt;(type: K, payload: T[K]): Promise&lt;void&gt; {

const event: Event&lt;T[K]&gt; = {

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 =&gt;

Promise.resolve(handler(event))

.catch(error =&gt; {

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&lt;T&gt; {

handler: EventHandler&lt;T&gt;;

priority: number; // Maior número = maior prioridade

}

export class EventBus&lt;T extends EventMap = EventMap&gt; implements EventEmitter&lt;T&gt; {

private handlers: Map&lt;keyof T, HandlerEntry&lt;any&gt;[]&gt; = new Map();

on&lt;K extends keyof T&gt;(

type: K,

handler: EventHandler&lt;T[K]&gt;,

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) =&gt; b.priority - a.priority);

}

async emit&lt;K extends keyof T&gt;(type: K, payload: T[K]): Promise&lt;void&gt; {

const event: Event&lt;T[K]&gt; = {

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 &#039;../types/events&#039;;

export interface EventMiddleware {

before?(event: Event): Promise&lt;void&gt;;

after?(event: Event, result: any): Promise&lt;void&gt;;

}

export class EventBusWithMiddleware&lt;T extends EventMap = EventMap&gt; extends EventBus&lt;T&gt; {

private middlewares: EventMiddleware[] = [];

use(middleware: EventMiddleware): void {

this.middlewares.push(middleware);

}

async emit&lt;K extends keyof T&gt;(type: K, payload: T[K]): Promise&lt;void&gt; {

const event: Event&lt;T[K]&gt; = {

type: String(type),

payload,

timestamp: Date.now(),

};

// Executa middlewares &quot;before&quot;

for (const middleware of this.middlewares) {

if (middleware.before) {

await middleware.before(event);

}

}

// Executa handlers normais

await super.emit(type, payload);

// Executa middlewares &quot;after&quot;

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) =&gt; {

console.log([EVENT] ${event.type} emitted at ${new Date(event.timestamp).toISOString()});

},

after: async (event) =&gt; {

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces
O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces

Entendendo Herança em TypeScript A herança é um dos pilares da Programação Or...

Como Usar Testes End-to-End com Playwright e TypeScript: Page Objects Tipados em Produção
Como Usar Testes End-to-End com Playwright e TypeScript: Page Objects Tipados em Produção

Entendendo Testes End-to-End e a Importância do Playwright Testes end-to-end...

Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção
Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção

O Problema das APIs Tradicionais Quando desenvolvemos aplicações modernas com...