DEV Community

Young Gao
Young Gao

Posted on

Building a Type-Safe Event Bus in TypeScript: Decouple Your Microservices

Building a Type-Safe Event Bus in TypeScript: Decouple Your Microservices

Your payment service calls notification directly. One change breaks three services. An event bus decouples producers from consumers.

The Problem

Untyped events break at runtime. Typos compile fine but crash in production.

Event Map

interface EventMap {
  "user.created": { id: string; email: string; name: string };
  "user.deleted": { id: string };
  "order.placed": { orderId: string; userId: string; total: number };
  "payment.completed": { orderId: string; amount: number; currency: string };
}
Enter fullscreen mode Exit fullscreen mode

The Type-Safe Event Bus

type Handler<T> = (payload: T) => void | Promise<void>;
class EventBus<E extends Record<string, unknown>> {
  private handlers = new Map();
  on(event, handler) { if (\!this.handlers.has(event)) this.handlers.set(event, new Set()); this.handlers.get(event).add(handler); return () => this.handlers.get(event).delete(handler); }
  async emit(event, payload) { const h = this.handlers.get(event); if (\!h) return; await Promise.all([...h].map(fn => Promise.resolve(fn(payload)).catch(console.error))); }
}
Enter fullscreen mode Exit fullscreen mode

Cross-Service With Redis Pub/Sub

Wrap the local bus with Redis for cross-process type safety:

class DistributedEventBus<E extends Record<string, unknown>> {
  private local = new EventBus<E>();
  private pub: Redis; private sub: Redis;
  constructor(url: string) { this.pub = new Redis(url); this.sub = new Redis(url); this.sub.on("message", (ch, msg) => this.local.emit(ch, JSON.parse(msg))); }
  on(event, handler) { this.sub.subscribe(String(event)); return this.local.on(event, handler); }
  async emit(event, payload) { await this.pub.publish(String(event), JSON.stringify(payload)); }
}
Enter fullscreen mode Exit fullscreen mode

Dead Letter Queue

When handlers fail, push to a dead letter queue and retry later. Extend EventBus with a dlq array that catches errors and provides retryDLQ().

Testing

No mocks needed. Emit and assert. Test duplicate key returns same response, unsubscribe stops events, concurrent handling works.

When to Use This

Yes: Decoupling services, audit logs, notifications, analytics.
No: Request-response flows. Use direct calls or RPC.


Part of my Production Backend Patterns series. Follow for more practical backend engineering.


If this article helped you, consider buying me a coffee on Ko-fi! Follow me for more production backend patterns.

Top comments (0)