This page is meant as a template for writing a KIP. To create a KIP choose Tools->Copy on this page and modify with your content and replace the heading with the next KIP number and a description of your issue. Replace anything in italics with your own description.

Status

Current state: Under Discussion

Discussion thread: here 

JIRA: [KAFKA-19883]()

Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

Motivation


Share Groups currently support only immediate acknowledgement modes (IMPLICIT/EXPLICIT). This causes data loss in distributed streaming frameworks:

1. Worker acknowledges records → Records removed from Kafka
2. Checkpoint fails before sink write
3. Records lost (acknowledged but never persisted)

Goal: Enable exactly-once read semantics via transactional acknowledgements.

We will be following the similar existing pattern we have in Kafka Producer Transactions.

Use Cases

- Apache Flink: Exactly-once checkpointing with Share Group sources
Apache Spark: Structured Streaming with Share Group consumers
Any coordinator-worker streaming framework requiring atomic acknowledgements

Public Interfaces

APIs 

```

// When you have a producer and want atomic output + acks

producer.beginTransaction();

producer.send(output);

producer.sendShareAcksToTransaction(acks, groupId);

producer.commitTransaction();

```


```

// When there's no producer (Flink source, Spark source, pure consumer)

public class TransactionalShareAcknowledger implements Closeable {

    public TransactionalShareAcknowledger(Map<String, Object> config);

    public void initTransactions();

    public void commitAcknowledgements(

        Map<TopicPartition, List<ShareAcknowledgement>> acks, String groupId);

    public void abortAcknowledgements();

    public void close();

}

```

Internally, TransactionalShareAcknowledger is a thin wrapper around a KafkaProducer (or its TransactionManager). It use the exact same RPCs - InitProducerId, AddShareAcksToTxn, TxnShareAcknowledge, EndTxn.

No new server-side infrastructure needed.

```

// TransactionalShareAcknowledger — internally just wraps a KafkaProducer 
TransactionalShareAcknowledger acknowledger = 
    new TransactionalShareAcknowledger(config);  // config has transactional.id
acknowledger.initTransactions();
acknowledger.commitAcknowledgements(acks, shareGroupId);  // begin+ack+commit in one call

```

Coordinator API


New Configuration

Config                           Default      Description
share.acknowledgement.mode       explicit     Values: implicit, explicit, transactional
share.transaction.timeout.ms     60000        Maximum transaction duration before auto-abort
share.transaction.max.pending    5            Maximum concurrent transactions per group


New Metrics

Metric                                Type        Description
share-transaction-active              Gauge       Active transactions
share-transaction-prepare-time-ms     Histogram   Prepare latency
share-transaction-commit-time-ms      Histogram   Commit latency
share-transaction-abort-total         Counter     Aborted transactions
share-transaction-timeout-total       Counter     Timed-out transactions

Proposed Changes

1. Two-Phase Commit Protocol

Transaction States Transitions

existing Kafka 2PC (TransactionState: ONGOING→PREPARE_COMMIT→COMPLETE_COMMIT)

- EMPTY -> ONGOING -> PREPARE -> ABORTED (abort after prepare)

- EMPTY -> ONGOING -> ABORTED (early abort)

Sequence Diagram

Two-Phase Commit Protocol Flow:

Step 1: Coordinator sends BeginBatchTxn(id) to Broker
Step 2: Executor-1 calls poll() to Broker - gets records
Step 3: Executor-2 calls poll() to Broker - gets records
Step 4: Executor-1 sends ack(txnId, records) to Broker - records become LOCKED
Step 5: Executor-2 sends ack(txnId, records) to Broker - records become LOCKED
Step 6: Coordinator sends PrepareBatchTxn(id) to Broker - state becomes PREPARE
Step 7: Broker responds "prepared" to Coordinator
Step 8: Coordinator sends CommitBatchTxn(id) to Broker - LOCKED records become ACKNOWLEDGED
Step 9: Broker responds "committed" to Coordinator





This KIP reuses Kafka's existing two-phase commit protocol:


Transaction metadata stays in __transaction_state.

Share acks are written transactionally to __share_group_state using CoordinatorRuntime.scheduleTransactionalWriteOperation().

Failure Handling

existing TransactionCoordinator recovery

2. Wire Protocol Details

TxnShareAcknowledgeRequest

```
ShareAcknowledgeTransactionalRequest => GroupId TransactionId [Topics]
  GroupId => STRING
  TransactionId => STRING
  Topics => TopicId [Partitions]
    Partitions => Partition [AcknowledgementBatches]
      AcknowledgementBatches => FirstOffset LastOffset AcknowledgeType
```

ShareBeginTransactionRequest

Use existing InitProducerIdRequest

SharePrepareTransactionRequest

 Use existing EndTxnRequest

ShareCommitTransactionRequest / ShareAbortTransactionRequest

Handled by existing WriteTxnMarkersRequest (inter-broker) and  EndTxnRequest(ABORT)

AddShareAcksToTxnRequest

mirrors AddOffsetsToTxnRequest; This tells the TransactionCoordinator to add __share_group_state partitions to the producer's transaction.

Consume-Transform-Produce Pattern

This is the exactly-once guarantee: either both output AND acks commit, or neither does: 

```

KafkaShareConsumer<K, V> consumer = new KafkaShareConsumer<>(props);  // NOT transactional
KafkaProducer<K, V> producer = new KafkaProducer<>(props);            // transactional.id

producer.initTransactions();

while (true) {
    ConsumerRecords<K, V> records = consumer.poll(...);  // ← just reads, not transactional
    
    producer.beginTransaction();
    for (var record : records) {
        producer.send(new ProducerRecord<>("output", transform(record)));
    }
    producer.sendShareAcksToTransaction(buildAcks(records), "my-share-group");  //  transactional write
    producer.commitTransaction();
    // Atomically: output records committed + share acks committed
    // If abort: output rolled back + acks discarded → records return to AVAILABLE
}

```

In the existing consumer group CTP pattern, the same thing happens (in the above code instead of KafkaShareConsumer, use KafkaConsumer and Consumer offsets to __consumer_offsets).


If abortTransaction() is called (or the transaction times out):

Compatibility, Deprecation, and Migration Plan

Impact on Existing Users

- No breaking changes for existing Share Group users
- `transactional` mode is opt-in via `share.acknowledgement.mode` config
- Existing `implicit` and `explicit` modes continue to work unchanged

Migration Path

1. Phase 1: Add `transactional` acknowledgement mode (backward compatible)
2. Phase 2: Streaming frameworks (Flink, Spark) implement transactional sources
3. Phase 3: Documentation and best practices for exactly-once semantics

Deprecation

- No deprecation of existing modes planned
- `transactional` mode recommended for exactly-once use cases

Test Plan

Unit Tests

- Transaction state machine transitions
- Idempotency of all transaction operations
- Timeout handling and auto-abort
- Record state transitions (ACQUIRED → LOCKED → ACKNOWLEDGED)

Integration Tests

- End-to-end transaction commit/abort flows
- Multi-consumer transactions within same group
- Coordinator failure and recovery
- Network partition scenarios

System Tests

- Flink checkpoint integration with transactional Share Groups
- Exactly-once delivery verification under failures
- Performance benchmarks comparing transactional vs non-transactional modes
- Stress testing with concurrent transactions

Chaos Tests

- Broker failure during PREPARE state
- Coordinator crash before/after prepare
- Consumer crash during transaction
- Network partitions between coordinator and brokers

Rejected Alternatives

If there are alternative ways of accomplishing the same thing, what were they? The purpose of this section is to motivate why the design is the way it is and not some other way.