Event-Driven Architecture with Java: A Practical Guide (2025)

Event-Driven Architecture (EDA) has become a cornerstone of modern distributed systems. This comprehensive guide explores how to implement EDA patterns in Java, from basic event handling to advanced event sourcing and CQRS patterns.
Pro Tip: Understanding event-driven patterns helps developers build scalable, loosely coupled systems that can handle complex business workflows.
Table of Contents
EDA Basics
Note: Event-Driven Architecture enables loose coupling between components through asynchronous event communication.
Basic Event Model
public abstract class DomainEvent {
private final String eventId;
private final LocalDateTime timestamp;
private final String aggregateId;
protected DomainEvent(String aggregateId) {
this.eventId = UUID.randomUUID().toString();
this.timestamp = LocalDateTime.now();
this.aggregateId = aggregateId;
}
// Getters
}
public class OrderCreatedEvent extends DomainEvent {
private final String orderId;
private final List items;
public OrderCreatedEvent(String orderId, List items) {
super(orderId);
this.orderId = orderId;
this.items = items;
}
}
public class OrderProcessedEvent extends DomainEvent {
private final String orderId;
private final OrderStatus status;
public OrderProcessedEvent(String orderId, OrderStatus status) {
super(orderId);
this.orderId = orderId;
this.status = status;
}
}
Event Patterns
Pro Tip: Understanding different event patterns helps in choosing the right approach for your use case.
Event Publisher
public interface EventPublisher {
void publish(DomainEvent event);
}
@Service
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate kafkaTemplate;
@Autowired
public KafkaEventPublisher(KafkaTemplate kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void publish(DomainEvent event) {
kafkaTemplate.send(event.getClass().getSimpleName(), event)
.addCallback(
success -> log.info("Event published successfully: {}", event.getEventId()),
failure -> log.error("Failed to publish event: {}", event.getEventId(), failure)
);
}
}
@Service
public class OrderService {
private final EventPublisher eventPublisher;
@Autowired
public OrderService(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void createOrder(OrderRequest request) {
// Create order
Order order = new Order(request);
// Publish event
eventPublisher.publish(new OrderCreatedEvent(
order.getId(),
order.getItems()
));
}
}
Kafka Integration
Note: Kafka provides a robust platform for event streaming with high throughput and fault tolerance.
Kafka Configuration
@Configuration
public class KafkaConfig {
@Bean
public KafkaTemplate kafkaTemplate(
ProducerFactory producerFactory) {
return new KafkaTemplate<>(producerFactory);
}
@Bean
public NewTopic orderEventsTopic() {
return TopicBuilder.name("order-events")
.partitions(3)
.replicas(2)
.build();
}
}
@Service
public class OrderEventConsumer {
@KafkaListener(topics = "order-events", groupId = "order-processor")
public void handleOrderEvent(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
handleOrderCreated((OrderCreatedEvent) event);
} else if (event instanceof OrderProcessedEvent) {
handleOrderProcessed((OrderProcessedEvent) event);
}
}
private void handleOrderCreated(OrderCreatedEvent event) {
// Process order creation
orderService.processOrder(event.getOrderId());
}
}
RabbitMQ Integration
Pro Tip: RabbitMQ is excellent for traditional message queuing with support for various exchange types and routing patterns.
RabbitMQ Configuration
@Configuration
public class RabbitMQConfig {
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order-events")
.withArgument("x-dead-letter-exchange", "order-dlx")
.build();
}
@Bean
public Exchange orderExchange() {
return ExchangeBuilder.topicExchange("order-events")
.durable(true)
.build();
}
@Bean
public Binding orderBinding(Queue orderQueue, Exchange orderExchange) {
return BindingBuilder
.bind(orderQueue)
.to(orderExchange)
.with("order.*")
.noargs();
}
}
@Service
public class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
@Autowired
public OrderEventPublisher(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void publishOrderEvent(DomainEvent event) {
rabbitTemplate.convertAndSend(
"order-events",
"order." + event.getClass().getSimpleName().toLowerCase(),
event
);
}
}
Event Sourcing
Note: Event Sourcing stores the complete history of state changes as a sequence of events.
Event Store Implementation
public interface EventStore {
void save(String aggregateId, List events);
List getEvents(String aggregateId);
}
@Service
public class JpaEventStore implements EventStore {
private final EventRepository eventRepository;
@Autowired
public JpaEventStore(EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
@Override
public void save(String aggregateId, List events) {
List entities = events.stream()
.map(event -> new EventEntity(event))
.collect(Collectors.toList());
eventRepository.saveAll(entities);
}
@Override
public List getEvents(String aggregateId) {
return eventRepository.findByAggregateIdOrderByTimestampAsc(aggregateId)
.stream()
.map(EventEntity::toDomainEvent)
.collect(Collectors.toList());
}
}
@Entity
public class EventEntity {
@Id
@GeneratedValue
private Long id;
private String eventId;
private String aggregateId;
private String eventType;
private LocalDateTime timestamp;
@Column(columnDefinition = "TEXT")
private String eventData;
// Getters, setters, and conversion methods
}
CQRS Pattern
Pro Tip: CQRS separates read and write operations to optimize performance and scalability.
CQRS Implementation
@Service
public class OrderCommandService {
private final EventStore eventStore;
private final EventPublisher eventPublisher;
@Autowired
public OrderCommandService(EventStore eventStore, EventPublisher eventPublisher) {
this.eventStore = eventStore;
this.eventPublisher = eventPublisher;
}
public void createOrder(OrderRequest request) {
Order order = new Order(request);
List events = order.getUncommittedEvents();
eventStore.save(order.getId(), events);
events.forEach(eventPublisher::publish);
order.clearUncommittedEvents();
}
}
@Service
public class OrderQueryService {
private final OrderReadRepository readRepository;
@Autowired
public OrderQueryService(OrderReadRepository readRepository) {
this.readRepository = readRepository;
}
public OrderDTO getOrder(String orderId) {
return readRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
public List getOrdersByStatus(OrderStatus status) {
return readRepository.findByStatus(status);
}
}
@Projection
public class OrderProjection {
@EventHandler
public void on(OrderCreatedEvent event) {
OrderDTO order = new OrderDTO(event.getOrderId());
order.setStatus(OrderStatus.CREATED);
readRepository.save(order);
}
@EventHandler
public void on(OrderProcessedEvent event) {
OrderDTO order = readRepository.findById(event.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(event.getOrderId()));
order.setStatus(event.getStatus());
readRepository.save(order);
}
}
Best Practices
Note: Following best practices ensures reliable and maintainable event-driven systems.
Event Versioning
public abstract class VersionedEvent extends DomainEvent {
private final int version;
protected VersionedEvent(String aggregateId, int version) {
super(aggregateId);
this.version = version;
}
public int getVersion() {
return version;
}
}
public class OrderCreatedEventV1 extends VersionedEvent {
private final String orderId;
private final List items;
public OrderCreatedEventV1(String orderId, List items) {
super(orderId, 1);
this.orderId = orderId;
this.items = items;
}
}
public class OrderCreatedEventV2 extends VersionedEvent {
private final String orderId;
private final List items;
private final String customerId;
public OrderCreatedEventV2(String orderId, List items, String customerId) {
super(orderId, 2);
this.orderId = orderId;
this.items = items;
this.customerId = customerId;
}
}
Best Practices Summary
- Use event versioning for schema evolution
- Implement proper error handling and retries
- Use event validation and schema enforcement
- Implement proper monitoring and tracing
- Use appropriate event granularity
- Implement proper security measures
- Use event sourcing for audit trails
- Implement proper testing strategies
Conclusion
Event-Driven Architecture with Java provides a powerful approach to building scalable, maintainable systems. By understanding the patterns, implementing proper event handling, and following best practices, developers can create robust event-driven applications that meet modern business needs.