When events make sense
Use events when the producer does not care about the outcome. User signs up? Emit user.registered. Whether that triggers a welcome email, an analytics event, or a Slack notification is not the signup handler's concern.
In-process events with typed emitters
import { EventEmitter } from 'events';
interface AppEvents {
'user.registered': [{ userId: string; email: string }];
'order.completed': [{ orderId: string; total: number }];
}
class TypedEmitter extends EventEmitter {
emit<K extends keyof AppEvents>(event: K, ...args: AppEvents[K]): boolean {
return super.emit(event, ...args);
}
on<K extends keyof AppEvents>(event: K, listener: (...args: AppEvents[K]) => void): this {
return super.on(event, listener as (...args: unknown[]) => void);
}
}
export const appEvents = new TypedEmitter();
Now appEvents.emit('user.registered', { userId: '123' }) will type-error if email is missing.
The lost event problem
In-process events vanish if the process crashes between emitting and handling. For critical events (payments, order confirmations), use a transactional outbox:
- Write the event to an
outboxtable in the same transaction as the business data - A separate worker polls the outbox and publishes events
- After successful publish, mark the outbox row as processed
BEGIN;
INSERT INTO orders (id, total) VALUES ('abc', 99.00);
INSERT INTO outbox (event_type, payload) VALUES ('order.completed', '{"orderId": "abc", "total": 99}');
COMMIT;
The event is guaranteed to exist if the order exists.
Idempotent handlers
Events can be delivered more than once (network retry, worker restart). Every handler must be idempotent:
appEvents.on('user.registered', async ({ userId, email }) => {
// Check if we already sent the welcome email
const sent = await redis.get(`welcome:${userId}`);
if (sent) return;
await sendWelcomeEmail(email);
await redis.set(`welcome:${userId}`, '1', 'EX', 86400);
});
Debugging event-driven systems
The biggest downside: "why did this happen" is hard to answer when there is no direct call stack. Two things help:
- Correlation IDs: Every event carries a correlation ID from the original request. Log it everywhere.
- Event log: Store every emitted event with timestamp, type, and payload. This is your audit trail.
When to use a message broker instead
If you need cross-service events, durability, or replay capability, graduate to RabbitMQ or Kafka. In-process events are for in-process decoupling only.
