Kafka Básico: Topic, Partition y Offset
1. Descripción general
Kafka es un sistema de message broker distribuido, utilizado para transmitir datos entre servicios con alto throughput. Antes de escribir tu primer producer/consumer, necesitas dominar tres conceptos clave: topic, partition, offset. Este artículo explica cada concepto, junto con ejemplos específicos de Spring Boot. Cuando termines de leer, entenderás cómo se organizan y se leen los datos en Kafka, evitando errores comunes cuando empiezas a usarlo.
2. ¿Qué es un topic?
Un topic es como una “tabla” o “canal” que contiene mensajes. El producer envía mensajes al topic, el consumer lee mensajes del topic. Un topic puede tener múltiples producers y múltiples consumers al mismo tiempo.
Por ejemplo, en un sistema de e-commerce, puedes tener los siguientes topics:
orders.created
orders.cancelled
payments.processed
Cada topic contiene mensajes relacionados con un tipo de evento. Kafka no impone un schema en los mensajes — tú decides el formato (JSON, Avro, Protobuf…).
Crea un topic mediante CLI para visualizar mejor:
kafka-topics.sh --create \
--topic orders.created \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
Este comando crea el topic orders.created con 3 partitions. Este es precisamente el concepto siguiente.
3. Partition — unidad de paralelismo
3.1. ¿Por qué necesitamos partitions?
Un topic se divide en múltiples partitions. Cada partition es un archivo de log independiente, los mensajes se añaden al final, nunca se modifican ni se eliminan en el medio. Las partitions permiten que Kafka:
- Distribuya datos en múltiples brokers.
- Permita que múltiples consumers lean en paralelo (cada consumer una partition).
Topic: orders.created (3 partitions)
Partition 0: [msg0][msg1][msg4][msg7]...
Partition 1: [msg2][msg5][msg8]...
Partition 2: [msg3][msg6][msg9]...
3.2. La key determina a qué partition va
Cuando envías un mensaje, Kafka usa la key para calcular la partition (mediante hash), garantizando que los mensajes con la misma key siempre vayan a la misma partition — manteniendo el orden correcto para los mensajes relacionados.
// Supongamos que enviamos un evento usando orderId como key
ProducerRecord<String, String> record =
new ProducerRecord<>("orders.created", "order-123", payloadJson);
kafkaTemplate.send(record);
Esta línea crea un ProducerRecord con key "order-123". Todos los eventos de esta order siempre caerán en la misma partition, así que el consumer puede leerlos en el orden correcto en que se enviaron.
Nota: Si no envías una key (key =
null), Kafka distribuye los mensajes en round-robin (o sticky partitioner desde Kafka 2.4+), sin garantizar el orden entre mensajes.
3.3. El orden solo se garantiza dentro de una partition
Este es el punto más confuso: Kafka solo garantiza el orden dentro de una partition, no garantiza el orden en todo el topic. Si necesitas un orden absoluto para un grupo de mensajes (por ejemplo, todos los eventos de un orderId), usa la misma key para ese grupo.
4. Offset — la posición de lectura en una partition
Cada mensaje en una partition tiene un número secuencial creciente llamado offset, que comienza desde 0.
Partition 0:
offset: 0 1 2 3 4
message: m0 m1 m2 m3 m4
El offset es un número único dentro del rango de una partition, no en todo el topic. El consumer usa el offset para saber “hasta dónde ha leído”.
4.1. Consumer group y commit del offset
Kafka rastrea los offsets leídos a través de consumer groups. Cada consumer group tiene offsets propios para cada partition, almacenados en el topic interno __consumer_offsets.
# application.properties
spring.kafka.consumer.group-id=order-service
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=false
group-id: identifica el consumer group. Múltiples instancias con el mismo group-id se repartirán las partitions del topic.auto-offset-reset=earliest: si no hay offsets guardados previamente (consumer group nuevo), lee desde el inicio del topic.enable-auto-commit=false: desactiva el auto-commit para controlar manualmente cuándo hacer commit del offset (evita perder mensajes cuando hay errores en el procesamiento intermedio).
4.2. Múltiples consumers en el mismo group
Kafka asigna las partitions a los consumers en el mismo group siguiendo la regla: una partition solo puede ser leída por un consumer en el group en un momento dado.
Topic orders.created tiene 3 partitions
Consumer group "order-service" tiene 2 instancias:
Consumer A -> partition 0, partition 1
Consumer B -> partition 2
Si ejecutas 4 consumers para un topic con 3 partitions, el cuarto consumer estará inactivo, sin recibir ninguna partition. Por lo tanto, el número máximo útil de consumers = número de partitions.
5. Configuración de Kafka en Spring Boot
5.1. Dependencia
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
5.2. Producer enviando mensajes
@Service
public class OrderEventProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderCreated(String orderId, String payloadJson) {
kafkaTemplate.send("orders.created", orderId, payloadJson);
}
}
kafkaTemplate.send(topic, key, value) envía un mensaje al topic orders.created, usando orderId como key. Spring Boot crea automáticamente el bean KafkaTemplate basado en la configuración spring.kafka.bootstrap-servers.
5.3. Consumer leyendo mensajes y haciendo commit manual del offset
@Component
public class OrderEventConsumer {
@KafkaListener(topics = "orders.created", groupId = "order-service")
public void onOrderCreated(
ConsumerRecord<String, String> record,
Acknowledgment ack) {
System.out.printf("partition=%d offset=%d key=%s value=%s%n",
record.partition(), record.offset(), record.key(), record.value());
// Procesa la lógica de negocio aquí
ack.acknowledge(); // Commit del offset después de procesarlo correctamente
}
}
record.partition()yrecord.offset()indican en qué partition se encuentra este mensaje y cuál es su posición.ack.acknowledge()solo hace commit del offset después de procesarlo correctamente — si el servicio se cae en el medio, el mensaje se releerá en la siguiente ejecución (gracias aenable-auto-commit=falsecombinado conAckMode.MANUAL).
Necesitas añadir una configuración para que Acknowledgment funcione:
spring.kafka.listener.ack-mode=manual
Resultado cuando se ejecuta (enviando 3 mensajes con keys diferentes):
partition=1 offset=0 key=order-123 value={"status":"CREATED"}
partition=0 offset=0 key=order-456 value={"status":"CREATED"}
partition=2 offset=0 key=order-789 value={"status":"CREATED"}
Cada key cae en una partition diferente debido a hashes diferentes — exacto como la teoría de la sección 3.2.
6. Pruebas con JUnit 5
Usa spring-kafka-test para ejecutar un Kafka embebido en las pruebas unitarias, sin necesidad de un broker real.
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = "orders.created")
class OrderEventProducerTest {
@Autowired
private OrderEventProducer producer;
@Autowired
private EmbeddedKafkaBroker embeddedKafka;
@Test
void shouldPublishMessageWithGivenKey() throws Exception {
Consumer<String, String> consumer = createTestConsumer();
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "orders.created");
producer.publishOrderCreated("order-123", "{\"status\":\"CREATED\"}");
ConsumerRecord<String, String> record =
KafkaTestUtils.getSingleRecord(consumer, "orders.created");
assertThat(record.key()).isEqualTo("order-123");
assertThat(record.value()).contains("CREATED");
assertThat(record.offset()).isEqualTo(0L);
}
private Consumer<String, String> createTestConsumer() {
Map<String, Object> props = KafkaTestUtils.consumerProps(
"test-group", "true", embeddedKafka);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
DefaultKafkaConsumerFactory<String, String> factory =
new DefaultKafkaConsumerFactory<>(props);
return factory.createConsumer();
}
}
@EmbeddedKafka inicia un broker de Kafka en memoria solo para pruebas. KafkaTestUtils.getSingleRecord obtiene el primer mensaje del topic, y aseveramos que la key, el value y el offset son los esperados — offset 0 porque es el primer mensaje de la única partition.
7. Errores comunes
- Confundir el número de partitions con el número de consumers: crear demasiados consumers en comparación con las partitions hace que algunos consumers no hagan nada. El número de partitions determina el máximo nivel de paralelismo.
- No configurar la key cuando necesitas mantener el orden: si necesitas que los eventos de la misma entidad (order, user…) se procesen en el orden correcto, debes usar una key, no dejar
null. - Aumentar partitions después de estar en producción: aumentar el número de partitions de un topic que ya contiene datos cambiará la forma en que se hace hash en la key → mensajes con la misma key podrían caer en partitions diferentes, rompiendo la garantía de orden para datos antiguos. Debes determinar un número razonable de partitions desde el principio.
- Auto-commit por defecto (
enable-auto-commit=true): el offset se hace commit en ciclos fijos (por defecto 5s), no vinculado a si el mensaje se ha procesado correctamente o no — es fácil perder mensajes cuando el consumer falla durante el procesamiento.
Nota: Si tu aplicación no requiere un orden estricto y el throughput no es un problema, puedes saltarte la optimización de key/partition — Kafka sigue funcionando correctamente con la configuración por defecto. No optimices prematuramente si aún no tienes una necesidad real.
8. Conclusión
Un topic es donde se almacenan los mensajes por tema, una partition es la unidad de división para paralelismo y distribución, el offset es la posición de lectura dentro de cada partition. Comprender claramente estos tres conceptos te ayuda a diseñar correctamente: elige una key razonable para mantener el orden, elige un número de partitions acorde con el número de consumers, y controla el commit del offset para evitar perder datos cuando hay errores. Esta es la base obligatoria antes de profundizar en temas más avanzados como rebalancing, exactly-once semantics o Kafka Streams.