PatternsArchitecture Patterns
Event-Driven Architecture
Build decoupled systems that communicate through events
Event-Driven Architecture
Event-Driven Architecture (EDA) is a pattern where components communicate by emitting and listening to events, rather than directly calling each other. Used by Slack, Stripe, and Shopify.
🎯 The Concept
Traditional (Tight Coupling):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Service │─────>│ Service │─────>│ Service │
│ A │ │ B │ │ C │
└──────────┘ └──────────┘ └──────────┘
Each service knows about the next
Event-Driven (Loose Coupling):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Service │ │ Event │ │ Service │
│ A │─────>│ Bus │<─────│ B │
└──────────┘ └──────────┘ └──────────┘
↑
│
┌──────────┐
│ Service │
│ C │
└──────────┘
Services don't know about each other📊 Benefits
| Benefit | Description |
|---|---|
| Loose Coupling | Components don't depend on each other |
| Scalability | Add listeners without changing emitters |
| Flexibility | Easy to add/remove features |
| Async | Events processed asynchronously |
| Audit Trail | All events are logged |
🔧 Basic Event Bus
Simple Implementation
type EventCallback<T = unknown> = (data: T) => void;
class EventBus {
private events = new Map<string, EventCallback[]>();
// Subscribe to event
on<T = unknown>(event: string, callback: EventCallback<T>): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
// Return unsubscribe function
return () => {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
// Subscribe once (auto-unsubscribe after first call)
once<T = unknown>(event: string, callback: EventCallback<T>): void {
const unsubscribe = this.on(event, (data) => {
callback(data);
unsubscribe();
});
}
// Emit event
emit<T = unknown>(event: string, data?: T): void {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
// Remove all listeners for event
off(event: string): void {
this.events.delete(event);
}
// Remove all listeners
clear(): void {
this.events.clear();
}
}
// Usage
const bus = new EventBus();
// User service emits events
function createUser(userData: User) {
const user = saveToDatabase(userData);
bus.emit('user:created', user);
return user;
}
// Email service listens
bus.on('user:created', (user: User) => {
sendWelcomeEmail(user.email);
});
// Analytics service listens
bus.on('user:created', (user: User) => {
trackSignup(user.id);
});
// Notification service listens
bus.on('user:created', (user: User) => {
showNotification(`Welcome ${user.name}!`);
});🎨 Typed Event Bus
// Define event types
interface Events {
'user:created': User;
'user:updated': { id: string; changes: Partial<User> };
'user:deleted': { id: string };
'order:placed': Order;
'payment:completed': { orderId: string; amount: number };
}
class TypedEventBus {
private events = new Map<keyof Events, Function[]>();
on<K extends keyof Events>(
event: K,
callback: (data: Events[K]) => void
): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
return () => {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
const callbacks = this.events.get(event) as ((data: Events[K]) => void)[];
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
}
// Usage with type safety!
const bus = new TypedEventBus();
bus.on('user:created', (user) => {
// user is typed as User ✅
console.log(user.name);
});
bus.emit('user:created', {
id: '1',
name: 'John',
email: 'john@example.com'
});🔄 Event Sourcing Pattern
// Store all events, not just current state
interface Event {
id: string;
type: string;
timestamp: number;
data: unknown;
}
class EventStore {
private events: Event[] = [];
append(type: string, data: unknown): Event {
const event: Event = {
id: `${Date.now()}-${Math.random()}`,
type,
timestamp: Date.now(),
data
};
this.events.push(event);
return event;
}
getEvents(filter?: { type?: string; since?: number }): Event[] {
let events = this.events;
if (filter?.type) {
events = events.filter(e => e.type === filter.type);
}
if (filter?.since) {
events = events.filter(e => e.timestamp >= filter.since);
}
return events;
}
replay(handler: (event: Event) => void): void {
this.events.forEach(handler);
}
}
// Usage - Shopping Cart with Event Sourcing
class ShoppingCart {
private store = new EventStore();
private items: CartItem[] = [];
constructor() {
// Replay all events to rebuild state
this.store.replay((event) => {
this.applyEvent(event);
});
}
addItem(item: CartItem): void {
this.store.append('cart:item-added', item);
this.applyEvent({ type: 'cart:item-added', data: item } as Event);
}
removeItem(itemId: string): void {
this.store.append('cart:item-removed', { itemId });
this.applyEvent({ type: 'cart:item-removed', data: { itemId } } as Event);
}
private applyEvent(event: Event): void {
switch (event.type) {
case 'cart:item-added':
this.items.push(event.data);
break;
case 'cart:item-removed':
this.items = this.items.filter(i => i.id !== event.data.itemId);
break;
}
}
getItems(): CartItem[] {
return this.items;
}
getHistory(): Event[] {
return this.store.getEvents();
}
}
// Benefits:
// - Full audit trail
// - Can rebuild state from events
// - Time travel debugging
// - Can replay events to different states🎯 Domain Events Pattern
// Define domain events
abstract class DomainEvent {
readonly occurredAt: Date = new Date();
abstract readonly eventName: string;
}
class UserCreatedEvent extends DomainEvent {
readonly eventName = 'user:created';
constructor(
public readonly userId: string,
public readonly email: string,
public readonly name: string
) {
super();
}
}
class OrderPlacedEvent extends DomainEvent {
readonly eventName = 'order:placed';
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly amount: number
) {
super();
}
}
// Event handler
interface EventHandler<T extends DomainEvent> {
handle(event: T): Promise<void>;
}
class WelcomeEmailHandler implements EventHandler<UserCreatedEvent> {
async handle(event: UserCreatedEvent): Promise<void> {
console.log(`Sending welcome email to ${event.email}`);
await sendEmail({
to: event.email,
subject: 'Welcome!',
body: `Hello ${event.name}!`
});
}
}
class OrderConfirmationHandler implements EventHandler<OrderPlacedEvent> {
async handle(event: OrderPlacedEvent): Promise<void> {
console.log(`Order ${event.orderId} placed for $${event.amount}`);
await sendOrderConfirmation(event.orderId);
}
}
// Event dispatcher
class EventDispatcher {
private handlers = new Map<string, EventHandler<DomainEvent>[]>();
register<T extends DomainEvent>(
eventName: string,
handler: EventHandler<T>
): void {
if (!this.handlers.has(eventName)) {
this.handlers.set(eventName, []);
}
this.handlers.get(eventName)!.push(handler);
}
async dispatch(event: DomainEvent): Promise<void> {
const handlers = this.handlers.get(event.eventName) || [];
await Promise.all(
handlers.map(handler => handler.handle(event))
);
}
}
// Setup
const dispatcher = new EventDispatcher();
dispatcher.register('user:created', new WelcomeEmailHandler());
dispatcher.register('order:placed', new OrderConfirmationHandler());
// Emit events
await dispatcher.dispatch(new UserCreatedEvent(
'123',
'john@example.com',
'John Doe'
));🔌 Integration with React
React Event Bus Hook
const EventBusContext = createContext<EventBus | null>(null);
function EventBusProvider({ children }: { children: React.ReactNode }) {
const bus = useMemo(() => new EventBus(), []);
return (
<EventBusContext.Provider value={bus}>
{children}
</EventBusContext.Provider>
);
}
function useEventBus() {
const bus = useContext(EventBusContext);
if (!bus) {
throw new Error('useEventBus must be used within EventBusProvider');
}
return bus;
}
// Hook to subscribe to events
function useEvent<T>(event: string, callback: EventCallback<T>) {
const bus = useEventBus();
useEffect(() => {
const unsubscribe = bus.on(event, callback);
return unsubscribe;
}, [bus, event, callback]);
}
// Hook to emit events
function useEmit() {
const bus = useEventBus();
return useCallback(<T>(event: string, data?: T) => {
bus.emit(event, data);
}, [bus]);
}
// Usage
function UserForm() {
const emit = useEmit();
const handleSubmit = (userData: User) => {
emit('user:created', userData);
};
return <form onSubmit={handleSubmit}>...</form>;
}
function Notifications() {
useEvent<User>('user:created', (user) => {
toast.success(`Welcome ${user.name}!`);
});
return null;
}
function Analytics() {
useEvent<User>('user:created', (user) => {
analytics.track('signup', { userId: user.id });
});
return null;
}⚡ Async Event Processing
class AsyncEventBus {
private events = new Map<string, EventCallback[]>();
private queue: Array<{ event: string; data: unknown }> = [];
private processing = false;
on(event: string, callback: EventCallback): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
return () => {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
emit(event: string, data?: unknown): void {
this.queue.push({ event, data });
this.processQueue();
}
private async processQueue(): Promise<void> {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
const { event, data } = this.queue.shift()!;
const callbacks = this.events.get(event) || [];
// Process callbacks sequentially
for (const callback of callbacks) {
try {
await callback(data);
} catch (error) {
console.error(`Error in event handler for "${event}":`, error);
}
}
}
this.processing = false;
}
}🏢 Real-World Examples
Stripe Webhooks (Event-Driven)
// Stripe emits events for everything
stripe.on('payment_intent.succeeded', (event) => {
const paymentIntent = event.data.object;
// Send receipt email
// Update order status
// Trigger fulfillment
});
stripe.on('payment_intent.payment_failed', (event) => {
const paymentIntent = event.data.object;
// Notify customer
// Log failure
});Shopify Event System
// Shopify triggers events for store actions
shopify.on('order_created', (order) => {
// Send confirmation email
// Update inventory
// Notify warehouse
});
shopify.on('product_updated', (product) => {
// Invalidate cache
// Sync with search index
// Update recommendations
});📚 Key Takeaways
- Loose coupling - Services don't know about each other
- Easy to extend - Just add new listeners
- Audit trail - All events are logged
- Type safety - Use TypeScript for event types
- Async processing - Don't block on event handling
- Error handling - Catch errors in handlers
- Testing - Easy to test with mock event bus
When to use: Building modular systems, microservices, or when you need loose coupling between components.