KafkaIQ

8.1 Transaction & Exactly-Once Internals (Broker Side)

Module Overview

Goal: Explain broker-side transaction and exactly-once semantics (EOS) internals.

What You'll Learn:

  • Transaction coordinator internals
  • Transaction log (__transaction_state)
  • Producer IDs and epochs
  • Transaction markers (commit/abort)
  • Exactly-once semantics (EOS) at broker level
  • Transactional reads and isolation levels

What's NOT Covered Here:

  • Producer API usage (covered in Producer modules)
  • Kafka Streams EOS (covered in Streams modules)
  • Consumer transaction handling (covered in Consumer modules)

Key Insight: Transactions in Kafka are broker-coordinated using a transaction coordinator, transaction log, and commit/abort markers. Understanding broker-side mechanics explains how Kafka achieves exactly-once semantics.


1. Why Transactions?

The Problem

WITHOUT TRANSACTIONS:

Scenario: Move money between accounts (read from account A, write to account B)

1. Producer reads from topic "accountA"
2. Producer processes data
3. Producer writes to topic "accountB"
4. Producer crashes before committing offset

Result:
├─ Data written to "accountB" (duplicate)
├─ Offset not committed
└─ Reprocessing creates duplicate write

Exactly-once goal:
└─ Write to accountB and commit offset atomically
└─ Either both succeed or both fail (no duplicates)

Kafka Transactions

WITH TRANSACTIONS:

1. Producer begins transaction
2. Producer writes to topic "accountB"
3. Producer sends offset commit to transaction (not consumer group)
4. Producer commits transaction (atomic)

Result:
├─ If commit succeeds: Write and offset both visible
├─ If commit fails: Write and offset both discarded
└─ Atomic "all-or-nothing" semantics

Guarantees:
└─ Exactly-once processing (no duplicates, no data loss)

2. Transaction Coordinator

Role and Responsibility

TRANSACTION COORDINATOR:

Purpose:
└─ Manage transaction state and lifecycle

One coordinator per transaction:
└─ Determined by hashing transactional.id

Responsibilities:
├─ Assign Producer IDs (PIDs)
├─ Track transaction state (ongoing, prepared, completed)
├─ Write transaction state to transaction log
├─ Send transaction markers (commit/abort) to partitions
└─ Handle coordinator failover

Coordinator is:
└─ A broker designated for a range of transactional IDs
└─ Not a separate process (part of broker)

Coordinator Selection

DETERMINE COORDINATOR:

transactional.id = "order-processor-1"

1. Hash transactional.id:
   └─ hash("order-processor-1") % num_transaction_partitions

2. Result: Partition X in __transaction_state topic

3. Leader of partition X = Transaction coordinator

4. Producer sends FindCoordinatorRequest:
   └─ "Who coordinates transactional.id='order-processor-1'?"

5. Broker responds:
   └─ "Coordinator is Broker 2"

6. Producer connects to Broker 2 for transaction operations

3. Producer ID (PID) and Epoch

PID Assignment

PRODUCER ID (PID):

Definition:
└─ Unique 64-bit ID assigned to producer by coordinator

Assignment:
1. Producer sends InitProducerIdRequest to coordinator
   └─ Includes transactional.id (if transactional)

2. Coordinator:
   ├─ Checks if transactional.id exists
   ├─ If new: Assign new PID
   ├─ If existing: Return existing PID (and increment epoch)
   └─ Write to transaction log

3. Coordinator responds:
   └─ PID: 12345
   └─ Epoch: 0

4. Producer uses PID in all subsequent produce requests

Purpose:
└─ Identify producer uniquely
└─ Enable idempotence and transactions

Producer Epoch

EPOCH FOR FENCING:

Epoch increments on:
├─ Producer restart (same transactional.id)
├─ Transaction coordinator failover
└─ Explicit fence (e.g., via admin API)

Example:
├─ Producer 1 (PID=12345, epoch=0) starts
├─ Producer 1 crashes
├─ Producer 2 (same transactional.id) starts
├─ Gets PID=12345, epoch=1
└─ Old producer (epoch=0) is fenced

Fencing:
└─ Brokers reject requests from lower epochs
└─ Prevents zombie producers from writing

4. Transaction Log (__transaction_state)

Log Structure

__transaction_state TOPIC:

Internal Kafka topic:
├─ 50 partitions (default, configurable)
├─ Replication factor: 3 (configurable)
├─ Compacted (only latest transaction state per key)
└─ Not visible to regular consumers

Key:
└─ transactional.id

Value:
└─ Transaction metadata:
    ├─ PID (Producer ID)
    ├─ Epoch
    ├─ Transaction state (Ongoing, PrepareCommit, CompleteCommit, etc.)
    ├─ Transaction timeout
    ├─ Partitions involved in transaction
    └─ Timestamp

Transaction States

TRANSACTION STATE MACHINE:

Empty:
└─ No active transaction

