Kafka Básico: Topic, Partition e Offset
1. Visão Geral
Kafka é um sistema de message broker distribuído, usado para transmitir dados entre serviços com alto throughput. Antes de escrever seu primeiro producer/consumer, você precisa dominar três conceitos fundamentais: topic, partition, offset. Este artigo explica cada conceito, com exemplos específicos de Spring Boot. Após ler, você entenderá como os dados no Kafka são organizados e lidos, evitando erros comuns ao começar a usar.
2. O que é Topic
Topic é como uma “tabela” ou “canal” que contém mensagens. Producer envia mensagens para um topic, consumer lê mensagens de um topic. Um topic pode ter vários producers e vários consumers ao mesmo tempo.
Por exemplo, em um sistema de e-commerce, você pode ter os seguintes topics:
orders.created
orders.cancelled
payments.processed
Cada topic contém mensagens relacionadas a um tipo de evento. Kafka não impõe um schema para as mensagens — você decide o formato (JSON, Avro, Protobuf…).
Crie um topic usando CLI para visualizar melhor:
kafka-topics.sh --create \
--topic orders.created \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
Este comando cria um topic orders.created com 3 partitions. Este é exatamente o próximo conceito.
3. Partition — Unidade de Paralelização
3.1. Por que Precisamos de Partitions
Um topic é dividido em várias partitions. Cada partition é um arquivo de log independente; mensagens são adicionadas ao final, nunca são editadas ou deletadas no meio. Partitions permitem que Kafka:
- Distribua dados em vários brokers.
- Permita que vários consumers leiam em paralelo (cada consumer uma partition).
Topic: orders.created (3 partitions)
Partition 0: [msg0][msg1][msg4][msg7]...
Partition 1: [msg2][msg5][msg8]...
Partition 2: [msg3][msg6][msg9]...
3.2. A Chave Determina Qual Partition
Ao enviar uma mensagem, Kafka usa a chave para calcular a partition (através de hash), garantindo que mensagens com a mesma chave sempre vão para a mesma partition — mantendo a ordem correta para mensagens relacionadas.
// Suponha que estamos enviando um evento com orderId como chave
ProducerRecord<String, String> record =
new ProducerRecord<>("orders.created", "order-123", payloadJson);
kafkaTemplate.send(record);
Esta linha cria um ProducerRecord com a chave "order-123". Todos os eventos deste pedido sempre cairão na mesma partition, para que o consumer possa lê-las na ordem exata em que foram enviadas.
Observação: Se você não passar uma chave (key =
null), Kafka distribui mensagens em round-robin (ou sticky partitioner desde Kafka 2.4+), não garante a ordem entre as mensagens.
3.3. A Ordem é Garantida Apenas Dentro de uma Partition
Este é o ponto mais confuso: Kafka garante a ordem apenas dentro de uma partition, não garante a ordem em todo o topic. Se você precisar de uma ordem absoluta para um grupo de mensagens (por exemplo, todos os eventos de um orderId), use a mesma chave para esse grupo.
4. Offset — Posição de Leitura em uma Partition
Cada mensagem em uma partition tem um número sequencial crescente chamado offset, começando do 0.
Partition 0:
offset: 0 1 2 3 4
message: m0 m1 m2 m3 m4
Offset é um número único no escopo de uma partition, não em todo o topic. Consumer usa offset para saber até onde já leu.
4.1. Consumer Group e Offset Commit
Kafka rastreia o offset que foi lido através de um consumer group. Cada consumer group tem seu próprio offset para cada partition, armazenado no 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 o consumer group. Múltiplas instâncias com o mesmo group-id compartilharão as partitions do topic.auto-offset-reset=earliest: se não houver offset salvo (novo consumer group), leia desde o início do topic.enable-auto-commit=false: desabilita auto-commit para você controlar quando fazer commit do offset (evita perder mensagens em caso de erro durante o processamento).
4.2. Múltiplos Consumers no Mesmo Group
Kafka distribui partitions para consumers no mesmo group de acordo com o princípio: uma partition é lida por apenas um consumer no group em um determinado momento.
Topic orders.created tem 3 partitions
Consumer group "order-service" tem 2 instâncias:
Consumer A -> partition 0, partition 1
Consumer B -> partition 2
Se você executar 4 consumers para um topic com 3 partitions, o 4º consumer ficará ocioso, não receberá nenhuma partition. Portanto, o número máximo útil de consumers = número de partitions.
5. Configurando Kafka no Spring Boot
5.1. Dependência
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
5.2. Producer Enviando Mensagens
@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) envia a mensagem para o topic orders.created, usando orderId como chave. Spring Boot cria automaticamente o bean KafkaTemplate baseado na configuração spring.kafka.bootstrap-servers.
5.3. Consumer Lendo Mensagens e Fazendo Commit Manual de 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());
// Processe a lógica de negócio aqui
ack.acknowledge(); // faz commit do offset após processar com sucesso
}
}
record.partition()erecord.offset()informam em qual partition está a mensagem e sua posição.ack.acknowledge()faz commit do offset após o processamento — se o serviço falhar no meio do caminho, a mensagem será relida na próxima reinicialização (graças aenable-auto-commit=falsecombinado comAckMode.MANUAL).
Você precisa adicionar uma configuração para que Acknowledgment funcione:
spring.kafka.listener.ack-mode=manual
Saída ao executar (enviando 3 mensagens com chaves 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 chave vai para uma partition diferente devido a hashes diferentes — exatamente como a teoria na seção 3.2.
6. Testando com JUnit 5
Use spring-kafka-test para executar um Kafka embedded nos testes unitários, sem precisar de um 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 um broker Kafka na memória apenas para testes. KafkaTestUtils.getSingleRecord obtém a primeira mensagem do topic, e afirmamos que a chave, valor e offset estão corretos conforme esperado — offset 0 porque esta é a primeira mensagem da única partition.
7. Erros Comuns
- Confundir número de partitions com número de consumers: criar muitos consumers em relação ao número de partitions deixa alguns consumers ociosos. O número de partitions determina o grau máximo de paralelismo.
- Não definir chave quando a ordem é importante: se você precisar que os eventos de uma mesma entidade (pedido, usuário…) sejam processados na ordem correta, deve usar chave, não deixar
null. - Aumentar partitions após estar em produção: aumentar o número de partitions de um topic com dados existentes mudará como a chave é hash → mensagens com a mesma chave podem cair em partitions diferentes, quebrando a garantia de ordem para dados antigos. Você deve determinar um número razoável de partitions desde o início.
- Auto-commit padrão (
enable-auto-commit=true): offset é feito commit em ciclos fixos (padrão 5s), não está vinculado ao fato de a mensagem ter sido processada com sucesso ou não — fácil perder mensagens se o consumer falhar durante o processamento.
Observação: Se seu aplicativo não precisa de uma ordem rigorosa e o throughput não é um problema, você pode pular a otimização de chave/partition — Kafka ainda funcionará corretamente com a configuração padrão. Não otimize antecipadamente se não houver uma necessidade real.
8. Conclusão
Topic é o lugar onde as mensagens são armazenadas por assunto, partition é a unidade de divisão para paralelização e distribuição, offset é a posição de leitura em cada partition. Entender estes três conceitos claramente ajuda você a projetar corretamente: escolher chaves adequadas para manter a ordem, escolher um número apropriado de partitions para o número de consumers, e controlar quando fazer commit de offset para evitar perder dados em caso de erro. Esta é a base obrigatória antes de aprofundar em tópicos mais avançados como rebalancing, exactly-once semantics ou Kafka Streams.