Capstone Project: Build an Event-Driven Order System
End-to-end build across Order, Payment, Inventory, and Notification with topic design, the outbox, exactly-once, Kafka Streams, retries and a DLT, schemas, and observability.
This capstone ties the whole course together. You build the four-service Order/Payment/Inventory/Notification system for real, applying topic design, reliable publishing, exactly-once processing, streaming aggregation, error handling, schemas, and observability. Work through the milestones in order: each has acceptance criteria and links to the module that teaches it. The goal is a system you could defend in a design review, not just code that runs.
What you’ll be able to do after this module
- Design topics, keys, and partitions for a real event flow.
- Publish reliably with the transactional outbox pattern.
- Implement exactly-once consume-transform-produce.
- Add a Kafka Streams aggregation, retries, a DLT, schemas, and observability.
1. The reference architecture
The target system: four services communicating only through Kafka topics, with a Streams app for analytics and observability throughout.
flowchart TD
order["Order service"] -->|OrderCreated| t1["orders"]
t1 --> pay["Payment service"]
pay -->|PaymentSucceeded / Failed| t2["payments"]
t2 --> inv["Inventory service"]
inv -->|StockReserved / Rejected| t3["inventory"]
t1 --> notif["Notification service"]
t2 --> notif
t3 --> notif
notif -->|sends| cust["Customer"]
t2 --> streams["Streams: revenue by customer"]
streams --> t4["revenue-by-customer"]
Each service owns its data and publishes events; no service calls another synchronously. This is the choreography model from Event-Driven Architecture.
2. Milestones
Build in this order. Do not start a milestone until the previous one meets its acceptance criteria.
Milestone 1: Topic design and the Order service
Design the topics and get orders flowing.
- Create
orders,payments,inventory,notificationswith RF 3 andmin.insync.replicas2 (RF 1 locally if single-broker). - Choose keys: order id where per-order ordering matters, customer id where per-customer ordering matters.
- The Order service exposes
POST /ordersand publishesOrderCreated.
Acceptance: an API call produces an OrderCreated record on orders, verifiable with the console consumer. See Event-Driven Architecture and First Producer and Consumer.
Milestone 2: Reliable publishing with the outbox
Remove the dual-write risk.
- Persist the order and an outbox row in one DB transaction.
- Relay outbox rows to
orders(a poller or CDC).
Acceptance: killing the app between DB commit and publish never loses or double-commits an event. See Outbox and CDC.
Milestone 3: Exactly-once in the Payment service
Consume-transform-produce without duplicates.
- Payment consumes
OrderCreated, charges, and publishesPaymentSucceeded/PaymentFailedinside a Kafka transaction withsendOffsetsToTransaction. - Consumers of
paymentsread withisolation.level=read_committed.
Acceptance: a forced failure mid-transaction leaves no partial output visible to read_committed consumers. See Transactions and EOS.
Milestone 4: Idempotent Inventory service
Survive redelivery.
- Inventory consumes
PaymentSucceeded, reserves stock, publishesStockReserved/StockRejected. - Dedup with an inbox table keyed by event id.
Acceptance: replaying the same PaymentSucceeded reserves stock only once. See Idempotency and Ordering.
Milestone 5: Retries and a DLT
Handle bad data without blocking.
- Add
ErrorHandlingDeserializer,@RetryableTopicorDefaultErrorHandler, and a DLT on the consumers.
Acceptance: a poison record retries, lands in the DLT, and does not block its partition. See Retry and Error Handling.
Milestone 6: Schemas
Enforce contracts.
- Define Avro schemas for the events and register them; produce/consume via the Schema Registry.
Acceptance: a backward-incompatible change is rejected at registration. See Schema Registry.
Milestone 7: Streams aggregation
Add analytics.
- A Kafka Streams app aggregates
paymentsintorevenue-by-customer.
Acceptance: the aggregate updates as payments flow, verifiable by reading revenue-by-customer. See Kafka Streams.
Milestone 8: Observability
Make it visible.
- Expose Micrometer/Actuator metrics; watch consumer lag; add tracing across services.
Acceptance: you can see per-group lag and trace one order across all services. See Observability.
3. Worked slice: exactly-once payment
The heart of the system is Milestone 3. The Payment listener consumes, charges, and produces atomically.
@KafkaListener(topics = "orders", groupId = "payment-service")
@Transactional("kafkaTransactionManager")
public void onOrderCreated(OrderCreated event) {
PaymentResult result = paymentGateway.charge(event.orderId(), event.amount());
var outcome = result.succeeded()
? new PaymentSucceeded(event.orderId(), event.customerId(), event.amount())
: new PaymentFailed(event.orderId(), result.reason());
kafkaTemplate.send("payments", String.valueOf(event.orderId()), outcome);
// offsets are committed as part of the same Kafka transaction
}
With the transaction manager and read_committed downstream, either the payment event and the consumed offset both commit, or neither does, the exactly-once guarantee within Kafka from Transactions and EOS.
4. Stretch goals
Once the eight milestones pass, extend the system.
- Connect sink: stream
paymentsto a database or S3 with a sink connector (see Kafka Connect). - Interactive queries: expose the Streams
revenue-by-customerstore over an HTTP endpoint. - Multi-partition ordering: prove per-customer ordering holds under load with the customer id as key.
5. Guided practical
The milestones are the practical. Track your progress against the acceptance criteria.
- Build Milestones 1 to 8 in order, checking each acceptance criterion before moving on.
- For each milestone, produce a short note: the design choice, why, and the module it came from.
- Run the Hands-On Lab incidents against your finished system to confirm it degrades gracefully.
- Present the reference architecture and defend your topic, key, and delivery-semantics choices.
Next:Assessment, a scenario-based self-check across the whole course.