How to Implement Exactly-Once Transactions in Spring Boot with Kafka

This guide explains how to configure Spring Boot and Kafka to achieve Exactly-Once semantics, covering core concepts, Maven dependencies, YAML settings, sample code for producers and consumers, execution flow, and advanced tips for reliable transactional messaging.

Ray's Galactic Tech
Ray's Galactic Tech
Ray's Galactic Tech
How to Implement Exactly-Once Transactions in Spring Boot with Kafka

Core Concepts

Kafka transaction : Treats producing, consuming, and offset committing as a single atomic operation.

Transactional producer : Opens, commits or aborts a transaction; messages become visible to consumers only after a commit.

Transaction coordinator : Broker component that records transaction state and logs.

Transactional ID : Unique identifier for a producer instance; enables ordering and recovery after failures.

Isolation level : read_committed reads only messages from committed transactions; read_uncommitted (default) reads all messages.

Project Dependencies and Configuration

Add the Spring for Kafka starter to pom.xml:

<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
  <version>2.8.0</version>
</dependency>

Configure application.yml (replace your-kafka-server with the actual broker address):

spring:
  kafka:
    bootstrap-servers: your-kafka-server:9092
    producer:
      retries: 3
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      transaction-id-prefix: tx-
      properties:
        enable.idempotence: true
    consumer:
      group-id: my-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      properties:
        isolation.level: read_committed

Practical Code Example

1. Event POJO

public class UserRegisteredEvent {
    private String userId;
    private String email;
    private java.time.LocalDateTime timestamp;
    // getters, setters, constructors omitted for brevity
}

2. Service Layer – combine DB and Kafka transaction

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    private final UserRepository userRepository;
    private final KafkaTemplate<String, Object> kafkaTemplate;
    private static final String TOPIC_USER_REGISTERED = "topic.user.registered";

    public UserService(UserRepository userRepository, KafkaTemplate<String, Object> kafkaTemplate) {
        this.userRepository = userRepository;
        this.kafkaTemplate = kafkaTemplate;
    }

    @Transactional(rollbackFor = Exception.class)
    public User registerUser(User user) {
        // 1. Persist user in the database
        User savedUser = userRepository.save(user);
        // 2. Build the event object
        UserRegisteredEvent event = new UserRegisteredEvent();
        event.setUserId(savedUser.getId());
        event.setEmail(savedUser.getEmail());
        event.setTimestamp(java.time.LocalDateTime.now());
        // 3. Send the event to Kafka (participates in the same transaction)
        kafkaTemplate.send(TOPIC_USER_REGISTERED, savedUser.getId(), event);
        return savedUser;
    }
}

3. Consumer Listener

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class UserEventListener {
    @KafkaListener(topics = "topic.user.registered")
    public void handleUserRegistered(UserRegisteredEvent event) {
        System.out.println("Received user registered event: " + event);
        // business logic such as sending a welcome email
    }
}

Execution Flow

Spring starts a combined database + Kafka transaction.

The service saves the user record to the database.

The Kafka producer writes the event to the transaction log (pre‑commit).

If the method returns normally, Spring commits both the database transaction and the Kafka transaction, making the message visible to consumers.

If an exception is thrown, Spring rolls back the database transaction and aborts the Kafka transaction, discarding the message.

Advanced Usage and Tips

Programmatic Kafka Transaction

@Autowired
private KafkaTransactionManager<String, Object> transactionManager;

public void someMethod() {
    TransactionTemplate template = new TransactionTemplate(transactionManager);
    template.execute(status -> {
        kafkaTemplate.send("someTopic", "key", "value");
        return null;
    });
}

Multiple Transaction Managers

Spring Boot can auto‑configure ChainedTransactionManager to coordinate a relational database transaction and a Kafka transaction in a single @Transactional boundary.

Transactional ID and Performance

Do not reuse the same transactional.id across many producer instances; each instance should have a unique ID.

The transaction-id-prefix property automatically appends a numeric suffix (e.g., tx-0, tx-1) to guarantee uniqueness.

Error Handling and Retries

Configure retries and delivery.timeout.ms in the producer settings; the transactional producer will retry internally before aborting.

When a transaction aborts, the surrounding Spring transaction is rolled back, preserving atomicity.

Summary

Enable a transactional producer by setting transaction-id-prefix and enable.idempotence=true in the producer configuration.

Wrap database and Kafka operations inside a single @Transactional method (or a programmatic TransactionTemplate) to achieve exactly‑once semantics.

Configure consumers with isolation.level=read_committed so they only receive messages from committed transactions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendtransactionExactly-Oncespring-boot
Ray's Galactic Tech
Written by

Ray's Galactic Tech

Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.