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.topicwithinventory.queueandnotification.queuebound toorder.created. - Use JSON serialization with typed DTOs and trusted-package type mapping.
POST /orderspublishesOrderCreated; 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
OrderCreatedand publishesStockReservedorStockRejected. - Notification reacts to both
OrderCreatedand 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
guestuser 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
OrderCreatedserialization.
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.