noddde
Event Bus

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

PackageBrokerClient LibraryDelivery Guarantee
@noddde/engineNode.js EventEmitterIn-process only
@noddde/kafkaKafkakafkajsAt-least-once
@noddde/natsNATSnatsAt-least-once (JetStream)
@noddde/rabbitmqRabbitMQamqplibAt-least-once

Per-Adapter Setup

The EventBus Interface

Every adapter implements this interface:

packages/core/src/edd/event-bus.ts
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 from Closeable. 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 Logger at error level with structured fields: eventName, eventId?, handlerName, error: { name, message, stack? }, and traceId?/spanId? when an OpenTelemetry span is active.
  • EventEmitterEventBus.dispatch never 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 calls channel.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:

  1. Construct — you create the bus with configuration in the buses() factory.
  2. Register handlers — the Domain registers event handlers via on() for projections, sagas, and standalone event handlers. All adapter implementations buffer handlers registered before connect().
  3. Auto-connect — after all handler registration is complete, the Domain detects Connectable buses and calls connect() 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.
  4. Dispatch eventsdispatch() is called by the engine during the command lifecycle.
  5. Auto-close — the Domain calls close() on shutdown (via Closeable auto-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

FieldKafkaNATSRabbitMQ
maxAttemptsretries (minus 1)maxReconnectAttempts (-1 = infinite)Initial connect retry attempts (mid-session reconnection is indefinite)
initialDelayMsinitialRetryTimereconnectTimeWait (fixed interval)Base delay (exponential backoff)
maxDelayMsmaxRetryTimeIgnored (fixed intervals)Backoff cap

Message Delivery Resilience

The maxRetries field limits per-message delivery attempts, preventing poison messages from blocking consumers indefinitely:

AdapterMechanismWhat Happens After Limit
KafkaConsumer-side delivery count trackingMessage offset committed (skipped)
NATSJetStream maxDeliver consumer optionNATS discards the message server-side
RabbitMQIn-memory delivery count trackingMessage 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

ScenarioRecommended Adapter
Development and testingEventEmitterEventBus (in-memory)
Single-process productionEventEmitterEventBus (in-memory)
High-throughput event streamingKafkaEventBus
Lightweight distributed messagingNatsEventBus
Reliable message brokering with flexible routingRabbitMqEventBus

All broker adapters provide at-least-once delivery. Choose based on your existing infrastructure and operational expertise rather than feature differences.

Next Steps

On this page