← Back to list

Transactional Pub/Sub in Spring for Multi-Instance, Production-Grade Systems

When building distributed systems with Spring Boot, one recurring challenge appears sooner or later:

Mahdi Tilab · 2025-11-30 19:48 · 0 claps · 3.1 min read
#microservices #outbox-pattern #pubnub #distributed-systems
Open on Medium ↗

Transactional Pub/Sub in Spring for Multi-Instance, Production-Grade Systems

When building distributed systems with Spring Boot, one recurring challenge appears sooner or later:

How do we publish and consume events in a way that is consistent with database transactions, especially when the system runs on multiple instances?

In simple setups, sending a message after saving something in the database seems fine. But in real-world, multi-instance microservice environments, this can easily lead to inconsistent data, lost messages, or duplicate events.

This article breaks down:

  • What “transactional pub/sub” really means
  • The safest patterns in multi-instance systems
  • Which solutions work at scale
  • What you should use in production with Spring

The Core Problem

Consider this common code:

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    publisher.publish(new OrderCreatedEvent(order));
}

Here is the risk:

  1. The message is published ✅
  2. The database transaction fails and rolls back ❌
  3. Now other services think the order exists, but it doesn’t

This breaks the fundamental principle of data consistency.

We need a way to ensure:

The message is only published if and when the database transaction is committed successfully.

That is where transactional pub/sub comes in.

Solution 1 — @TransactionalEventListener (Local only)

Spring provides a powerful but often misunderstood feature:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

Example:

Publisher

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    applicationEventPublisher.publishEvent(new OrderCreatedEvent(order));
}

Listener

@Component
public class OrderListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handle(OrderCreatedEvent event) {
        System.out.println("Order successfully committed: " 
            + event.getOrder().getId());
    }
}

This guarantees:

✅ Event is fired only after commit ❌ No event if rollback occurs

However:

This mechanism is only local to that Spring instance.

If you run 10 instances of this app:

  • Each instance receives only its own events
  • There is no inter-instance communication

Use this when:

  • You need in-process consistency
  • For cache updates, audit logs, metrics, internal workflows

Do NOT use this for:

  • Microservice-to-microservice messaging
  • Multi-node coordination
  • Integration events for other systems

Solution 2 — The Transactional Outbox Pattern (BEST)

The Transactional Outbox Pattern is the industry-proven solution for transactional messaging in distributed systems.

Instead of publishing directly to Kafka, SQS, or RabbitMQ, we write the event to an outbox table inside the same database transaction.

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    OutboxEvent event = new OutboxEvent(
        "ORDER_CREATED",
        convertToJson(order)
    );
    outboxRepository.save(event);
}

Now there is only one truth: the database.

After commit, a separate process publishes messages from the Outbox.

Publishing strategies:

Option A — Polling worker (recommended)

A background job runs:

SELECT * 
FROM outbox 
WHERE published = false
FOR UPDATE SKIP LOCKED
LIMIT 100;

Then it:

  1. Sends the event to Kafka/SQS/SNS
  2. Marks it as published = true

This is 100% safe for multi-instance setups because:

  • SKIP LOCKED prevents double processing
  • All services share one DB
  • No race conditions
  • No duplicates

✅ Kubernetes safe ✅ Multi-replica safe ✅ Cloud-native

Option B — CDC with Debezium (enterprise scale)

Instead of polling:

  • Debezium listens to DB commit log
  • Streams changes to Kafka
  • Kafka distributes to consumers

This is what companies like Netflix, Uber, and Airbnb use.

Solution 3 — Kafka Transactions (Advanced & risky)

Kafka supports transactions:

spring.kafka.producer.transaction-id-prefix=tx-
@Transactional
public void createOrder(Order order) {
   orderRepository.save(order);
   kafkaTemplate.send("orders", order);
}

But in multi-instance environments:

  • Each producer must have unique IDs
  • Fencing issues can occur
  • Failed instances can block partitions
  • Hard to operate & debug

✅ Works ❌ Complex ❌ Not recommended for most systems

The Outbox Pattern is simply better.

Does This Work in Multi-Instance Setups?

Here is the honest answer:

Pattern Multi-instance safe? Recommended TransactionalEventListener ❌ (local only) For internal only Kafka Transactions ⚠️ (fragile) Rarely Outbox Pattern ✅✅✅ BEST Debezium (CDC) ✅✅✅✅✅ For scale

If you run:

  • Multiple pods in Kubernetes
  • Docker Swarm
  • AWS ECS / EKS
  • Multiple Spring Boot nodes

You want the Outbox pattern.

Final Architecture (Production Ready)

[ Spring Boot — Multiple Instances ]
     |
     |   ( single transaction )
     V
[ Database + Outbox Table ]
     |
     |   (Publisher / CDC / Poller)
     V
[ Kafka / SNS / SQS ]
     |
     V
[ Downstream Services ]

This architecture guarantees:

✅ Strong consistency ✅ No data loss ✅ No double sending ✅ Multi-instance safety ✅ Fully scalable

Perfect for:

  • Microservices
  • Event-driven systems
  • Payment systems
  • Airline / booking / fintech systems

Conclusion

If you’re using Spring Boot and care about consistency in distributed systems:

Use the Transactional Outbox pattern.

Not only is it the safest solution — it’s the same pattern used by the biggest technology companies in the world.

If you are serious about production-grade architecture in Spring, this is a must-have in your toolbox.

If you’d like, next I can provide you with:

✅ Full Spring Boot code ✅ Outbox entity & schema ✅ Multi-instance safe scheduler ✅ Kafka & AWS SQS versions ✅ Architecture diagram image for Medium

Just reply with: “Give me the code version”


메타데이터
post_id
7eaac2b6894b
slug
transactional-pub-sub-in-spring-for-multi-instance-production-grade-systems-7eaac2b6894b
url
https://medium.com/@mtilab/transactional-pub-sub-in-spring-for-multi-instance-production-grade-systems-7eaac2b6894b
canonical_url
https://medium.com/@mtilab/transactional-pub-sub-in-spring-for-multi-instance-production-grade-systems-7eaac2b6894b
author_url
https://medium.com/@mtilab
status
ok
fetched_at
2026-06-24 04:09:36