Ongoing:
└─ Transaction in progress (writes happening)

PrepareCommit / PrepareAbort:
└─ Producer requested commit/abort, coordinator preparing

CompleteCommit / CompleteAbort:
└─ Markers written, transaction complete

Dead:
└─ Transaction timed out or explicitly aborted

STATE TRANSITIONS:

Empty → Ongoing:
└─ Producer begins transaction (first write)

Ongoing → PrepareCommit:
└─ Producer calls commitTransaction()

PrepareCommit → CompleteCommit:
└─ Coordinator writes commit markers to all partitions

Ongoing → PrepareAbort:
└─ Producer calls abortTransaction() or timeout

PrepareAbort → CompleteAbort:
└─ Coordinator writes abort markers

CompleteCommit/CompleteAbort → Empty:
└─ Transaction finalized, ready for next transaction

5. Transaction Flow

Beginning a Transaction

START TRANSACTION:

1. Producer configured with:
   └─ transactional.id = "order-processor-1"
   └─ enable.idempotence = true (required)

2. Producer calls initTransactions():
   ├─ Sends FindCoordinatorRequest
   ├─ Connects to coordinator
   ├─ Sends InitProducerIdRequest
   └─ Receives PID and epoch

3. Producer calls beginTransaction():
   └─ Local state change (no broker call yet)

4. Producer sends records:
   ├─ Records include PID, epoch, sequence number
   ├─ Broker writes to partition log
   └─ Broker updates transaction log (add partition to transaction)

5. Transaction now "Ongoing" in coordinator state

Committing a Transaction

COMMIT TRANSACTION:

1. Producer calls commitTransaction():
   └─ Sends EndTxnRequest(commit) to coordinator

2. Coordinator:
   a. Transition state: Ongoing → PrepareCommit
   b. Write state to transaction log (durable)

3. Coordinator sends WriteTxnMarkersRequest to all partitions involved:
   └─ "Write COMMIT marker for PID=12345, epoch=0"

4. Partition leaders:
   ├─ Write COMMIT control record to log
   ├─ Respond to coordinator: Success
   └─ Consumer isolation level determines visibility

5. Coordinator:
   ├─ Transition state: PrepareCommit → CompleteCommit
   ├─ Write to transaction log
   └─ Respond to producer: Commit successful

6. Producer ready for next transaction

Aborting a Transaction

ABORT TRANSACTION:

1. Producer calls abortTransaction() OR transaction times out

2. Coordinator:
   a. Transition state: Ongoing → PrepareAbort
   b. Write state to transaction log

3. Coordinator sends WriteTxnMarkersRequest to all partitions:
   └─ "Write ABORT marker for PID=12345, epoch=0"

4. Partition leaders:
   ├─ Write ABORT control record to log
   ├─ Marks all records in transaction as aborted
   └─ Respond to coordinator: Success

5. Coordinator:
   ├─ Transition state: PrepareAbort → CompleteAbort
   ├─ Write to transaction log
   └─ Respond to producer: Abort successful

6. Aborted records remain in log but invisible to read_committed consumers

6. Transaction Markers

Control Records

COMMIT/ABORT MARKERS:

Control records:
└─ Special records in partition log
└─ Not visible to consumers (filtered out)
└─ Indicate transaction boundaries

COMMIT marker:
├─ Type: CONTROL_RECORD
├─ ControlType: COMMIT
├─ PID: 12345
├─ Epoch: 0
└─ Offset: 1050

ABORT marker:
├─ Type: CONTROL_RECORD
├─ ControlType: ABORT
├─ PID: 12345
├─ Epoch: 0
└─ Offset: 1055

Purpose:
└─ Mark end of transaction
└─ Enable read_committed isolation

LSO (Last Stable Offset)

LAST STABLE OFFSET:

Definition:
└─ Offset of first message in open transaction
└─ Or LEO if no open transactions

Calculation:
├─ Scan log for ongoing transactions
├─ Find earliest offset of open transaction
└─ LSO = that offset (or LEO if none)

Example:
├─ Offsets 0-999: Committed
├─ Offsets 1000-1010: Transaction 1 (ongoing)
├─ Offsets 1011-1020: Committed
└─ LSO: 1000 (first open transaction)

read_committed consumers:
└─ Can only read up to LSO
└─ Ensures they never see uncommitted data

7. Exactly-Once Semantics (EOS)

Broker-Side Guarantees

BROKER ENSURES:

1. Idempotent Writes:
   ├─ Producer sends: PID, epoch, sequence number
   ├─ Broker deduplicates based on sequence
   └─ Retries don't create duplicates

2. Transactional Writes:
   ├─ Multiple writes across partitions
   ├─ Atomic commit (all visible or none)
   └─ No partial writes visible

3. Fencing:
   ├─ Epoch increments fence old producers
   └─ Prevents zombie writes

4. Coordinator Durability:
   ├─ Transaction state written to replicated log
   └─ Survives coordinator failure

