advanced~2h

Event-Driven Architecture with Kafka

Module 18 §4 flagged that a producer shouldn't have to block waiting for every interested service to finish reacting. This module is the mechanism that makes that possible.

Learning objectives

  • Beginner: A single producer and single consumer for one event type, within a small system, to see decoupling in action.
  • Intermediate: Multiple independent consumer services (inventory, notification, analytics) reacting to the same event, each with its own consumer group, added without ever modifying the producer.
  • Advanced: Idempotent consumer logic (e.g. an idempotency key stored per processed order) so at-least-once delivery can never double-charge or double-notify, combined with dead-letter handling for events a consumer can't process even after retries.

◆ The problem

If an Order service called the Inventory, Notification, and Analytics services directly via REST when an order is placed, it would need to know about all three, block waiting for all three to respond, and fail the entire order-placement flow if any one of them is temporarily down — even though "send an analytics event" failing shouldn't ever block a customer's order from completing.

A Kafka broker sits between them: the Order service publishes an "order placed" event to a topic and moves on immediately, with zero knowledge of who (if anyone) is listening. Any number of independent consumers can subscribe to that same topic and react in their own time, completely decoupled from the producer's own request lifecycle.

The producer publishes once and moves on. Kafka fans the event out to every subscribed consumer independently — a new consumer can be added later with zero change to the Order service.

@Service public class OrderService { private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate; public OrderResponse placeOrder(CreateOrderRequest request) { Order order = orderRepository.save(toOrder(request)); kafkaTemplate.send("order-placed", order.getId().toString(), new OrderPlacedEvent(order.getId(), order.getCustomerId(), order.getTotal())); return toResponse(order); // returns to the client immediately — doesn't wait for any consumer } }

💻 Code example

@Service public class OrderService { private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate; public OrderResponse placeOrder(CreateOrderRequest request) { Order order = orderRepository.save(toOrder(request)); kafkaTemplate.send("order-placed", order.getId().toString(), new OrderPlacedEvent(order.getId(), order.getCustomerId(), order.getTotal())); return toResponse(order); // returns to the client immediately — doesn't wait for any consumer } }
@Component public class OrderPlacedListener { @KafkaListener(topics = "order-placed", groupId = "notification-service") public void onOrderPlaced(OrderPlacedEvent event) { emailService.sendOrderConfirmation(event.customerId(), event.orderId()); } }

Notice this listener lives in a completely separate Spring Boot application (the Notification service) from the code that published the event — the two services share no direct dependency on each other, only a shared understanding of the event's shape and the topic name.

💻 Code example

@Component public class OrderPlacedListener { @KafkaListener(topics = "order-placed", groupId = "notification-service") public void onOrderPlaced(OrderPlacedEvent event) { emailService.sendOrderConfirmation(event.customerId(), event.orderId()); } }

◆ Under the hood — what happens when a consumer crashes mid-event

Kafka's consumer offset mechanism means a consumer that crashes after receiving an event but before finishing its work will reprocess that same event on restart — Kafka guarantees at-least-once delivery by default, not exactly-once. This has a direct design implication: onOrderPlaced above (sending an email) should ideally be idempotent or tolerant of a duplicate call, since "the same order confirmation email sent twice" is a realistic outcome under consumer restart/rebalance, not an edge case to ignore.

▲ Pitfall

Treating a Kafka consumer method as if it will run exactly once per event, without considering redelivery on crash/rebalance, is one of the most common event-driven design mistakes — for anything with a real-world side effect (charging a card, sending an email), design for "this might run twice" from the start rather than retrofitting idempotency later.

✓ Quick recap

Does the Order service need to know which services consume the order-placed event? No — that's the entire point; producers and consumers are decoupled through the broker. What delivery guarantee does Kafka provide by default, and what does that imply for consumer code? At-least-once — a crash can cause an event to be reprocessed, so consumer side effects should be designed to tolerate duplicates.

Want a visual for this concept?

Generate a diagram tailored to “Event-Driven Architecture with Kafka” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to API Gateway & Service Communication← Back to all Spring Boot chapters