# ADR-036: Event Bus and Pub/Sub Pattern **Date**: 2026-01-09 **Status**: Accepted **Implemented**: 2026-01-09 ## Context Modern web applications often need to handle cross-component communication without creating tight coupling between modules. In our application, several scenarios require broadcasting events across the system: 1. **Session Expiry**: When a user's session expires, multiple components need to respond (auth state, UI notifications, API client). 2. **Real-time Updates**: When data changes on the server, multiple UI components may need to update. 3. **Cross-Component Communication**: Independent components need to communicate without direct references to each other. Traditional approaches like prop drilling or global state management can lead to tightly coupled code that is difficult to maintain and test. ## Decision We will implement a lightweight, in-memory event bus pattern using a publish/subscribe (pub/sub) architecture. This provides: 1. **Decoupled Communication**: Publishers and subscribers don't need to know about each other. 2. **Event-Driven Architecture**: Components react to events rather than polling for changes. 3. **Testability**: Events can be easily mocked and verified in tests. ### Design Principles - **Singleton Pattern**: A single event bus instance is shared across the application. - **Type-Safe Events**: Event names are string constants to prevent typos. - **Memory Management**: Subscribers must unsubscribe when components unmount to prevent memory leaks. ## Implementation Details ### EventBus Class Located in `src/services/eventBus.ts`: ```typescript type EventCallback = (data?: any) => void; export class EventBus { private listeners: { [key: string]: EventCallback[] } = {}; on(event: string, callback: EventCallback): void { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(callback); } off(event: string, callback: EventCallback): void { if (!this.listeners[event]) return; this.listeners[event] = this.listeners[event].filter((l) => l !== callback); } dispatch(event: string, data?: any): void { if (!this.listeners[event]) return; this.listeners[event].forEach((callback) => callback(data)); } } // Singleton instance export const eventBus = new EventBus(); ``` ### Event Constants Define event names as constants to prevent typos: ```typescript // src/constants/events.ts export const EVENTS = { SESSION_EXPIRED: 'session:expired', SESSION_REFRESHED: 'session:refreshed', USER_LOGGED_OUT: 'user:loggedOut', DATA_UPDATED: 'data:updated', NOTIFICATION_RECEIVED: 'notification:received', } as const; ``` ### React Hook for Event Subscription ```typescript // src/hooks/useEventBus.ts import { useEffect } from 'react'; import { eventBus } from '../services/eventBus'; export function useEventBus(event: string, callback: (data?: any) => void) { useEffect(() => { eventBus.on(event, callback); // Cleanup on unmount return () => { eventBus.off(event, callback); }; }, [event, callback]); } ``` ### Usage Examples **Publishing Events**: ```typescript import { eventBus } from '../services/eventBus'; import { EVENTS } from '../constants/events'; // In API client when session expires function handleSessionExpiry() { eventBus.dispatch(EVENTS.SESSION_EXPIRED, { reason: 'token_expired' }); } ``` **Subscribing in Components**: ```typescript import { useCallback } from 'react'; import { useEventBus } from '../hooks/useEventBus'; import { EVENTS } from '../constants/events'; function AuthenticatedComponent() { const handleSessionExpired = useCallback((data) => { console.log('Session expired:', data.reason); // Redirect to login, show notification, etc. }, []); useEventBus(EVENTS.SESSION_EXPIRED, handleSessionExpired); return