Home/Learn/Apache Kafka/Transactional Producer

Transactional Producer

Advanced
Producers

Transactions 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.

Kafka — transactional producer EOS pipeline
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.

Kafka — read_committed consumer isolation
# 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 offset

Spring 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`.

Spring Kafka — KafkaTransactionManager EOS
@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 Aria
1

What does transactional.id do and why must it be stable across producer restarts?

HardConfluent
2

What is the difference between idempotent and transactional producer in Kafka?

MediumLinkedIn
3

Why must consumers set isolation.level=read_committed in an EOS pipeline?

MediumAmazon
4

What is ProducerFencedException and when does it occur?

HardUber
5

How does sendOffsetsToTransaction() enable exactly-once consume-transform-produce?

HardThoughtWorks

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.

Loading discussion…