cswork / libraryOpen practice workspace

Articles

Kafka Transactions Interview Questions: Producer Fencing and Exactly-Once Boundaries

Prepare for Kafka transactions interview questions on producer fencing, atomic offset commits, read_committed visibility, and safe error recovery boundaries.

A worker reads a Kafka record, charges a card through an HTTP API, and publishes a receipt inside a Kafka transaction. The process crashes before committing. On restart, it reads the input again and charges the card again. Kafka's transaction can be correct while the business outcome is wrong.

Kafka transactions interview questions test whether you can name the atomic boundary, fence stale producers, commit output with consumption progress, and recover without inventing certainty after a timeout. Exactly-once processing does not mean application code can never run twice. It describes the observable result within a defined protocol and storage boundary.

Evidence boundary: Official facts below use current Apache Kafka 4.3 documentation and Java client API references. Examples assume the standard consume-transform-produce transaction flow, not a separately integrated external two-phase-commit system. Crash schedules and recommendations are original exercises; no candidate reports or employer-frequency claims are used. The examples were documentation-reviewed, not executed against a Kafka cluster.

How are idempotent production and transactions different?

Official fact: An idempotent producer protects against duplicates caused by its supported producer retry protocol. Configuring transactional.id enables transactional delivery across producer sessions and implies idempotence. Compatible acknowledgment, retry, and in-flight-request settings remain relevant. Producer configuration.

Design inference: Distinguish a client retry of the same send from application code deliberately issuing another send for the same business event. A message key is not a universal deduplication key. Two application sends with the same key can represent two ordinary records.

A transaction adds a commit-or-abort boundary around a group of Kafka writes. In a consume-transform-produce application, the boundary can also include consumed offsets. This addresses a different failure: publishing output successfully while leaving input progress uncommitted, so a replacement worker publishes that output again.

Before choosing configuration, ask what must agree after recovery. Is it one partition's producer retry sequence, several output partitions, input progress plus output, or a database row plus a message? These are different requirements even when the interviewer uses “exactly once” for all of them.

For the rest of this article, assume one worker transforms each input record into Kafka output, with no irreversible external side effect inside the transformation. That keeps the first correctness argument precise before we extend it.

What should transactional.id identify, and why does fencing matter?

Official fact: ProducerFencedException indicates that another producer with the same transactional ID has superseded the current producer. The fenced producer cannot continue transactional work and must be closed. ProducerFencedException.

Original ownership timeline: Worker A owns logical shard 7 and uses billing-transform-shard-7. A pauses long enough for orchestration to replace it. Worker B initializes transactional work using that same logical ID. When A resumes, it must not continue committing as though it still owned the shard.

Fencing makes the stale instance distinguishable from the current owner. Giving B an unrelated random ID would create an independent producer identity rather than deliberately fencing A through the same ID. Conversely, assigning one shared ID to every live shard makes legitimate independent workers supersede one another.

Identity choiceIntended effectFailure to discuss
Stable ID for a logical worker across replacementReplacement can fence its predecessorOwnership assignment must itself be controlled
Distinct IDs for independent live workersWorkers can transact concurrentlyDo not accidentally collide after scaling
New random ID on every retryNew producer identityDoes not replace business-event deduplication
One ID shared by unrelated workersCompeting ownershipRepeated fencing and failed progress

Official fact: With the strengthened transaction protocol introduced in Kafka 4.0, compatible clients and enabled transaction.version=2 advance producer epochs per transaction. Epoch changes are therefore not limited to process restarts. Transaction protocol.

Keep the interview explanation at the ownership level unless protocol details are requested. “The old instance is rejected after takeover” explains the safety objective. Claiming that an epoch only changes on restart would make the explanation version-dependent and inaccurate for the newer protocol.

How do output records and consumed offsets commit together?

Official fact: sendOffsetsToTransaction includes consumer-group offsets in the current transaction. They become committed with the transaction, not merely when that method returns. Supply the consumer's group metadata and disable separate automatic or manual offset commits. KafkaProducer API.

Original single-record example: The group's next input offset is 40. The worker processes record 40, produces one output, and includes next offset 41 in the same transaction. A successful commit publishes the output under transactional visibility and advances group progress together. An abort does neither.

This Java-style sequence assumes the entire polled batch is processed successfully:

producer.initTransactions(); // once during producer initialization

var batch = consumer.poll(pollTimeout);
producer.beginTransaction();
for (var record : batch) {
    producer.send(transform(record));
}
producer.sendOffsetsToTransaction(
    batch.nextOffsets(), consumer.groupMetadata());
producer.commitTransaction();

This is a success-path sketch, not complete production code. Initialization belongs outside the repeated processing loop. Track send failures and apply the method-specific recovery rules below. If only part of a batch is processed, do not commit the entire batch's next offsets.

Commit the next position to consume. After processing record 40 in this fixture, that position is 41. In a larger batch, track progress per partition and never advance past unfinished work. Parallel transformations can finish out of order, so the highest completed offset is not necessarily a safe commit frontier.

