Event-Driven Architecture
IntermediateServices emit domain events when state changes; other services subscribe and react autonomously — enabling loose coupling, audit trails, and temporal decoupling.
Overview
Event-Driven Architecture (EDA) is a design paradigm where services communicate by producing and consuming events rather than making synchronous API calls. When something meaningful happens in a service (an order is placed, a payment succeeds, a user registers), the service publishes an immutable domain event to a message broker. Other services subscribe to the events they care about and react independently — they don't know who produced the event and the producer doesn't know who consumes it. This temporal decoupling means services can evolve, scale, and fail independently. EDA is the foundation for microservices architectures that require high resilience, horizontal scalability, and audit trails.
Domain Events — What to Publish
A domain event is a fact that describes something that happened in the past — always named in the past tense (OrderPlaced, PaymentFailed, UserRegistered). Events are immutable: you never update a published event. They should carry enough data for consumers to act without needing a follow-up API call (event-carried state transfer), but not so much that the producer is coupled to every consumer's data needs.
Design events at the correct granularity: too fine-grained events create chatty consumers; too coarse events force consumers to ignore irrelevant fields.
// Domain event — immutable record of something that happened
@Value // Lombok immutable POJO
public class OrderPlacedEvent {
String eventId; // UUID — for idempotency
String orderId;
String customerId;
BigDecimal totalAmount;
List<OrderItem> items;
Instant occurredAt;
}
// Publish via Spring ApplicationEventPublisher (in-process)
// or Kafka/RabbitMQ (cross-service)
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final KafkaTemplate<String, OrderPlacedEvent> kafka;
@Transactional
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
OrderPlacedEvent event = OrderPlacedEvent.builder()
.eventId(UUID.randomUUID().toString())
.orderId(order.getId().toString())
.customerId(req.getCustomerId())
.totalAmount(req.getTotal())
.items(req.getItems())
.occurredAt(Instant.now())
.build();
kafka.send("order-events", order.getId().toString(), event);
return order;
}
}Consumers & Idempotency
Event consumers must be idempotent — processing the same event twice must produce the same result as processing it once. Message brokers guarantee at-least-once delivery, so duplicates are inevitable (network retries, consumer restarts, rebalances). Achieve idempotency by storing a processed eventId in a DB table and checking it before processing.
Keep consumer logic simple: one event → one local transaction. Avoid triggering synchronous API calls inside a consumer — use published events or commands instead to stay decoupled.
@Component
@RequiredArgsConstructor
public class NotificationConsumer {
private final NotificationService notificationService;
private final ProcessedEventRepository processedEventRepo;
@KafkaListener(topics = "order-events", groupId = "notification-service")
@Transactional
public void onOrderPlaced(OrderPlacedEvent event) {
// Idempotency check — skip if already processed
if (processedEventRepo.existsByEventId(event.getEventId())) {
log.info("Skipping duplicate event {}", event.getEventId());
return;
}
// Business logic
notificationService.sendOrderConfirmation(
event.getCustomerId(),
event.getOrderId(),
event.getTotalAmount()
);
// Mark as processed within same transaction
processedEventRepo.save(new ProcessedEvent(event.getEventId(), Instant.now()));
}
}Outbox Pattern — Reliable Event Publishing
A critical problem: if a service saves data to its DB and then publishes an event to Kafka, a crash between the two steps leaves the DB updated but no event published — an inconsistency. The Outbox Pattern solves this: the service writes both the domain entity AND the event to the same DB transaction. A separate relay process (Debezium CDC or a polling loop) reads unpublished events and forwards them to the broker, then marks them as published.
// Outbox pattern — atomic DB write + event record
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final OutboxRepository outboxRepo; // same DB as orders
@Transactional // single DB transaction — either both saved or neither
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
// Write event to outbox table in the SAME transaction
outboxRepo.save(OutboxEvent.builder()
.aggregateId(order.getId().toString())
.eventType("OrderPlaced")
.payload(serialize(order)) // JSON snapshot
.createdAt(Instant.now())
.published(false)
.build());
return order; // Kafka publish happens via CDC relay — NOT here
}
}
// Outbox relay (Debezium or scheduled job) — separate process
// Reads unpublished rows → forwards to Kafka → marks published=trueKey Points to Remember
- 1Domain events are immutable past-tense facts (OrderPlaced, PaymentFailed); they carry enough data for consumers to act without follow-up API calls.
- 2EDA provides temporal decoupling — the producer does not know consumers exist; consumers do not know the producer's internal state.
- 3Consumers must be idempotent because at-least-once delivery guarantees duplicates; use an eventId deduplication table.
- 4The Outbox Pattern ensures reliable event publishing: write the event and business data in the same DB transaction, relay separately to the broker.
- 5EDA is eventually consistent — a window exists between event publication and consumer processing; design your system to handle this gracefully.
- 6Prefer event-carried state transfer (events contain enough data) over event notification (consumers must call back) to avoid synchronous coupling.
Interview Questions
Sign in to ask AriaWhat is event-driven architecture and how does it differ from synchronous REST communication?
Why must event consumers be idempotent in an EDA system?
What is the Outbox Pattern and what problem does it solve?
What is the difference between event notification and event-carried state transfer?
How do you handle schema evolution of events in a long-lived EDA system?
Ask Aria about Event-Driven Architecture
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.