ADR-002: Event-Driven Architecture with TypedEventBus
Statusโ
Accepted - Core communication mechanism with advanced middleware and memory management
Contextโ
The application needed a way to decouple components and enable reactive communication between:
- Backend services and frontend UI
- Different services within the backend
- Multiple UI components reacting to state changes
- Cross-cutting concerns like logging, monitoring, and error handling
Traditional direct method calls would create tight coupling, make the system difficult to extend and test, and provide no mechanism for monitoring or debugging inter-component communication.
Decisionโ
We will use an Event-Driven Architecture based on a custom TypedEventBus
with the following characteristics:
1. Enhanced Type Safetyโ
interface UptimeEvents extends Record<string, unknown> {
"sites:added": {
site: Site;
timestamp: number;
correlationId: string;
};
"monitor:status-changed": {
monitor: Monitor;
newStatus: "up" | "down";
previousStatus: "up" | "down";
timestamp: number;
responseTime?: number;
error?: string;
};
"database:transaction-completed": {
duration: number;
operation: string;
success: boolean;
timestamp: number;
};
"system:error": {
error: Error;
context: string;
severity: "low" | "medium" | "high" | "critical";
recovery?: string;
timestamp: number;
};
}
// Usage with compile-time type checking
await eventBus.emitTyped("sites:added", {
site: newSite,
timestamp: Date.now(),
correlationId: generateCorrelationId(),
});
2. Advanced Metadata and Correlationโ
- Unique correlation IDs for request tracing across system boundaries
- Automatic timestamps for event ordering and debugging
- Bus identification for multi-bus architectures
- Event metadata enrichment for comprehensive monitoring
3. Consistent Event Namingโ
- Domain-based naming:
domain:action
(e.g.,sites:added
,monitor:status-changed
) - Hierarchical structure: Major category followed by specific action
- Past tense verbs for completed actions
4. Production-Ready Middleware Supportโ
// Logging middleware with correlation tracking
eventBus.use(async (eventName, data, next) => {
const correlationId = data._meta?.correlationId;
logger.debug(`[Event] ${eventName} [${correlationId}]`, data);
await next();
logger.debug(`[Event] ${eventName} completed [${correlationId}]`);
});
// Rate limiting middleware
eventBus.use(
createRateLimitMiddleware({
maxEventsPerSecond: 100,
burstLimit: 10,
onRateLimit: (eventName, data) => {
logger.warn(`Rate limit exceeded for ${eventName}`);
},
})
);
// Validation middleware
eventBus.use(
createValidationMiddleware({
"monitor:status-changed": (data) => validateMonitorStatusData(data),
"sites:added": (data) => validateSiteData(data),
})
);
5. Memory-Safe IPC Event Forwardingโ
Events are automatically forwarded from backend to frontend with proper cleanup:
// Backend emits event with automatic IPC forwarding
await this.eventBus.emitTyped("monitor:status-changed", eventData);
// Frontend receives with automatic cleanup functions
const cleanup = window.electronAPI.events.onMonitorStatusChanged((data) => {
// Handle event
});
// Cleanup prevents memory leaks
useEffect(() => cleanup, []);
6. Advanced Memory Managementโ
- Max listeners: Configurable limit (default: 50) prevents memory leaks
- Automatic cleanup: All event listeners provide cleanup functions
- Middleware limits: Configurable middleware chain size (default: 20)
- Event validation: Type-safe event structures prevent runtime errors
Architecture Flowโ
Event Categoriesโ
1. Site Eventsโ
Public Events:
site:added
- When a site is successfully addedsite:updated
- When site properties are modifiedsite:removed
- When a site is deletedsites:state-synchronized
- When frontend and backend state are synchronizedsite:cache-updated
- When site cache is refreshedsite:cache-miss
- When cache lookup fails
Internal Events:
internal:site:added
- Internal site creation eventsinternal:site:updated
- Internal site modification eventsinternal:site:removed
- Internal site deletion eventsinternal:site:cache-updated
- Internal cache managementinternal:site:start-monitoring-requested
- Internal monitoring controlinternal:site:stop-monitoring-requested
- Internal monitoring controlinternal:site:restart-monitoring-requested
- Internal monitoring controlinternal:site:restart-monitoring-response
- Internal monitoring responsesinternal:site:is-monitoring-active-requested
- Internal status queriesinternal:site:is-monitoring-active-response
- Internal status responses
2. Monitor Eventsโ
Public Events:
monitor:added
- When a monitor is createdmonitor:removed
- When a monitor is deletedmonitor:status-changed
- When monitor status changesmonitor:up
- When monitor detects service is onlinemonitor:down
- When monitor detects service is offlinemonitor:check-completed
- When a health check finishes
Internal Events:
internal:monitor:started
- Internal monitor activationinternal:monitor:stopped
- Internal monitor deactivationinternal:monitor:all-started
- When all monitors are activatedinternal:monitor:all-stopped
- When all monitors are deactivatedinternal:monitor:manual-check-completed
- Manual check resultsinternal:monitor:site-setup-completed
- Site monitor setup completion
3. Database Eventsโ
database:transaction-completed
- When database transactions finishdatabase:error
- When database operations faildatabase:success
- When database operations succeeddatabase:retry
- When database operations are retrieddatabase:backup-created
- When database backups are created
Internal Database Events:
internal:database:initialized
- Database initialization completioninternal:database:data-exported
- Data export completioninternal:database:data-imported
- Data import completioninternal:database:backup-downloaded
- Backup download completioninternal:database:history-limit-updated
- History retention changesinternal:database:sites-refreshed
- Site data refreshinternal:database:get-sites-from-cache-requested
- Cache requestsinternal:database:get-sites-from-cache-response
- Cache responsesinternal:database:update-sites-cache-requested
- Cache update requests
4. System Eventsโ
monitoring:started
- When monitoring system startsmonitoring:stopped
- When monitoring system stopssystem:startup
- Application startupsystem:shutdown
- Application shutdownsystem:error
- System-level errors
5. Performance and Configuration Eventsโ
performance:metric
- Performance measurementsperformance:warning
- Performance threshold alertsconfig:changed
- Configuration changescache:invalidated
- Cache invalidation events
Consequencesโ
Positiveโ
- Decoupled architecture - Components don't need direct references
- Enhanced type safety - Compile-time checking prevents runtime errors
- Extensibility - Easy to add new event listeners without modifying emitters
- Advanced debugging - Correlation IDs and metadata enable comprehensive request tracing
- Superior testability - Easy to mock and verify event emissions
- Memory safety - Automatic cleanup and configurable limits prevent leaks
- Production monitoring - Middleware enables comprehensive observability
- Cross-cutting concerns - Logging, validation, and rate limiting handled declaratively
Negativeโ
- Initial complexity - Indirect flow can be harder to follow initially
- Minimal performance overhead - Event processing adds negligible latency
- Learning curve - Developers need to understand event-driven patterns
- Debugging complexity - Async event flows require correlation tracking
Quality Assuranceโ
Memory Managementโ
- Automatic cleanup: All event listeners return cleanup functions
- Configurable limits: Max listeners and middleware prevent resource exhaustion
- Leak prevention: Proper cleanup in component unmount lifecycle
Error Handlingโ
- Middleware isolation: Errors in one middleware don't affect others
- Event validation: Type-safe structures prevent runtime errors
- Error propagation: Failed events don't crash the event bus
Performanceโ
- Rate limiting: Middleware prevents event flooding
- Efficient forwarding: IPC events use optimized serialization
- Minimal overhead: Event processing designed for production use
Implementation Requirementsโ
Event Emissionโ
// In services/managers
await this.eventBus.emitTyped("domain:action", {
// Event-specific data
timestamp: Date.now(),
// ... other properties
});
Event Listeningโ
// Type-safe event listening
eventBus.onTyped("domain:action", (data) => {
// data is properly typed
// _meta is automatically available
});
IPC Integrationโ
// Automatic forwarding in IpcService
private async forwardEventToRenderer(eventName: string, data: unknown) {
this.webContents?.send(eventName, data);
}
Complianceโ
All communication follows this pattern:
- Service layer emits domain events
- UI components listen to events via IPC
- Database operations emit lifecycle events
- Error handling emits failure events