← Back to list

2025 Event-Driven Microservices with Java Spring Boot: Kafka & RabbitMQ

Introduction

Anand · 2025-12-19 18:38 · 2 claps · 4.3 min read
#apache-kafka #rabbitmq-cluster #microservice-architecture #java #springboot-3
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

2025 Event-Driven Microservices with Java Spring Boot: Kafka & RabbitMQ

Introduction

Event-driven architecture (EDA) has become a cornerstone of modern microservices design, enabling systems to be more scalable, resilient, and loosely coupled. In this comprehensive guide, we’ll explore how to build event-driven microservices using Java Spring Boot with two popular message brokers: Apache Kafka and RabbitMQ.

What is Event-Driven Architecture?

Event-driven architecture is a software design pattern where services communicate through events rather than direct API calls. When something significant happens in one service (an event), it publishes that information, and other interested services can react to it asynchronously.

Key Benefits:

  • Loose Coupling: Services don’t need to know about each other directly
  • Scalability: Easy to scale individual components independently
  • Resilience: Services can continue operating even if others are temporarily unavailable
  • Flexibility: New services can subscribe to existing events without modifying producers

Kafka vs RabbitMQ: Choosing the Right Tool

Apache Kafka

Kafka is a distributed event streaming platform designed for high-throughput, fault-tolerant data pipelines. It excels at handling millions of events per second and retaining data for replay.

Best for:

  • Event streaming and processing
  • Log aggregation
  • Real-time analytics
  • High-throughput scenarios
  • Event sourcing patterns

RabbitMQ

RabbitMQ is a traditional message broker that implements AMQP and supports various messaging patterns. It’s excellent for complex routing and reliable message delivery.

Best for:

  • Complex routing requirements
  • Request-reply patterns
  • Priority queues
  • Traditional messaging workflows
  • Lower throughput but complex delivery semantics

Building Event-Driven Microservices with Spring Boot

1. Setting Up Dependencies

For Kafka:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

For RabbitMQ:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2. Configuration

Kafka Configuration (application.yml):

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: my-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"

RabbitMQ Configuration (application.yml):

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

3. Implementing Event Producers

Kafka Producer Example:

@Service
@RequiredArgsConstructor
public class OrderEventProducer {

    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
    private static final String TOPIC = "order-events";

    public void publishOrderCreated(OrderEvent event) {
        kafkaTemplate.send(TOPIC, event.getOrderId(), event)
            .thenAccept(result -> 
                log.info("Order event published: {}", event.getOrderId()))
            .exceptionally(ex -> {
                log.error("Failed to publish order event", ex);
                return null;
            });
    }
}

RabbitMQ Producer Example:

@Service
@RequiredArgsConstructor
public class OrderEventPublisher {

    private final RabbitTemplate rabbitTemplate;
    private static final String EXCHANGE = "order-exchange";
    private static final String ROUTING_KEY = "order.created";

    public void publishOrderCreated(OrderEvent event) {
        try {
            rabbitTemplate.convertAndSend(EXCHANGE, ROUTING_KEY, event);
            log.info("Order event published: {}", event.getOrderId());
        } catch (Exception e) {
            log.error("Failed to publish order event", e);
        }
    }
}

4. Implementing Event Consumers

Kafka Consumer Example:

@Service
@Slf4j
public class OrderEventConsumer {

    @KafkaListener(topics = "order-events", groupId = "inventory-service")
    public void consumeOrderEvent(OrderEvent event) {
        log.info("Received order event: {}", event.getOrderId());
        // Process the event
        updateInventory(event);
    }

    private void updateInventory(OrderEvent event) {
        // Business logic to update inventory
        log.info("Inventory updated for order: {}", event.getOrderId());
    }
}

RabbitMQ Consumer Example:

@Service
@Slf4j
public class OrderEventListener {

    @RabbitListener(queues = "inventory-queue")
    public void handleOrderEvent(OrderEvent event) {
        log.info("Received order event: {}", event.getOrderId());
        // Process the event
        processOrder(event);
    }

    private void processOrder(OrderEvent event) {
        // Business logic
        log.info("Order processed: {}", event.getOrderId());
    }
}

