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 };
}
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))); }
}
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)); }
}
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)