Consumir Topicos de Kafka con Spring Boot Stream Listeners
Construir consumers de Kafka en Spring Boot usando anotaciones @KafkaListener, consumers concurrentes, error handlers, patrones DLQ y batch listeners con acknowledgment manual.
Descripcion general
La integracion de Kafka con Spring Boot proporciona anotaciones @KafkaListener para configuracion declarativa de consumers. A continuacion: configurar consumers de Kafka, consumo concurrente, manejo de errores con DLQ, batch listeners, acknowledgment manual y routing basado en headers.
Cuando Usar Esto
-
For alternatives, see Kafka Consumer Groups with Python for Scalable Streaming.
-
Microservicios de Spring Boot que consumen eventos de Kafka
-
Aplicaciones que necesitan deserializacion tipada de mensajes (JSON a POJO)
-
Procesamiento en batch de mensajes de Kafka con garantias transaccionales
-
Consumers multi-topico con logica de procesamiento diferente por topico
Prerrequisitos
- Java 17+
- Spring Boot 3.x
- Dependencia Spring Kafka
Solucion
1. Configuracion
# application.yml
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: order-service
auto-offset-reset: earliest
enable-auto-commit: false
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "com.example.events"
listener:
ack-mode: manual
concurrency: 3
2. Kafka Listener Basico
package com.example.kafka;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class OrderConsumer {
@KafkaListener(
topics = "orders",
groupId = "order-service",
containerFactory = "kafkaListenerContainerFactory"
)
public void handleOrder(OrderEvent event) {
System.out.println("Received order: " + event.getOrderId());
processOrder(event);
}
private void processOrder(OrderEvent event) {
// Logica de negocio
}
}
3. Bean de Configuracion de Consumer
package com.example.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import java.util.HashMap;
import java.util.Map;
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public ConsumerFactory<String, Object> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.example.events");
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(3);
factory.getContainerProperties().setAckMode(
org.springframework.kafka.listener.ContainerProperties.AckMode.MANUAL
);
return factory;
}
}
4. Manejo de Errores con DLQ
package com.example.kafka;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.ErrorHandler;
import org.springframework.kafka.listener.MessageListenerContainer;
import org.springframework.stereotype.Service;
@Service
public class OrderConsumerWithErrorHandler {
private final KafkaTemplate<String, Object> kafkaTemplate;
public OrderConsumerWithErrorHandler(KafkaTemplate<String, Object> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@KafkaListener(topics = "orders", groupId = "order-service")
public void handleOrder(ConsumerRecord<String, OrderEvent> record) {
try {
OrderEvent event = record.value();
processOrder(event);
} catch (Exception e) {
// Enviar a topico DLQ
kafkaTemplate.send("orders.dlq", record.key(), record.value());
System.err.println("Sent to DLQ: " + e.getMessage());
}
}
private void processOrder(OrderEvent event) {
if (event.getOrderId() == null) {
throw new IllegalArgumentException("Missing orderId");
}
// Logica de negocio
}
}
5. Batch Listener
package com.example.kafka;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BatchOrderConsumer {
@KafkaListener(
topics = "orders",
groupId = "order-batch-service",
batch = "true"
)
public void handleBatch(List<ConsumerRecord<String, OrderEvent>> records, Acknowledgment ack) {
System.out.println("Processing batch of " + records.size() + " records");
for (ConsumerRecord<String, OrderEvent> record : records) {
try {
processOrder(record.value());
} catch (Exception e) {
System.err.println("Failed to process record at offset " + record.offset() + ": " + e);
// Continuar procesando el resto del batch
}
}
// Acknowledge despues de que el batch se procesa
ack.acknowledge();
}
private void processOrder(OrderEvent event) {
// Logica de negocio
}
}
6. Acknowledgment Manual
@Service
public class ManualAckConsumer {
@KafkaListener(topics = "orders", groupId = "order-manual-service")
public void handleOrder(
ConsumerRecord<String, OrderEvent> record,
Acknowledgment acknowledgment
) {
try {
OrderEvent event = record.value();
processOrder(event);
// Acknowledge solo despues de procesamiento exitoso
acknowledgment.acknowledge();
} catch (Exception e) {
// No acknowledge — el mensaje se re-entregara
System.err.println("Processing failed, not acknowledging: " + e);
// Opcionalmente: dormir y reintentar, o enviar a DLQ
}
}
}
7. Routing Basado en Headers
@Service
public class RoutingConsumer {
@KafkaListener(topics = "events", groupId = "event-processor")
public void handleEvent(
ConsumerRecord<String, byte[]> record,
Acknowledgment ack
) {
String eventType = new String(record.headers().lastHeader("event-type").value());
switch (eventType) {
case "ORDER_CREATED" -> processOrderCreated(record.value());
case "ORDER_CANCELLED" -> processOrderCancelled(record.value());
case "PAYMENT_PROCESSED" -> processPayment(record.value());
default -> System.err.println("Unknown event type: " + eventType);
}
ack.acknowledge();
}
}
8. Multiples Topicos con Diferentes Containers
@Configuration
public class MultiTopicConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> ordersFactory(
ConsumerFactory<String, Object> cf) {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf);
factory.setConcurrency(3);
return factory;
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> paymentsFactory(
ConsumerFactory<String, Object> cf) {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf);
factory.setConcurrency(2);
return factory;
}
}
@Service
public class MultiTopicConsumer {
@KafkaListener(topics = "orders", groupId = "multi-service", containerFactory = "ordersFactory")
public void handleOrder(OrderEvent event) {
processOrder(event);
}
@KafkaListener(topics = "payments", groupId = "multi-service", containerFactory = "paymentsFactory")
public void handlePayment(PaymentEvent event) {
processPayment(event);
}
}
Como Funciona
@KafkaListener: Spring escanea esta anotacion y crea un message listener container por cada metodo. El container gestiona el ciclo de vida del consumer, polling y dispatch de mensajes al handler.- Container factory: El
ConcurrentKafkaListenerContainerFactorycrea instancias deConcurrentMessageListenerContainer.setConcurrency(n)crea N threads de consumer, cada uno asignado a particiones por Kafka. - Modos de acknowledgment:
MANUALrequiereack.acknowledge()explicito.MANUAL_IMMEDIATEcommitea sincronamente.BATCHcommitea despues de cada batch.RECORDcommitea despues de cada record. - Batch listener: Establecer
batch = "true"hace que el listener reciba unList<ConsumerRecord>en lugar de registros individuales. Esto reduce overhead por mensaje y habilita operaciones batch en base de datos. - Manejo de errores: Sin un error handler, las excepciones se propagan y el container se detiene. Usa un
DefaultErrorHandleroErrorHandlercustom para manejar fallos sin detener el container.
Variantes
Retry con Dead Letter Topic
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> retryFactory(
ConsumerFactory<String, Object> cf,
KafkaTemplate<String, Object> template) {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf);
// Reintentar 3 veces con backoff exponencial, luego enviar a DLT
DefaultErrorHandler errorHandler = new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(template),
new ExponentialBackOffWithMaxRetries(3)
);
factory.setCommonErrorHandler(errorHandler);
return factory;
}
Filtrado de Mensajes
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> filteringFactory(
ConsumerFactory<String, Object> cf) {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf);
// Solo procesar ordenes con status "PENDING"
factory.setRecordFilterStrategy(record -> {
OrderEvent event = (OrderEvent) record.value();
return !"PENDING".equals(event.getStatus());
});
return factory;
}
}
Asignacion de Particiones de Topico
@KafkaListener(
topics = "orders",
groupId = "partitioned-service",
topicPartitions = @TopicPartition(
topic = "orders",
partitions = {"0", "1", "2"}
)
)
public void handlePartition(ConsumerRecord<String, OrderEvent> record) {
System.out.println("Partition " + record.partition() + " offset " + record.offset());
}
Mejores Practicas
- Deshabilitar auto-commit: Establece
enable-auto-commit: falsey usa acknowledgment manual. Esto previene commits de offset antes de que el procesamiento complete. - Establecer
concurrencysegun particiones: Si el topico tiene 6 particiones, establececoncurrency=6para maximo paralelismo. Threads extra estan inactivos. - Usar batch listeners para alto throughput: El procesamiento en batch reduce overhead por mensaje y habilita operaciones bulk en base de datos. Establece
MAX_POLL_RECORDS_CONFIGpara controlar el tamano del batch. - Siempre manejar errores: Excepciones no manejadas detienen el container. Usa
DefaultErrorHandlerconDeadLetterPublishingRecovererpara retry automatico y DLQ. - Usar
JsonDeserializercon trusted packages: Solo deserializa clases de paquetes confiables para prevenir vulnerabilidades de deserializacion. - Monitorear consumer lag: Usa Spring Boot Actuator con indicadores de salud de Kafka, o Micrometer para rastrear metricas de lag.
Errores Comunes
- No establecer
ack-mode: El modo defaultBATCHcommitea despues de cada batch. Si necesitas garantias por record, estableceack-mode: manualy acknowledge explicitamente. - Bloquear en el listener: Operaciones de larga duracion bloquean el consumer, previniendo polls y disparando rebalance. Descarga trabajo pesado a un thread pool.
- No configurar
MAX_POLL_RECORDS_CONFIG: El default de 500 records por poll puede ser demasiado para procesamiento lento. Reduce para evitar timeout demax.poll.interval.ms. - Usar auto-commit con procesamiento lento: Auto-commit corre cada 5 segundos. Si el procesamiento toma mas, los offsets se commitean antes de que el procesamiento complete — perdida de datos en crash.
- No manejar errores de deserializacion: Si un mensaje no se puede deserializar, el consumer se atasca. Usa
ErrorHandlingDeserializerpara manejar fallos de deserializacion graceful.
Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Construir Consumer Groups de Kafka con Python para
Crear consumer groups de Kafka en Python con asignacion de particiones, gestion de offsets, estrategias de commit, manejo de rebalance y semantica exactly-once para stream processing escalable.
RecipeConstruir un Consumer de RabbitMQ con Python y Pika
Crear un consumer y producer de RabbitMQ en Python usando pika con colas durables, dispatching de trabajo, acknowledgments, dead-letter exchanges y ajuste de prefetch.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
GuideKafka Stream Processing
Construye pipelines de event streaming en tiempo real con Kafka. Cubre producers, consumers, Kafka Streams, Kafka Connect, schema registry y patrones de procesamiento.
GuideSistemas Event-Driven
Disenar y operar backends event-driven. Cubre event sourcing, CQRS, sagas, outbox pattern, idempotency, eventual consistency y patrones de produccion para arquitecturas event-driven confiables.