Back to Home

Kafka Idempotency: Delivery Guarantees in Practice

The article covers practical setup of idempotency in Kafka for producer and consumer. It discusses ProducerId, ProducerEpoch, critical configuration parameters and protection against double processing via business logic in PostgreSQL.

Idempotent Kafka: producer and consumer without duplicates
Advertisement 728x90

# Kafka Delivery Guarantees: Producer and Consumer Idempotency in Practice

The inventory-service is switching from in-memory storage to PostgreSQL to support the Outbox Pattern and delivery guarantees. Add a postgres-inventory container to docker-compose.yaml with image postgres:17, ports 5432:5432, and environment variables POSTGRES_USER=postgres, POSTGRES_PASSWORD=postgres, POSTGRES_DB=inventory_db.

In the inventory-service's pom.xml, add the following dependencies:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

In application.properties, specify the connection:

Google AdInline article slot
spring.datasource.url=jdbc:postgresql://localhost:5432/inventory_db
spring.datasource.username=postgres
spring.datasource.password=postgres

Create the inventory table:

CREATE TABLE inventory (
    id BIGSERIAL PRIMARY KEY,
    product_name VARCHAR(256) NOT NULL UNIQUE,
    quantity INT NOT NULL CHECK (quantity >= 0)
);

INSERT INTO inventory (product_name, quantity) VALUES
('Smartphone', 5),
('Tablet', 10),
('Desktop', 6);

Inventory entity:

@Entity
@Table(name = "inventory")
public class Inventory {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "product_name", nullable = false, unique = true, length = 256)
    private String productName;

    @Column(name = "quantity", nullable = false)
    private Integer quantity;

    public Inventory(String productName, Integer quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }
    // getters, setters
}

Repository with deductStock:

Google AdInline article slot
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
    @Modifying
    @Query("UPDATE Inventory i SET i.quantity = i.quantity - :quantity WHERE i.productName = :productName AND i.quantity >= :quantity")
    int deductStock(@Param("productName") String productName, @Param("quantity") Integer quantity);
}

The KafkaListener handler in the service checks stock availability and sends InventoryReservedEvent or InventoryRejectedEvent.

Setting up acks and understanding exactly-once semantics

The spring.kafka.producer.acks parameter controls acknowledgments: 0 for at-most-once, 1 for partial, all for at-least-once with replication on all followers.

Exactly-once delivery in distributed systems is impossible due to the Two Generals' Problem. It is implemented as exactly-once semantics: at-least-once delivery + producer and consumer idempotency.

Google AdInline article slot

Producer Idempotency: ProducerId, Epoch, and sequence number

Idempotency prevents duplicates on retry due to ack loss. It is enabled by:

spring.kafka.producer.properties.enable.idempotence=true

The broker assigns a (ProducerId, ProducerEpoch) pair to the connection:

  • ProducerId: unique producer identifier
  • ProducerEpoch: increments on restart, protection against zombie producers

Each message is numbered with a sequence number. The broker rejects duplicates within ProducerId/Epoch/sequence.

Zombie producer scenario:

  • Producer crashes, but "revives" after a new one restarts
  • Old one continues with high sequence number
  • New one starts from 1 — ProducerEpoch distinguishes them

Transactional.id ensures ProducerId preservation on restart (Spring calls initTransactions()). Without it — new random ProducerId.

Critical idempotency parameters

spring.kafka.producer.acks=all
spring.kafka.producer.retries=2147483647  # >0
spring.kafka.producer.properties.max.in.flight.requests.per.connection=5  # ≤5
spring.kafka.producer.properties.enable.idempotence=true

Violation disables idempotency. Apply to order-service and inventory-service.

Consumer Idempotency Against Double Processing

The consumer may process a message twice if it crashes after committing the offset but before completing business logic. For inventory-service:

  • Reads OrderPlacedEvent
  • deductStock() in PostgreSQL (atomically)
  • Sends InventoryReservedEvent
  • Crash
  • Re-reads offset, repeats deductStock() — but quantity already reduced, UPDATE won't work

This ensures idempotency at the business logic level via conditional UPDATE.

Advantages of the approach:

  • No additional offset storage
  • Uses DB ACID transactions
  • Simple implementation via WHERE quantity >= :quantity

Key Points

  • Producer Idempotency: enable.idempotence=true + acks=all + retries>0 + max.in.flight≤5
  • ProducerEpoch: protection against zombie producers on restart
  • Transactional.id: preserves ProducerId, requires initTransactions()
  • Consumer: business logic with conditional UPDATE in DB ensures idempotence
  • Exactly-once semantics: at-least-once + idempotence, not literal exactly-once

— Editorial Team

Advertisement 728x90

Read Next