Kafka
Camel
Data Polling
Transactional Polling
Software Architecture

How to transactionally poll Kafka from Camel?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In Camel, "transactionally poll Kafka" usually means two practical things: consume only committed Kafka records and commit offsets only after your route has finished processing successfully. That is close to transactional behavior, but it is not the same as pretending Kafka consumption is one ordinary local transaction.

The reliable pattern is to disable auto-commit, enable manual commits, and use read_committed isolation when upstream producers use Kafka transactions. Then commit offsets only after the route reaches a safe completion point.

Read Only Committed Kafka Records

If upstream services produce records transactionally, the consumer should not read aborted records. Kafka solves that with consumer isolation level.

java
1from("kafka:orders"
2    + "?brokers=localhost:9092"
3    + "&groupId=order-service"
4    + "&isolationLevel=read_committed")
5    .to("log:orders");

read_committed tells the consumer to skip records from aborted transactions. That is an important part of correctness, but it does not say anything about when Camel advances the consumer offset.

Those are separate concerns:

  • visibility of committed records
  • timing of offset commits
  • error handling when route processing fails

Treating them separately makes the route design much easier to reason about.

Disable Auto-Commit and Commit Manually

If offsets should only advance after business logic succeeds, turn off background auto-commit and let the route decide when to acknowledge work.

java
1from("kafka:orders"
2    + "?brokers=localhost:9092"
3    + "&groupId=order-service"
4    + "&autoCommitEnable=false"
5    + "&allowManualCommit=true"
6    + "&isolationLevel=read_committed"
7    + "&breakOnFirstError=true")
8    .routeId("orders-consumer")
9    .bean(OrderService.class, "handle")
10    .process(new CommitOffsetProcessor());

This route does three useful things:

  • it prevents automatic offset advancement
  • it exposes a manual commit handle on the exchange
  • it stops on failure so the uncommitted record can be retried

That is the heart of a transaction-like polling strategy in Camel.

Commit the Offset After Successful Processing

Camel exposes the manual commit handle in a Kafka-specific header. A processor can retrieve it and commit only after the route has done the work that makes replay unnecessary.

java
1import org.apache.camel.Exchange;
2import org.apache.camel.Processor;
3import org.apache.camel.component.kafka.KafkaConstants;
4import org.apache.camel.component.kafka.consumer.KafkaManualCommit;
5
6public class CommitOffsetProcessor implements Processor {
7    @Override
8    public void process(Exchange exchange) {
9        KafkaManualCommit manual = exchange.getMessage()
10            .getHeader(KafkaConstants.MANUAL_COMMIT, KafkaManualCommit.class);
11
12        if (manual == null) {
13            throw new IllegalStateException("Manual commit is not available");
14        }
15
16        manual.commit();
17    }
18}

The ordering matters. If you commit first and the downstream work fails later, the message is already acknowledged and will not be replayed from Kafka.

Understand What This Does Not Guarantee

Manual offset commits improve reliability, but they do not automatically create end-to-end exactly-once semantics across every external system. If the route reads from Kafka and writes to a database, the database transaction and the Kafka offset commit are still different systems unless you add a broader coordination strategy.

That is why production solutions often combine manual commit with:

  • idempotent writes downstream
  • retry logic or dead-letter handling
  • Kafka transactions on the producer side
  • route error handling that prevents premature commits

Camel helps orchestrate the flow, but the overall guarantee depends on the full design, not one flag on the consumer endpoint.

Common Pitfalls

The biggest mistake is leaving autoCommitEnable=true and assuming a transacted Camel route will make the Kafka poll transactional. Auto-commit can move the offset forward before your business logic is actually safe.

Another common issue is forgetting isolationLevel=read_committed when the topic contains transactional producer output. In that case, the consumer may see records you did not want to process.

It is also easy to commit offsets too early. The commit belongs after the successful side effect, not before it.

Finally, do not move manual commits into arbitrary background threads without understanding the consumer threading model. Offset handling should stay aligned with how the Kafka consumer is being driven.

Summary

  • Use read_committed when upstream Kafka transactions matter.
  • Disable auto-commit if the route should control offset advancement.
  • Enable manual commit and commit only after successful processing.
  • Combine the route with idempotency and error handling for stronger guarantees.
  • Treat Kafka offset commits and other resource transactions as related but distinct concerns.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.