5. Event Model

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderEvent {
    private String orderId;
    private String customerId;
    private LocalDateTime timestamp;
    private OrderStatus status;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem {
    private String productId;
    private Integer quantity;
    private BigDecimal price;
}
public enum OrderStatus {
    CREATED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

Advanced Patterns

1. Event Sourcing

Store all changes as a sequence of events rather than just the current state. This provides a complete audit trail and enables time travel debugging.

@Service
public class OrderEventStore {

    public void saveEvent(DomainEvent event) {
        // Store event in event store
        eventRepository.save(event);
        // Publish to event bus
        eventPublisher.publish(event);
    }

    public List<DomainEvent> getEventsByAggregateId(String aggregateId) {
        return eventRepository.findByAggregateId(aggregateId);
    }
}

2. CQRS (Command Query Responsibility Segregation)

Separate read and write models to optimize for different use cases.

// Command side - writes
@Service
public class OrderCommandService {
    public void createOrder(CreateOrderCommand command) {
        Order order = new Order(command);
        orderRepository.save(order);
        eventPublisher.publish(new OrderCreatedEvent(order));
    }
}
// Query side - reads
@Service
public class OrderQueryService {
    public OrderView getOrder(String orderId) {
        return orderViewRepository.findById(orderId);
    }
}

3. Saga Pattern

Manage distributed transactions across microservices using a sequence of local transactions coordinated by events.

@Service
public class OrderSaga {

    @KafkaListener(topics = "order-events")
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Step 1: Reserve inventory
        inventoryService.reserveItems(event.getItems());
    }

    @KafkaListener(topics = "inventory-events")
    public void handleInventoryReserved(InventoryReservedEvent event) {
        // Step 2: Process payment
        paymentService.processPayment(event.getOrderId());
    }

    @KafkaListener(topics = "payment-events")
    public void handlePaymentProcessed(PaymentProcessedEvent event) {
        // Step 3: Confirm order
        orderService.confirmOrder(event.getOrderId());
    }
}

Best Practices

1. Idempotency

Ensure consumers can handle duplicate messages safely:

@Service
public class IdempotentOrderConsumer {

    private final Set<String> processedEvents = ConcurrentHashMap.newKeySet();

    @KafkaListener(topics = "order-events")
    public void consumeOrder(OrderEvent event) {
        if (processedEvents.contains(event.getEventId())) {
            log.info("Event already processed: {}", event.getEventId());
            return;
        }

        processOrder(event);
        processedEvents.add(event.getEventId());
    }
}

2. Error Handling and Dead Letter Queues

Kafka DLQ Configuration:

@Configuration
public class KafkaErrorConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, OrderEvent> 
            kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, OrderEvent> factory = 
            new ConcurrentKafkaListenerContainerFactory<>();
        factory.setCommonErrorHandler(
            new DefaultErrorHandler(
                new DeadLetterPublishingRecoverer(kafkaTemplate()),
                new FixedBackOff(1000L, 3L)
            )
        );
        return factory;
    }
}

RabbitMQ DLQ Configuration:

@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable("order-queue")
            .withArgument("x-dead-letter-exchange", "dlx-exchange")
            .withArgument("x-dead-letter-routing-key", "order.dlq")
            .build();
    }
}

3. Monitoring and Observability

@Aspect
@Component
@Slf4j
public class EventMonitoringAspect {

    private final MeterRegistry meterRegistry;

    @Around("@annotation(KafkaListener)")
    public Object monitorKafkaConsumer(ProceedingJoinPoint joinPoint) throws Throwable {
        Timer.Sample sample = Timer.start(meterRegistry);
        try {
            Object result = joinPoint.proceed();
            sample.stop(Timer.builder("kafka.consumer.duration")
                .tag("method", joinPoint.getSignature().getName())
                .register(meterRegistry));
            return result;
        } catch (Exception e) {
            meterRegistry.counter("kafka.consumer.errors",
                "method", joinPoint.getSignature().getName()).increment();
            throw e;
        }
    }
}

Testing Event-Driven Systems

Integration Testing with Testcontainers

@SpringBootTest
@Testcontainers
class OrderEventIntegrationTest {

    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.4.0")
    );

    @DynamicPropertySource
    static void kafkaProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Test
    void shouldPublishAndConsumeOrderEvent() {
        OrderEvent event = new OrderEvent("order-123", "customer-456");
        orderEventProducer.publishOrderCreated(event);

        await().atMost(Duration.ofSeconds(5))
            .until(() -> orderEventConsumer.getProcessedEvents().contains("order-123"));
    }
}

Conclusion

Event-driven microservices with Spring Boot, Kafka, and RabbitMQ provide a powerful foundation for building scalable, resilient distributed systems. The choice between Kafka and RabbitMQ depends on your specific requirements: use Kafka for high-throughput event streaming and RabbitMQ for complex routing scenarios.

Key takeaways include implementing proper error handling, ensuring idempotency, using patterns like CQRS and Saga for complex workflows, and maintaining comprehensive monitoring. With these practices in place, you’ll be well-equipped to build robust event-driven architectures.


메타데이터
post_id
84d028890a6d
slug
2025-event-driven-microservices-with-java-spring-boot-kafka-rabbitmq-84d028890a6d
url
https://medium.com/@anandjeyaseelan10/2025-event-driven-microservices-with-java-spring-boot-kafka-rabbitmq-84d028890a6d
canonical_url
https://medium.com/@anandjeyaseelan10/2025-event-driven-microservices-with-java-spring-boot-kafka-rabbitmq-84d028890a6d
author_url
https://medium.com/@anandjeyaseelan10
status
ok
fetched_at
2026-06-09 15:37:30