Event Bus Overview
The UDIOT event bus provides a publish-subscribe mechanism for decoupled communication between modules. Publishers emit named events with typed payloads; handlers subscribe to those event names and are invoked when a matching event is published. Neither side holds a direct reference to the other.
Key Concepts
- Event definition — a typed descriptor (
EventDefinition<TPayload>) that gives an event a name, category, version, and optional schema for payload validation. - Publisher — any code that calls
bus.publish(eventName, payload)orbus.publishWithDefinition(definition, payload). Publishing is always asynchronous. - Handler — a function registered with
bus.registerHandler(definition). Handlers are called in priority order for every matching event. - Telemetry integration — every
publishcall opens anevents.publishspan (producer kind). Every handler invocation opens anevents.handlerspan (consumer kind). Metrics are emitted for published events and handler executions. - Diagnostics —
bus.getDiagnostics()returns the current in-flight count, queue depth, and last dispatch timestamp.
Architecture
Publisher
│
│ bus.publish('user.registered', payload)
▼
┌─────────────────────────────────────────────┐
│ AIEventBus │
│ (BaseEventBus → InMemoryEventBus) │
│ │
│ 1. Look up EventDefinition in registry │
│ 2. Validate payload against schema │
│ 3. Build AIEvent with headers │
│ 4. Open events.publish span │
│ 5. Dispatch to registered handlers │
└──────────────────────────────────────────────┘
│
├──▶ Handler1 (EmailService.onUserRegistered)
│ └─▶ events.handler span
│
├──▶ Handler2 (AnalyticsService.onUserRegistered)
│ └─▶ events.handler span
│
└──▶ Handler3 (AuditService.onUserRegistered)
└─▶ events.handler span
When to Use the Event Bus
Use the event bus when:
- An action in one module needs to trigger work in multiple other modules without knowing which ones.
- You want to add new behaviour to an existing flow without modifying the code that triggers it.
- You need audit trails or observability hooks for cross-cutting events.
- Handler failures should not block the primary flow (use
async: trueinPublishOptions).
Use direct service calls when:
- You need the return value from the called code.
- The interaction is strictly one-to-one and always will be.
- Ordering and synchronous completion guarantees are required.
Event Categories
type EventCategory = 'domain' | 'system' | 'ai';
| Category | Description |
|---|---|
'domain' |
Business events: user actions, order state changes, payment events |
'system' |
Infrastructure events: health checks, metric snapshots, cache invalidation |
'ai' |
Model inference results, suggestion generation, AI pipeline events |
Telemetry Metrics
| Metric | Type | Labels |
|---|---|---|
events.published |
counter | eventName, category |
events.handlers.started |
counter | eventName |
events.handlers.failed |
counter | eventName |
events.handlers.duration_ms |
histogram | eventName, status (completed or failed) |
Error Types
| Class | Thrown when |
|---|---|
EventValidationError |
Event name not registered, or payload fails schema validation |
EventHandlerExecutionError |
A handler throws and max retry attempts are exhausted |
EventProviderNotFoundError |
A named provider is requested but not registered |
Related Documentation
- Bus —
AIEventBus,BaseEventBus,InMemoryEventBus,PublishOptions,AIEvent - Event Definitions —
EventDefinition,createEventDefinition, registry - Decorators —
@AIEventDefinition,@AIEventHandler,@AIEventBusProvider,@AISuggestedEvent
Pitfalls
- Every event name must be registered via
registerEventDefinition(or@AIEventDefinition) before it can be published. Callingbus.publish('unknown.event', payload)throwsEventValidationError. - Handlers registered on
BaseEventBusare local to that instance. The global bus (obtained viagetGlobalEventBus()) has its own handler list. Use one consistently or bridge them explicitly. - In test environments, call
resetEventRegistry()between tests to clear definitions and handlers. Also callbus.stop()on anyInMemoryEventBusinstance to clear the internal worker interval and avoid open handles.
From the canonical UDIOT public documentation.Try it in the playground ↗