Event-Driven Microservices with Spring Boot and Kafka
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: 3Lessons Learned
- Start with fewer topics. One topic per aggregate is enough.
- Idempotency is not optional. Every consumer must handle duplicates.
- Schema evolution is harder than you think. Version your events from day one.
- Consumer lag is your most important metric.
- Don't use events for queries. Use a local cache or read replica instead.
Comments
Stay Updated
Get my latest articles delivered straight to your inbox.
Related Articles
Designing Idempotent Payment Webhooks in Spring Boot
The exact patterns I use to handle Stripe and Coinbase callbacks without double-processing or lost events.
Custom Spring Boot Starters: Packaging Shared Infrastructure
Stop copy-pasting configs across services — build auto-configured starters your team will thank you for.