Also separate committed group progress from the consumer object's in-memory position. After an aborted transaction, a still-running consumer may already have moved past the polled records. Recovery must rewind to the intended retry positions for partitions it still owns, or recreate/rejoin and resume from committed progress. Calling abortTransaction alone is not a replay loop.

What does read_committed hide, and what can it delay?

Official fact: A consumer configured with isolation.level=read_committed filters aborted transactional records and withholds records beyond the relevant open-transaction boundary, governed by the last stable offset. Nontransactional records are still readable. The default isolation setting is read_uncommitted. Consumer configuration.

Original visibility exercise: A partition contains ordinary records, then records from a transaction that remains open, followed by later writes. A read_committed reader can stop making visible progress at that boundary even though producers continue appending data. Closing the open transaction can change what becomes available without any new application record arriving.

This gives you a practical diagnostic distinction: log growth does not necessarily imply equivalent committed-consumer progress. Inspect open transactions and transactional visibility before concluding that the consumer is failing to fetch.

Do not confuse atomic transaction outcome with one atomic application read across partitions. A consumer can receive a committed transaction's records over multiple polls and partitions. The application still needs its own grouping if it wants to present an entire business batch at once.

Long transactions also increase the time downstream readers may wait. Choose batch size and transaction duration together. A very large batch may reduce commit overhead while increasing replay work, memory use, and visibility delay after a failure.

Which failures require aborting, closing, or retrying the commit?

Official fact: In the Java API, a commitTransaction() timeout does not prove the commit failed. Retrying that commit is supported; switching to abort is not supported while the commit may be completing. Closing the producer is the alternative if the application will not retry the commit. Commit timeout semantics.

Original crash-and-recovery matrix: Assume the output and input offsets belong to one Kafka transaction.

Failure pointWhat can be concluded?Recovery direction
Before beginning the transactionNo output from this attempt was transactionally committedProcess from known input progress
During work, before a known successful abortOutcome is not yet resolvedComplete supported abort handling before replay
After a confirmed abortThis transaction's output and offsets did not commitRewind or rejoin before processing again
Commit call times outCommit may already be completingRetry the commit operation or close and recover ownership
Producer is fencedThis instance has lost transactional authorityClose it; let the current owner proceed
Commit succeeds, process then crashesKafka output and offsets committed togetherReplacement resumes from committed progress

An abortable application failure and an unresolved commit are different states. A catch-all handler that always aborts and resends the batch erases that distinction. Classify the exception and the transaction phase using the client version's API contract.

Rebalancing adds another ownership check. A worker that loses a partition must not commit progress for work it no longer owns. Use current group metadata and respond to ownership changes; do not turn a stale worker's failed commit into an independent manual offset commit.

For observability, record the logical transactional ID, input partition ranges, transaction phase, and whether the outcome was confirmed. “Retry count” alone cannot reveal whether the application retried a send, an entire transformation, or an unresolved commit.

Where does exactly-once stop when an external system is involved?

Official fact: Kafka's design documentation distinguishes Kafka-to-Kafka processing from output written to other storage systems, where cooperation with the destination is necessary. Kafka delivery semantics.

Original counterexample: A worker updates a database balance, then crashes before the Kafka transaction commits. The balance update survives in the database, while the input offset remains uncommitted. Reprocessing the record can apply the balance update twice. Reversing the order introduces a different gap: Kafka progress may advance before the external update happens.

Possible designs include a destination transaction that atomically stores the business mutation and a consumed-event identity, an idempotent external API keyed by the logical operation, or an explicitly integrated coordination protocol. Each design needs a crash analysis and a retention policy for its deduplication state.

A transactional outbox addresses the related database-to-message publication direction. It should not be described as a magical extension of any surrounding Kafka transaction.

Kafka Streams can manage much of the Kafka processing coordination. Official fact: its processing.guarantee configuration includes exactly_once_v2. Streams configuration. Enabling that setting still does not automatically enroll an arbitrary HTTP charge or database write into the managed transaction.

Practice explaining the guarantee with a crash point

Practice questionTransaction follow-up
Explain Kafka architecture and reliability semanticsSeparate producer retries, transaction outcome, and consumer visibility.
Explain Kafka Reliability and Offset RecoveryTrace offset40, next41, abort, and a replacement worker.
Explain Kafka and Message-Queue Delivery SemanticsName which destination participates in the guarantee.
Design a Payment Processing System with Exactly-Once ChargingAdd an HTTP charge and locate the uncoordinated side effect.
Explain Kafka and Design Bulk Record ProcessingDefine a safe per-partition commit frontier for parallel work.

A strong answer names the transactional resources, the current producer owner, the committed input position, and the downstream isolation setting. Then it introduces one crash point and predicts both stored output and resumed input. Add an external side effect only after that Kafka-only argument is complete.

Continue with the transactional-outbox guide to practice the database-to-message gap that Kafka transactions alone do not close.

Sources and Further Reading