Skip to main content
Patterns align with MICROSERVICES.md and Recipe G in the repo.

Primitives

Integration vs domain

  • Integration events cross service boundaries (order.created on NATS/Kafka).
  • Domain events can stay inside one binary via EventBus until you need delivery guarantees.

In-process #[on_event] sketch

Register OrderProjector in providers; wire_on_event_handlers runs on bootstrap for HTTP/micro apps. Emit:
See microservices_events.rs for ClientsModule + EventBus wiring.

HTTP → DB → emit

  1. Validate body → transactional OrderService::create.
  2. After commit → clients.expect("AUDIT").emit("order.created", &dto).
  3. If emit can fail post-commit, move to outbox (below).

End-to-end example: checkout, billing, fulfillment

This is a real production-style choreography:
  • Checkout API creates the order and writes an outbox row.
  • Outbox publisher emits order.placed to the broker.
  • Billing consumes order.placed and emits payment.captured.
  • Fulfillment consumes payment.captured and emits shipment.requested.

Producer side: create order + outbox row

Outbox (PostgreSQL / Prisma)

1

Transactional write

Persist business row and outbox row in one DB transaction.
2

Async publisher

Worker polls outbox, emit, marks delivered with retries + DLQ policy.
3

Idempotency

Consumers dedupe with event_id / idempotency_key—assume at-least-once delivery.
nestrs does not ship an outbox crate—model the table with prisma_model! or raw SQL.

Outbox table (PostgreSQL)

The HTTP handler inserts business row + outbox row in one transaction. A background tokio task (or separate worker binary) SELECT … FOR UPDATE SKIP LOCKED, publishes via ClientProxy::emit, then sets published_at. After N failures, move rows to integration_dead_letter for manual replay.

Publisher worker: ship pending outbox rows

Integration event envelope (what crosses the broker)

Consumers dedupe on event_id. event_version selects serde shapes. occurred_at drives SLAs and analytics.

CQRS read models

Subscribe to order.created (broker or on_event) and project into Redis, Mongo, or Elasticsearch for read-heavy queries—keep HTTP write path small.

Choreography example (multiple services)

Choreography means no central orchestrator: each service reacts to events it cares about.
  1. Checkout commits orders row, writes outbox row order.placed, publisher emits to Kafka topic prod.orders.events.v1.
  2. Billing consumes order.placed, charges card; emits payment.captured or payment.failed (another topic).
  3. Fulfillment listens for payment.captured, reserves inventory; emits shipment.requested.
  4. Notifications listens to multiple topics and sends email/SMS—idempotent on event_id.
nestrs #[event_pattern] handlers implement step 2–4 consumers on NestFactory::create_microservice_*; emit / ClientProxy emit the next integration events.

Consumer side: billing reacts to order.placed

Dead-letter and replay

When a handler throws after partial side effects, do not lose the message:
  • Message brokers: configure DLQ / retry topic with exponential backoff.
  • In-process EventBus: log + metric failures; optionally persist failed payloads to SQL for replay tooling.

What “exactly-once” really means

Brokers are at-least-once. Exactly-once processing is achieved only by idempotent handlers + dedupe store (event_id → processed timestamp). Design for duplicates from day one.
Keep event_version in payloads when evolving schemas so old consumers can branch safely.