Building a Type-Safe Event Bus in TypeScript: Decouple Your Microservices
Stay on top of this story
Follow the names and topics behind it.
Add this story's key topics to your watchlist so LyscoNews can highlight related developments and future matches.
Create a free account to sync your watchlist, saved stories, and alerts across devices.
Quick Summary
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. Untyped events break at runtime. Typos compile fine but crash in production. 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 }; }
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))); } }
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)); } }
When handlers fail, push to a dead letter queue and retry later. Extend EventBus with a dlq array that catches errors and provides retryDLQ(). No mocks needed. Emit and assert. Test duplicate key returns same response, unsubscribe stops events, concurrent handling works. 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.