Kafka Java Consumer

A Java consumer pulls messages from Kafka topics and hands them to your business logic. The consumer asks the broker for new data in a repeating loop, so the program controls its own reading speed. Think of a librarian who visits a shelf at regular intervals, collects every new book, and records the last book number in a notebook so the next visit starts at the right place.

The Consumer Loop

  subscribe to topic
        |
        v
  +-----------------+
  |  poll(timeout)  | <---------+
  +-----------------+           |
        |                       |
        v                       |
  process each record           |
        |                       |
        v                       |
  commit offsets  --------------+

The loop runs until the application shuts down. Every pass fetches a batch of records, processes them, and saves the position.

A Complete Consumer

import org.apache.kafka.clients.consumer.*;
import java.time.Duration;
import java.util.*;

public class OrderConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("group.id", "billing-app");
        props.put("key.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest");
        props.put("enable.auto.commit", "false");

        try (KafkaConsumer<String, String> consumer =
                 new KafkaConsumer<>(props)) {
            consumer.subscribe(List.of("orders"));
            while (true) {
                ConsumerRecords<String, String> records =
                    consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord<String, String> r : records) {
                    System.out.println(r.key() + " -> " + r.value()
                        + " (partition " + r.partition()
                        + ", offset " + r.offset() + ")");
                }
                consumer.commitSync();
            }
        }
    }
}

Key Settings Explained

SettingMeaning
group.idName of the consumer group that shares the work
auto.offset.resetWhere to start when no saved offset exists: earliest or latest
enable.auto.commitLets the client save offsets on a timer when true
max.poll.recordsUpper limit of records returned by one poll

Setting auto commit to false gives full control. The program commits only after processing succeeds, which prevents message loss during crashes.

Commit Choices

commitSync

Blocks until the broker confirms the commit. It retries on temporary errors and offers the strongest safety.

commitAsync

Returns immediately and reports the result through a callback. It gives higher throughput, but a failed commit does not retry automatically.

A common pattern uses commitAsync inside the loop and one final commitSync during shutdown.

Shut Down Cleanly

Call wakeup from a shutdown hook to break a blocked poll call. The loop catches the WakeupException, commits offsets, and closes the consumer so the group rebalances right away.

Runtime.getRuntime().addShutdownHook(new Thread(consumer::wakeup));

try {
    while (true) { /* poll and process */ }
} catch (WakeupException e) {
    // expected during shutdown
} finally {
    consumer.commitSync();
    consumer.close();
}

Thread Safety

A KafkaConsumer is not thread-safe. Keep one consumer per thread. Scale reading by starting more consumer instances in the same group, up to the number of partitions.

Assign Partitions by Hand

The assign method skips group management and attaches the consumer to specific partitions. Use it for tools that must read exact partitions, such as replay utilities. Regular applications prefer subscribe.

Seek to a Position

consumer.assign(List.of(new TopicPartition("orders", 0)));
consumer.seek(new TopicPartition("orders", 0), 150L);

Seeking lets a program replay old data from a chosen offset, which helps with bug fixes and audits.

Avoid Slow Processing

Long work between poll calls can exceed max.poll.interval.ms and push the consumer out of the group. Reduce max.poll.records or move heavy tasks to a worker pool so the poll loop stays fast.