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.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
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.
FAQ
Como funciona concurrency en Spring Kafka?
concurrency establece el numero de threads de consumer. Cada thread crea un KafkaConsumer separado. Kafka asigna particiones a consumers — con 6 particiones y concurrency=3, cada thread obtiene 2 particiones.
Cual es la diferencia entre ack mode BATCH y MANUAL?
BATCH commitea offsets despues de que cada batch de records se procesa. MANUAL requiere ack.acknowledge() explicito — dando control total sobre cuando se commitean los offsets. Usa MANUAL para garantias por record.
Como reintento mensajes fallidos?
Usa DefaultErrorHandler con ExponentialBackOffWithMaxRetries. Despues del maximo de reintentos, el DeadLetterPublishingRecoverer envia el mensaje a un topico dead-letter. Configura esto en el container factory.
Puedo consumir de multiples topicos con un listener?
Si. @KafkaListener(topics = {"orders", "payments"}) se suscribe a multiples topicos. Todos los mensajes van al mismo handler. Usa routing basado en headers para diferenciar tipos de mensaje.
Como manejo errores de deserializacion?
Usa ErrorHandlingDeserializer como value deserializer. Captura excepciones de deserializacion y retorna null o un header DeserializationException. Configura un DefaultErrorHandler para saltar o enviar a DLQ estos mensajes.
¿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
Kafka Consumer Groups with Python for Scalable Streaming
Create Kafka consumer groups in Python with partition assignment, offset management, commit strategies, rebalance handling, and exactly-once semantics for scalable stream processing.
RecipeBuild a RabbitMQ Consumer with Python and Pika
Create a RabbitMQ consumer and producer in Python using pika with durable queues, work dispatching, acknowledgments, dead-letter exchanges, and prefetch tuning.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
GuideComplete Guide to Kafka Stream Processing
Build real-time event streaming pipelines with Kafka. Covers producers, consumers, Kafka Streams, Kafka Connect, schema registry, and stream processing patterns.
GuideComplete Guide to Event-Driven Systems
Design and operate event-driven backends. Covers event sourcing, CQRS, sagas, outbox pattern, idempotency, eventual consistency, and production patterns for reliable event-driven architectures.