Event Bus Adapters
Cross-adapter concepts — the EventBus interface, handler error isolation, lifecycle, and resilience. See the per-adapter pages for setup details.
noddde ships with an in-memory EventEmitterEventBus for single-process development and three message-broker adapters for distributed production deployments. All implementations conform to the EventBus interface from @noddde/core, which extends Closeable for automatic lifecycle management. Broker adapters also implement Connectable — the Domain auto-calls connect() during wiring, so you never need to manage the connection lifecycle manually. For setup, configuration, and how each adapter works, see the per-adapter pages.
Available Adapters
| Package | Broker | Client Library | Delivery Guarantee |
|---|---|---|---|
@noddde/engine | — | Node.js EventEmitter | In-process only |
@noddde/kafka | Kafka | kafkajs | At-least-once |
@noddde/nats | NATS | nats | At-least-once (JetStream) |
@noddde/rabbitmq | RabbitMQ | amqplib | At-least-once |
Per-Adapter Setup
The EventBus Interface
Every adapter implements this interface:
import type { Closeable } from "@noddde/core";
type AsyncEventHandler = (event: Event) => void | Promise<void>;
interface EventBus extends Closeable {
dispatch<TEvent extends Event>(event: TEvent): Promise<void>;
on(eventName: string, handler: AsyncEventHandler): void;
}dispatch(event)— publishes a domain event to all subscribers.on(eventName, handler)— registers a handler for a given event name. Multiple handlers per event are supported (fan-out).close()— inherited fromCloseable. Unsubscribes all handlers and releases connections. Called automatically by the Domain on shutdown.
Handler Error Isolation
All four shipped adapters (EventEmitterEventBus, KafkaEventBus, NatsEventBus, RabbitMqEventBus) implement the same per-handler isolation contract:
- Every registered handler runs to completion for each event delivery, even when some throw. A buggy projection or saga can never short-circuit a sibling handler.
- Each handler failure is logged exactly once via the framework
Loggeraterrorlevel with structured fields:eventName,eventId?,handlerName,error: { name, message, stack? }, andtraceId?/spanId?when an OpenTelemetry span is active. EventEmitterEventBus.dispatchnever rejects from a handler failure — it always resolves. This means eventual-consistency projection or standalone-handler bugs cannot fail the originating command at the API boundary.- Broker adapters preserve transport-level redelivery semantics: after every handler has settled, if any rejected, the adapter re-throws so the consumer loop performs its existing ack/nack/commit-skip behavior (Kafka skips offset commit, NATS calls
msg.nak(), RabbitMQ callschannel.nack(msg, false, true)).
The only path by which a projection failure affects command outcomes is the strong-consistency projection mode (consistency: "strong"). Strong projections bypass the event bus entirely and enlist their load → reduce → save/delete in the originating command's UnitOfWork. A throw there fails the UoW commit and propagates to the command's dispatch. See View Persistence — Consistency Modes.
Connection Lifecycle
All three broker adapters implement Connectable from @noddde/core. The Domain manages the full lifecycle automatically:
- Construct — you create the bus with configuration in the
buses()factory. - Register handlers — the Domain registers event handlers via
on()for projections, sagas, and standalone event handlers. All adapter implementations buffer handlers registered beforeconnect(). - Auto-connect — after all handler registration is complete, the Domain detects
Connectablebuses and callsconnect()automatically. Connecting after handler registration prevents a race condition where broker-backed buses deliver queued messages before handlers are ready. If the broker is unreachable, wiring fails fast with a clear error. - Dispatch events —
dispatch()is called by the engine during the command lifecycle. - Auto-close — the Domain calls
close()on shutdown (viaCloseableauto-discovery). Unsubscribes handlers, disconnects, and releases resources. Idempotent.
You never need to call connect() or close() manually.
Resilience
All three broker adapters accept an optional resilience field of type BrokerResilience (from @noddde/core). This provides a consistent configuration shape across adapters for both connection-level and message-level resilience.
Connection Resilience
| Field | Kafka | NATS | RabbitMQ |
|---|---|---|---|
maxAttempts | retries (minus 1) | maxReconnectAttempts (-1 = infinite) | Initial connect retry attempts (mid-session reconnection is indefinite) |
initialDelayMs | initialRetryTime | reconnectTimeWait (fixed interval) | Base delay (exponential backoff) |
maxDelayMs | maxRetryTime | Ignored (fixed intervals) | Backoff cap |
Message Delivery Resilience
The maxRetries field limits per-message delivery attempts, preventing poison messages from blocking consumers indefinitely:
| Adapter | Mechanism | What Happens After Limit |
|---|---|---|
| Kafka | Consumer-side delivery count tracking | Message offset committed (skipped) |
| NATS | JetStream maxDeliver consumer option | NATS discards the message server-side |
| RabbitMQ | In-memory delivery count tracking | Message acked (discarded) |
All adapters also protect against deserialization failures (malformed JSON). Poison messages that cannot be parsed are logged and discarded without blocking the consumer.
Choosing an Adapter
| Scenario | Recommended Adapter |
|---|---|
| Development and testing | EventEmitterEventBus (in-memory) |
| Single-process production | EventEmitterEventBus (in-memory) |
| High-throughput event streaming | KafkaEventBus |
| Lightweight distributed messaging | NatsEventBus |
| Reliable message brokering with flexible routing | RabbitMqEventBus |
All broker adapters provide at-least-once delivery. Choose based on your existing infrastructure and operational expertise rather than feature differences.
Next Steps
- Outbox Pattern — for guaranteed-delivery publishing
- Custom Event Bus — to build your own