Skip to content
Blog
Backend15 min read

Event-Driven Microservices with Spring Boot and Kafka

B

BADJO Dibéa Koffi

Published on May 13, 2026

Why Event-Driven?

REST APIs are synchronous. Service A calls Service B, waits for a response, then continues. This works until Service B is slow or down, and Service A hangs with it.

Event-driven architecture decouples services through a message broker. Services publish events when something happens. Other services react when they're ready.

Defining Events

Events are immutable facts. They describe what happened, not what should happen:

public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    List<OrderItem> items,
    BigDecimal total,
    Instant createdAt
) implements DomainEvent {
    @Override
    public String aggregateId() {
        return orderId.toString();
    }
}

Publishing Events

@Service
public class OrderService {
    private final StreamBridge streamBridge;
    private final OrderRepository repo;
 
    public Order createOrder(CreateOrderRequest request) {
        var order = Order.from(request);
        repo.save(order);
 
        var event = new OrderCreatedEvent(
            order.getId(), order.getCustomerId(),
            order.getItems(), order.getTotal(), Instant.now()
        );
 
        streamBridge.send("order-events", event);
        return order;
    }
}

The Idempotency Problem

Kafka guarantees at-least-once delivery. Your consumer might process the same event twice. If your consumer charges a credit card, you'll charge it twice.

@Transactional
public void charge(UUID orderId, BigDecimal amount) {
    if (paymentRepo.existsByOrderId(orderId)) {
        log.warn("Payment already processed for order {}", orderId);
        return;
    }
    var payment = Payment.create(orderId, amount);
    paymentRepo.save(payment);
    stripeClient.charge(payment);
}

Dead Letter Topics

When a consumer fails after N retries, the message goes to a dead letter topic. Without this, a single bad message blocks the entire partition.

spring:
  cloud:
    stream:
      kafka:
        bindings:
          processPayment-in-0:
            consumer:
              enableDlq: true
              dlqName: order-events.payment.dlq
              maxRetryAttempts: 3

Lessons Learned

  1. Start with fewer topics. One topic per aggregate is enough.
  2. Idempotency is not optional. Every consumer must handle duplicates.
  3. Schema evolution is harder than you think. Version your events from day one.
  4. Consumer lag is your most important metric.
  5. Don't use events for queries. Use a local cache or read replica instead.
spring-bootkafkaevent-sourcingmicroservices
Share

Comments