Transactional Producer
AdvancedTransactions span multiple partitions atomically; beginTransaction/commitTransaction ensure consume-transform-produce pipelines are atomic without duplicates or losses.
Overview
The Kafka transactional producer provides **exactly-once semantics (EOS)** for multi-partition writes. Built on top of the idempotent producer, transactions add the ability to atomically write to multiple partitions AND commit consumer offsets in the same atomic operation. This enables **Exactly-Once Stream Processing (EOSP)**: a consume-transform-produce pipeline where a message is read, transformed, and written to an output topic exactly once — with no duplicates even on retries or crashes. Transactions require a `transactional.id` (unique, stable per producer instance), and consumers must set `isolation.level=read_committed` to only read committed records.
Producer-Side: Begin, Write, Commit
`initTransactions()` registers the transactional.id with the broker and fences any previous producer with the same ID (preventing zombie producers). `beginTransaction()` / `commitTransaction()` / `abortTransaction()` control the transaction scope. If the producer crashes mid-transaction, the broker aborts it automatically.
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker:9092");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "order-processor-0"); // stable, unique per instance
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions(); // fence any previous zombie with same transactional.id
try {
producer.beginTransaction();
// Atomically write to multiple partitions
producer.send(new ProducerRecord<>("orders-processed", orderId, processedJson));
producer.send(new ProducerRecord<>("inventory-reserved", itemId, reserveJson));
// Atomically commit consumer offset as part of the same transaction
producer.sendOffsetsToTransaction(
Map.of(new TopicPartition("orders-raw", partition),
new OffsetAndMetadata(offset + 1)),
consumerGroupMetadata
);
producer.commitTransaction();
} catch (ProducerFencedException e) {
producer.close(); // this instance has been fenced by a newer one
} catch (KafkaException e) {
producer.abortTransaction(); // roll back — consumer offset not advanced
// Retry from the unconsumed offset
}Consumer-Side: isolation.level=read_committed
Without `isolation.level=read_committed`, consumers see **uncommitted records** from in-flight transactions — they read records that may later be aborted. Setting `read_committed` makes consumers only see records from committed transactions (and non-transactional records). This is essential for downstream consumers in an EOS pipeline.
# Consumer config for EOS
spring.kafka.consumer.properties.isolation.level=read_committed
# (default is read_uncommitted — consumers see all records including aborted ones)
# What read_committed means:
# - Records written in a committed transaction: VISIBLE
# - Records written in an aborted transaction: HIDDEN (skipped)
# - Records written without a transaction: VISIBLE immediately
# - Records in an in-flight (uncommitted) transaction: HELD — consumer waits
# The consumer's last stable offset (LSO) limits what read_committed can see:
# LSO = offset of the earliest open (uncommitted) transaction
# If a transaction is never committed/aborted, LSO stops advancing → consumer lag!
# Monitor: kafka_log_LogStartOffset vs consumer group offsetSpring Kafka: @Transactional with KafkaTemplate
Spring Kafka wraps Kafka transactions in a `KafkaTransactionManager`. Annotating a service method with `@Transactional(transactionManager = "kafkaTransactionManager")` makes it transactional. For read-process-write (consume + produce), use `executeInTransaction()` or configure the container's `EOSMode`.
@Configuration
class KafkaTransactionConfig {
@Bean
ProducerFactory<String, String> producerFactory() {
var props = Map.of(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker:9092",
ProducerConfig.TRANSACTIONAL_ID_CONFIG, "spring-tx-", // Spring appends unique suffix
ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"
);
return new DefaultKafkaProducerFactory<>(props);
}
@Bean
KafkaTransactionManager<String, String> kafkaTransactionManager(
ProducerFactory<String, String> pf) {
return new KafkaTransactionManager<>(pf);
}
}
@Service
class OrderProcessor {
@Transactional(transactionManager = "kafkaTransactionManager")
public void process(ConsumerRecord<String, Order> record) {
Order processed = enrich(record.value());
// Both sends are in the same Kafka transaction
kafkaTemplate.send("orders-enriched", record.key(), processed);
kafkaTemplate.send("audit-log", record.key(), toAuditEvent(processed));
// Spring commits offsets + transaction atomically on method return
}
}Key Points to Remember
- 1Transactional producer requires transactional.id (unique, stable per instance) + initTransactions()
- 2Transactions atomically write to multiple partitions + commit consumer offsets
- 3ProducerFencedException means another producer with the same transactional.id has taken over
- 4Consumers must set isolation.level=read_committed to skip aborted transaction records
- 5LSO (last stable offset) stops advancing if a transaction is left open — monitor for consumer lag
- 6Spring Kafka: use KafkaTransactionManager + @Transactional for declarative EOS
Interview Questions
Sign in to ask AriaWhat does transactional.id do and why must it be stable across producer restarts?
What is the difference between idempotent and transactional producer in Kafka?
Why must consumers set isolation.level=read_committed in an EOS pipeline?
What is ProducerFencedException and when does it occur?
How does sendOffsetsToTransaction() enable exactly-once consume-transform-produce?
Ask Aria about Transactional Producer
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.