Learnitweb

Introduction to Transactions in Apache Kafka with Spring Boot

In this tutorial, we will explore Kafka Transactions—what they are, why they’re needed, and how to enable them in a Spring Boot Kafka microservices architecture. Transactions in Kafka are vital for ensuring data consistency and achieving exactly-once semantics (EOS) in distributed systems.

1. Why Use Transactions in Kafka?

Kafka transactions are primarily used to achieve two critical goals:

a. All-or-Nothing Semantics

All Kafka operations within a transaction either succeed together or fail together. If any step fails, none of the messages are committed. This ensures that the system does not end up in an inconsistent state due to partial processing.

b. Exactly-Once Semantics (EOS)

Kafka provides exactly-once delivery guarantees when using:

  • Idempotent Producer (prevents duplicate messages)
  • Transactional Producer (ensures atomicity and isolation)

Together, these features help avoid issues like processing the same event multiple times or committing inconsistent changes.

2. Common Kafka Application Pattern

A typical Kafka-based microservice follows this pattern:

Consume → Process → Produce

Example Scenario: Money Transfer System

  • A Transfer Kafka topic receives a message to transfer money from Account A to Account B.
  • The Transfer Microservice consumes this message and starts processing:
    • Produces a new message to the Withdrawal topic (handled by WithdrawMicroservice)
    • Later, produces a message to the Deposit topic (handled by DepositMicroservice)
  • If the microservice crashes in between these steps, the partial processing leads to inconsistent state.

3. The Problem Without Transactions

Let’s say the TransferMicroservice consumes a message and sends a Kafka message to Withdrawal topic. Then it crashes before producing to the Deposit topic.

What happens?

  • WithdrawMicroservice processes the message → money withdrawn
  • DepositMicroservice never receives its message → money never deposited

Outcome: Money is lost.

And since Kafka will deliver the original message again (because it wasn’t marked as successfully consumed), WithdrawMicroservice might withdraw again, leading to double withdrawal.

4. How Kafka Transactions Solve This

With Kafka transactions enabled:

  • All produce operations (to withdrawal and deposit) are grouped into a single transaction.
  • Until the transaction is successfully committed, these messages remain invisible to consumers.
  • If the transaction fails or the service crashes:
    • The transaction is aborted.
    • The events remain uncommitted and are not consumed.

5. Kafka Transactional Behavior Explained

Here’s how Kafka ensures data consistency using transactions:

StepDescription
1Transfer Microservice consumes a message from the transfer topic
2Kafka transaction begins
3A message is sent to the withdrawal topic (but it’s marked uncommitted)
4A message is sent to the deposit topic (also uncommitted)
5If all operations succeed, the transaction is committed, and both messages become visible
6If a failure occurs, the transaction is aborted, and the messages stay invisible

6. Visibility of Messages in Transactions

Kafka consumers can be configured to read only committed messages using:

isolation.level=read_committed
  • read_committed: Consumers see only committed messages.
  • read_uncommitted: Consumers see all messages, even uncommitted (not recommended for transactional setups).

This isolation ensures that partial or failed transactions don’t lead to inconsistencies.

7. Cross-Service Limitations

Kafka transactions do not span remote services or databases:

  • HTTP requests to other services are not part of Kafka’s transaction.
  • Database writes (like to MySQL or PostgreSQL) are not managed by Kafka.

To handle failures across systems:

  • You need to implement compensating transactions, which are logic routines to undo partial operations.
  • For example, if a deposit fails, you might issue a compensating event to reverse the withdrawal.

8. Spring Boot Integration with Kafka Transactions

While Kafka doesn’t manage database transactions, Spring Boot (with Spring Kafka) provides integration that allows:

  • Kafka transactions and database transactions to be synchronized.
  • If any part fails, both transactions (Kafka + DB) can be rolled back together.

9. Enabling Kafka Transactions in Spring Boot

Here’s how to enable and use Kafka transactions in a Spring Boot microservice.

Step 1: Enable Idempotent Producer and Transactions

Add this to your application.yml or application.properties:

spring:
  kafka:
    producer:
      transaction-id-prefix: tx-  # Required to enable transactions
      properties:
        enable.idempotence: true  # Ensures exactly-once delivery
    consumer:
      isolation-level: read_committed  # Consumer reads only committed messages

Step 2: Declare Kafka Transaction Manager

@Configuration
public class KafkaConfig {

    @Bean
    public KafkaTransactionManager<String, String> kafkaTransactionManager(ProducerFactory<String, String> pf) {
        return new KafkaTransactionManager<>(pf);
    }
}

Step 3: Use @Transactional in Your Service

@Service
public class TransferService {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Transactional("kafkaTransactionManager")
    public void processTransfer(Transfer transfer) {
        kafkaTemplate.send("withdrawal", transfer.getFromAccount(), transfer.toString());
        kafkaTemplate.send("deposit", transfer.getToAccount(), transfer.toString());

        // If something goes wrong here, both messages are rolled back
        if (transfer.getAmount() < 0) {
            throw new RuntimeException("Invalid amount");
        }
    }
}

Important:

You must annotate the method with @Transactional("kafkaTransactionManager") so that Spring manages the Kafka transaction boundaries.