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


Event-Driven Architecture with Java

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.

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.