Read time: ~

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, notifications with RF 3 and min.insync.replicas 2 (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 /orders and publishes OrderCreated.

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 publishes PaymentSucceeded/PaymentFailed inside a Kafka transaction with sendOffsetsToTransaction.
  • Consumers of payments read with isolation.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, publishes StockReserved/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, @RetryableTopic or DefaultErrorHandler, 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 payments into revenue-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 payments to a database or S3 with a sink connector (see Kafka Connect).
  • Interactive queries: expose the Streams revenue-by-customer store 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.

  1. Build Milestones 1 to 8 in order, checking each acceptance criterion before moving on.
  2. For each milestone, produce a short note: the design choice, why, and the module it came from.
  3. Run the Hands-On Lab incidents against your finished system to confirm it degrades gracefully.
  4. Present the reference architecture and defend your topic, key, and delivery-semantics choices.

Next:Assessment, a scenario-based self-check across the whole course.