Skip to main content

Command Palette

Search for a command to run...

Inbox Pattern with SpringBoot Kotlin and Kafka

Building Reliable Event Consumers with Idempotency and Exactly-Once Semantics

Updated
β€’3 min read

In the previous article, we explored the Outbox Pattern to guarantee that events are published reliably from a producer service.

But publishing events safely is only half of the problem.

πŸ‘‰ What happens on the consumer side ?

  • What if the same Kafka message is delivered twice ?

  • What if the consumer crashes after partially processing an event ?

  • How do we avoid corrupting state when events are reprocessed ?

This is where the Inbox Pattern becomes essential.

1. Context : Kafka guarantees and their limits

Kafka provides at-least-once delivery by default.

This means :

  • messages may be delivered more than once

  • duplicates are not a bug β€” they are expected

Kafka prioritizes availability and throughput, not business-level idempotency.

πŸ‘‰ Therefore, consumers must be designed to handle duplicates safely.

2. First attempt : Naive implementation

A typical consumer looks like this:

@KafkaListener(topics = ["commands"])
fun handle(event: CommandCreatedEvent) {
    orderService.createOrder(event)
}

This works… until something goes wrong.

3. Failure Scenario : Partial processing

Consider the following situation :

  1. Kafka delivers an event

  2. The service updates the database

  3. The service crashes before committing the offset

Result :

  • Kafka re-delivers the same event

  • the database update runs again

πŸ‘‰ Duplicate side effects occur.

This is how data corruption happens in event-driven systems.

4. Why idempotency is not optional

In distributed systems :

You must assume that everything can happen more than once.

Retries, rebalances, restarts, and network failures all cause reprocessing.

πŸ‘‰ The solution is not to prevent duplicates, but to detect and ignore them.

5. Introducing the inbox pattern

The Inbox Pattern is based on a simple principle :

Persist every consumed event before processing it.

If the event was already processed, skip it.

This requires :

  • a persistent inbox store

  • a unique event identifier

6. Modeling the Inbox Collection

We use MongoDB because :

  • fast writes

  • natural document model

  • unique index support

@Document(collection = "inbox_event")
data class InboxEvent(
    @Id val eventId: String,
    val eventType: String,
    val receivedAt: Instant = Instant.now()
)

MongoDB guarantees uniqueness via _id.

7. Reactive inbox repository

interface InboxRepository : ReactiveMongoRepository<InboxEvent, String>

A duplicate insert will fail automatically.

8. Safe event consumption flow

The correct flow becomes :

  1. receive Kafka event

  2. insert event ID into inbox

  3. if insert succeeds β†’ process event

  4. if insert fails β†’ ignore event

9. Reactive kafka consumer with inbox

@KafkaListener(topics = ["commands"], groupId = "orders")
fun consume(event: CommandCreatedEvent): Mono<Void> {
    return inboxRepository.save(
        InboxEvent(eventId = event.id, eventType = "CommandCreated")
    )
    .then(orderService.createOrder(event))
    .onErrorResume(DuplicateKeyException::class.java) {
        Mono.empty()
    }
}

This ensures exactly-once side effects.

10. Offset commit strategy

Offsets must be committed after successful processing.

Spring Kafka supports :

  • manual acknowledgment

  • reactive backpressure

The inbox guarantees safety even if offsets are replayed.

11. Global architecture with outbox + inbox

        Producer Service
              β”‚
          Outbox Table
              β”‚
           Debezium
              β”‚
            Kafka
              β”‚
        Consumer Service
              β”‚
         Inbox Collection
              β”‚
        Business Processing

12. Production benefits

With the Inbox Pattern :

  • no duplicate side effects

  • safe reprocessing

  • crash resilience

  • simpler retry logic

Combined with Outbox :

  • end-to-end reliability

  • strong consistency guarantees

Conclusion

The Inbox Pattern is the natural counterpart of the Outbox Pattern.

Outbox guarantees safe publishing. Inbox guarantees safe consumption.

Together, they form the foundation of reliable event-driven microservices.

In real-world systems, this combination is not optional β€” it is mandatory.