Combined:
└─ Exactly-once semantics (end-to-end with EOS pipeline)

Isolation Levels

CONSUMER ISOLATION LEVELS:

read_uncommitted (default):
└─ Consumers see all records (including uncommitted)
└─ High throughput, no transaction semantics

read_committed:
├─ Consumers only see committed records
├─ Reads up to LSO (Last Stable Offset)
├─ Aborted records filtered out
└─ Ensures transactional consistency

Configuration:
└─ isolation.level = read_committed (consumer config)

Trade-off:
├─ read_committed: Slight latency (wait for commit markers)
└─ read_uncommitted: No transaction guarantees

8. Coordinator Failover

Transaction Recovery

COORDINATOR FAILS:

1. Transaction coordinator (Broker 2) dies

2. Controller detects failure

3. New leader elected for __transaction_state partition
   └─ New coordinator: Broker 3

4. New coordinator loads transaction log:
   ├─ Reads all transaction state
   ├─ Rebuilds in-memory state
   └─ Identifies incomplete transactions

5. For each incomplete transaction:
   a. If PrepareCommit: Complete commit (write markers)
   b. If PrepareAbort: Complete abort (write markers)
   c. If Ongoing: Timeout (abort)

6. New coordinator fully operational

Recovery time:
└─ Seconds (depends on transaction log size)

9. Transaction Performance

Costs

TRANSACTION OVERHEAD:

1. Coordinator Communication:
   └─ InitProducerIdRequest, EndTxnRequest

2. Transaction Log Writes:
   └─ State changes written to replicated log

3. Marker Writes:
   └─ COMMIT/ABORT markers to all involved partitions

4. LSO Calculation:
   └─ Broker scans for open transactions

5. Read Latency (read_committed):
   └─ Consumers wait for commit markers

Impact:
├─ Throughput: ~10-20% reduction (vs non-transactional)
├─ Latency: +10-50ms (commit marker propagation)
└─ Acceptable for EOS guarantee

Optimizations

REDUCING OVERHEAD:

1. Batch transactions:
   └─ Large transactions amortize fixed costs

2. Minimize partitions per transaction:
   └─ Fewer marker writes

3. Tune transaction.timeout.ms:
   └─ Balance responsiveness vs abort rate

4. Use dedicated coordinator brokers:
   └─ Isolate coordinator load (not always needed)

5. Monitor transaction metrics:
   └─ transaction-count, transaction-duration

10. Key Takeaways

You've learned:

✅ Transaction coordinator manages transaction lifecycle
✅ Transaction log (__transaction_state) stores transaction state
✅ Producer ID (PID) + epoch identify and fence producers
✅ Transaction states: Ongoing → Prepare → Complete
✅ Commit markers indicate transaction boundaries
✅ LSO (Last Stable Offset) determines read_committed visibility
✅ Exactly-once semantics via idempotence, transactions, fencing
✅ Isolation levels: read_uncommitted vs read_committed
✅ Coordinator failover recovers incomplete transactions
✅ Transaction overhead ~10-20% throughput reduction (acceptable)


Summary

Kafka transactions enable exactly-once semantics:

Architecture =
├─ Transaction coordinator (broker-side role)
├─ Transaction log (__transaction_state, 50 partitions)
├─ Producer ID (PID) + epoch (fencing)
└─ Commit/abort markers (control records)

Transaction Flow =
├─ Begin: Producer gets PID, starts transaction
├─ Write: Records include PID, epoch, sequence
├─ Commit: Coordinator writes markers, transition state
└─ Abort: Coordinator writes abort markers

Guarantees =
├─ Idempotence: Sequence numbers deduplicate
├─ Atomicity: All writes visible or none
├─ Fencing: Epochs prevent zombie writes
└─ Durability: Transaction state replicated

Isolation =
├─ read_uncommitted: See all records (no EOS)
└─ read_committed: Only committed (EOS guarantee)

Coordinator Failover =
├─ New coordinator loads transaction log
├─ Completes incomplete transactions
└─ Seconds to recover

Remember: Kafka transactions are broker-coordinated and provide exactly-once semantics when used correctly. Understanding broker-side mechanics explains transaction behavior, performance characteristics, and failure recovery.


🎉 Congratulations!

You've completed all 15 modules of Kafka Internal Mechanics!

You now have deep knowledge of:

  • ✅ End-to-end message flow
  • ✅ Topics, partitions, and log structure
  • ✅ Replication and durability protocols
  • ✅ Leader election and failover
  • ✅ Cluster metadata and coordination (ZK and KRaft)
  • ✅ Controller internals
  • ✅ Broker lifecycle
  • ✅ Transactions and exactly-once semantics

This knowledge empowers you to:

  • Design highly available and durable Kafka architectures
  • Troubleshoot complex Kafka issues at the internals level
  • Optimize Kafka performance based on deep understanding
  • Confidently operate Kafka in production

Keep learning and experimenting with Kafka! 🚀