Home/Learn/Microservices/Event-Driven Architecture

Event-Driven Architecture

Intermediate
Communication

Services 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.

Java — Publishing a Domain Event
// 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.

Java — Idempotent Consumer
@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.

Java — Outbox Pattern
// 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=true

Key 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 Aria
1

What is event-driven architecture and how does it differ from synchronous REST communication?

EasyAmazon
2

Why must event consumers be idempotent in an EDA system?

MediumUber
3

What is the Outbox Pattern and what problem does it solve?

HardNetflix
4

What is the difference between event notification and event-carried state transfer?

MediumThoughtworks
5

How do you handle schema evolution of events in a long-lived EDA system?

HardLinkedIn

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.

Loading discussion…