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() e record.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 a enable-auto-commit=false combinado com AckMode.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.

kafkaspring-bootmessagingjava