Read time: ~

Capstone: Build an Event-Driven Order System

Tie the whole course together by building the Order, Inventory, and Notification system end to end, with routing, confirms, dead-lettering, idempotency, observability, and tests.

Prerequisite: all of Sections 1 through 6 (Foundations through Production Readiness) You’ll need: Docker, a JDK, a build tool, and a relational database (for the outbox milestone).

This is the project that turns knowledge into skill. You have learned every piece in isolation: routing, serialization, acks, confirms, dead-lettering, idempotency, events, sagas, security, observability, and testing. Here you assemble them into one working, event-driven system, the same Order, Inventory, and Notification scenario that ran through the whole course, now built for real.

Treat this as a build brief, not a tutorial. Each milestone states the goal and the acceptance criteria; the linked modules hold the how. Build incrementally and verify each milestone before moving on.


What you’ll be able to do after this project

  • Assemble a multi-service event-driven system on RabbitMQ from scratch.
  • Make publishing reliable and consuming safe under failure and redelivery.
  • Apply production concerns: security, observability, and tests.
  • Justify each design decision by pointing to the trade-off behind it.

Target architecture

Three Spring Boot services communicate only through RabbitMQ. The Order Service owns the workflow; Inventory and Notification react to events.

flowchart TD
    Client["Client / API"]
    subgraph order ["Order Service"]
        API["POST /orders"]
        Outbox["Outbox relay"]
    end
    EXorders.topic (topic exchange)
    QI["inventory.queue"]
    QN["notification.queue"]
    DLXorders.dlx
    DLQ["inventory.dlq (parking lot)"]
    Inv["Inventory Service"]
    Notif["Notification Service"]

    Client -->|"place order"| API
    API -->|"write order + outbox row (1 tx)"| Outbox
    Outbox -->|"publish order.created (confirms)"| EX
    EX -->|"order.created"| QI
    EX -->|"order.created"| QN
    QI --> Inv
    QN --> Notif
    Inv -->|"publish stock.reserved / stock.rejected"| EX
    EX -.->|"stock.*"| QN
    QI -. "poison / retries exhausted" .-> DLX --> DLQ

Milestones

Each milestone builds on the previous one. Do them in order.

Milestone 1: Topology and the first event

Stand up the exchange, queues, and bindings, and publish a typed OrderCreated event from a REST endpoint that a consumer receives.

  • Declare a topic exchange orders.topic with inventory.queue and notification.queue bound to order.created.
  • Use JSON serialization with typed DTOs and trusted-package type mapping.
  • POST /orders publishes OrderCreated; both consumers log receipt.

Reference: Exchanges and Routing, Queues and Messages, First Producer and Consumer.

Acceptance: placing an order results in both Inventory and Notification receiving exactly one OrderCreated each.

Milestone 2: Reliable publishing

Guarantee that an accepted order’s event is never silently lost.

  • Enable publisher confirms (correlated) and returns (mandatory) with callbacks.
  • Write the order row and an outbox row in one database transaction; a relay drains the outbox and publishes with confirms.

Reference: Publisher Confirms, Transactional Outbox and Saga.

Acceptance: killing the app between the DB commit and the publish still results in the event being published after restart, exactly-once from the consumer’s perspective (helped by Milestone 3).

Milestone 3: Consumer robustness

Make consumers safe against failures, redelivery, and poison messages.

  • Configure prefetch and concurrency deliberately.
  • Add retry with backoff for transient failures, then dead-letter to a parking-lot DLQ for deterministic ones.
  • Make both consumers idempotent using the event id, so duplicate delivery causes no double effect.

Reference: Acknowledgements and Prefetch, Retry and Error Handling, Dead Letter Exchanges, Idempotency and Duplicates.

Acceptance: a message that always fails lands in the DLQ instead of looping forever; a message delivered twice reserves stock only once.

Milestone 4: The choreographed workflow

Extend one event into a multi-step business process with compensation.

  • Inventory reacts to OrderCreated and publishes StockReserved or StockRejected.
  • Notification reacts to both OrderCreated and the stock outcome.
  • On StockRejected, the Order Service marks the order CANCELLED (a compensation).

Reference: Event-Driven Architecture, Transactional Outbox and Saga.

Acceptance: a successful order ends CONFIRMED with a confirmation notification; an out-of-stock order ends CANCELLED with a rejection notification, and no stock stays reserved.

Milestone 5: Production hardening

Make it deployable and observable.

  • Replace the guest user with least-privilege per-service users scoped to a vhost; connect over TLS.
  • Expose Prometheus metrics and a broker health check; add tracing so one order id links across services.
  • Tune the consumer for throughput and confirm no connection or channel leaks.

Reference: Security, Observability, Performance Tuning.

Acceptance: each service authenticates as its own user over TLS, queue depth and processing time are visible on a dashboard, and a single trace spans the publish and both consumers.

Milestone 6: Prove it with tests

Lock the behavior down so it survives future change.

  • Integration-test the happy path and the DLQ path against a real broker with Testcontainers, asserting with Awaitility.
  • Contract-test the OrderCreated serialization.

Reference: Testing RabbitMQ Apps.

Acceptance: a green test suite that fails if routing, dead-lettering, idempotency, or the event contract regresses.


Acceptance checklist

Your system is complete when all of these hold:

  • Placing an order fans out one event to Inventory and Notification.
  • An accepted order’s event is never lost, even across a crash (outbox plus confirms).
  • A poison message is parked in a DLQ, never looped.
  • Duplicate delivery has no duplicate effect (idempotent consumers).
  • The out-of-stock path compensates and leaves no reserved stock.
  • Services use least-privilege users over TLS.
  • Metrics, health, and a cross-service trace are available.
  • Integration and contract tests cover the happy and failure paths.

Stretch goals

  • Add an analytics consumer that replays history from a RabbitMQ Stream rather than a queue.
  • Add a fraud-check service as a new subscriber without changing the Order Service, proving the loose coupling.
  • Introduce event schema versioning (order.created.v2) and migrate a consumer with zero downtime.

Checkpoint

You should now be able to:

  • Build an event-driven system across multiple services on RabbitMQ.
  • Combine confirms, outbox, DLX, and idempotency into end-to-end reliability.
  • Add security, observability, and tests to make it production-ready.
  • Defend each design choice by naming the trade-off it resolves.

Next: the Assessment to validate your understanding across the whole course.