MICROSERVICES.md and Recipe G in the repo.
Primitives
Integration vs domain
- Integration events cross service boundaries (
order.createdon NATS/Kafka). - Domain events can stay inside one binary via
EventBusuntil you need delivery guarantees.
In-process #[on_event] sketch
OrderProjector in providers; wire_on_event_handlers runs on bootstrap for HTTP/micro apps.
Emit:
microservices_events.rs for ClientsModule + EventBus wiring.
HTTP → DB → emit
- Validate body → transactional
OrderService::create. - After commit →
clients.expect("AUDIT").emit("order.created", &dto). - 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.placedto the broker. - Billing consumes
order.placedand emitspayment.captured. - Fulfillment consumes
payment.capturedand emitsshipment.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.prisma_model! or raw SQL.
Outbox table (PostgreSQL)
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)
event_id. event_version selects serde shapes. occurred_at drives SLAs and analytics.
CQRS read models
Subscribe toorder.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.- Checkout commits
ordersrow, writes outbox roworder.placed, publisher emits to Kafka topicprod.orders.events.v1. - Billing consumes
order.placed, charges card; emitspayment.capturedorpayment.failed(another topic). - Fulfillment listens for
payment.captured, reserves inventory; emitsshipment.requested. - Notifications listens to multiple topics and sends email/SMS—idempotent on
event_id.
#[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 /
retrytopic 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.