Spring Boot Transaction Management
A practical guide to @Transactional, database consistency, Kafka event publishing,Redis and reliable transaction patterns in microservices

1. Introduction
Transaction management is one of the most critical aspects of building reliable Spring Boot applications. In a simple monolithic application, managing a transaction can appear straightforward: execute a few database operations inside a @Transactional method and either commit everything or roll everything back.
However, modern enterprise applications rarely depend on a single database.
A typical Spring Boot microservice may interact with multiple infrastructure components:
Spring Boot Service
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
PostgreSQL Redis Kafka
Persistent Cache Events
Data State MessagingNow consider a simple business operation such as placing an order:
Client
│
▼
Order Service
│
├── Create Order ───────────► PostgreSQL
│
├── Reserve Inventory ──────► Inventory Service
│
├── Process Payment ─────────► Payment Service
│
├── Update Cache ────────────► Redis
│
└── Publish Event ───────────► KafkaWhat happens if the order is successfully stored in PostgreSQL, but Kafka fails to publish the event?
Or if the payment succeeds but inventory reservation fails?
Or if PostgreSQL commits successfully but Redis still contains stale data?
This is where transaction management becomes significantly more complicated.
2. What Does a Transaction Actually Guarantee?
ACID
Atomicity
Consistency
Isolation
Durability
What Spring manages vs what the database manages
3. Understanding @Transactional
@Transactional
public void processOrder() {
saveOrder();
updateInventory();
}
Explain:
Transaction boundary
Commit
Rollback
Proxy mechanism
Runtime vs checked exceptions
4. Transaction Propagation
In Spring Boot, transaction propagation defines what happens when a transactional method calls another transactional method.
The key question is:
If method A is already running inside a transaction and calls method B, should B join A's transaction, create a new transaction, run without a transaction, or behave differently?
| Propagation | Existing transaction? | Behavior |
| --------------- | --------------------- | ----------------------------- |
| `REQUIRED` | Yes | Join existing (By Default) |
| `REQUIRED` | No | Create new |
| `REQUIRES_NEW` | Yes | Suspend existing + create new |
| `SUPPORTS` | Yes | Join existing |
| `SUPPORTS` | No | Run without transaction |
| `MANDATORY` | Yes | Join existing |
| `MANDATORY` | No | Throw exception |
| `NOT_SUPPORTED` | Yes | Suspend existing |
| `NOT_SUPPORTED` | No | Run without transaction |
| `NEVER` | Yes | Throw exception |
| `NEVER` | No | Run without transaction |
| `NESTED` | Yes | Nested transaction/savepoint |
| `NESTED` | No | Create transaction |
| --------------- | --------------------- | ----------------------------- |
Especially explain:
@Transactional
public void placeBet() {
walletService.debit();
auditService.saveAudit();
}
T1: placeBet
│
├── wallet.debit()
│
├── suspend T1
│
├── T2: saveAudit()
│ │
│ └── COMMIT
│
├── resume T1
│
└── COMMIT / ROLLBACK
So T2 is independent.5. Transaction Isolation
Explain:
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
Then demonstrate:
Transaction A
│
├── Read
│
│ Transaction B
│ │
│ └── Update
│
└── Read again
Cover:
Dirty read
Non-repeatable read
Phantom read
Lost update
6. Spring Boot + PostgreSQL Transaction Example
Build a realistic example:
POST /orders
│
▼
OrderService
│
├── Create Order
├── Reserve Inventory
└── Create Payment Transaction
│
▼
COMMIT
Show the actual Spring Boot implementation.
7. Transaction Management with Kafka
This should be one of the main sections.
Explain the problem:
DB Transaction
│
▼
Save Order
│
▼
Kafka Publish
What happens if:
DB COMMIT succeeds
Kafka publish fails
or:
Kafka publish succeeds
DB COMMIT fails
This creates inconsistency.
Explain Kafka transaction support
Spring Boot
│
├── Database
│
└── Kafka
Discuss:
Kafka producer transactions
KafkaTransactionManager@Transactionaltransactional.idExactly-once semantics
Consumer transactions
Offset management
Then explain an important limitation:
A Kafka transaction and a PostgreSQL transaction are not automatically one atomic transaction.
This leads naturally into the Outbox Pattern.
8. The Outbox Pattern
This should be a major practical section.
Instead of:
DB
│
└──► Kafka
use:
PostgreSQL
│
┌─────────┴─────────┐
▼ ▼
Business Outbox
Data Event
│ │
└──── Same DB TX ───┘
│
▼
Outbox Publisher
│
▼
Kafka
Example:
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(...);
outboxRepository.save(
new OutboxEvent(
order.getId(),
"ORDER_CREATED"
)
);
}
Then a publisher sends the outbox event to Kafka.
Explain:
Why it works
Retry
Idempotency
Duplicate events
Polling publisher
CDC/Debezium alternative
9. Redis and Transactions
This is another area where developers commonly make mistakes.
Explain why this is dangerous:
PostgreSQL Transaction
│
▼
Update DB
│
▼
Update Redis
If Redis fails after DB commit:
Database = NEW
Redis = OLD
Explain Redis's own transaction mechanisms:
MULTI
EXEC
WATCH
But make the distinction:
Redis transactions do not automatically participate in your PostgreSQL
@Transactionaltransaction.
10. Cache-Aside Transaction Pattern
Show the recommended pattern:
Request
│
▼
PostgreSQL
│
COMMIT
│
▼
Invalidate Cache
│
▼
Redis
Example:
@Transactional
public void updateProduct(Product product) {
repository.save(product);
}
Then after successful commit:
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT
)
public void evictCache(ProductUpdatedEvent event) {
redis.delete(event.productId());
}
This is a very useful production pattern.
11. Distributed Transactions
Explain the problem with:
Service A
│
▼
Service B
│
▼
Service C
Each service owns its own database.
You cannot simply assume:
@Transactional
will roll back changes made in another service.
Explain:
Option 1 — Two-Phase Commit
Coordinator
│
├── Service A
├── Service B
└── Service C
Explain:
Prepare
Commit
Rollback
Advantages
Performance
Availability concerns
Option 2 — Saga Pattern
Order Service
│
▼
Payment Service
│
▼
Inventory Service
If Inventory fails:
Inventory FAILED
│
▼
Compensating Action
│
▼
Refund Payment
│
▼
Cancel Order
For modern microservices, this deserves significant attention.
12. Distributed Transaction Example
Use a realistic e-commerce example:
Order Service
│
├── Create Order
│
▼
Payment Service
│
├── Charge Payment
│
▼
Inventory Service
│
└── Reserve Stock
Failure:
Order = CREATED
Payment = SUCCESS
Inventory = FAILED
Saga compensation:
Inventory FAILED
│
▼
Payment REFUND
│
▼
Order CANCELLED
This is much easier to understand than purely theoretical distributed transaction examples.
13. Common @Transactional Mistakes
This section can be particularly valuable.
Mistake 1 — Self Invocation
public void methodA() {
methodB();
}
@Transactional
public void methodB() {
}
methodB() may not pass through the Spring proxy.
Mistake 2 — Private Transactional Methods
@Transactional
private void process() {
}
Don't expect normal Spring proxy-based transaction behavior here.
Mistake 3 — Catching Exceptions
@Transactional
public void process() {
try {
save();
} catch (Exception e) {
log.error("Failed", e);
}
}
The exception is swallowed, so rollback behavior may not be what you expect.
Mistake 4 — Long Transactions
Avoid:
BEGIN
│
├── DB
├── HTTP API
├── Kafka
├── External Payment
├── Complex Processing
│
COMMIT
External calls inside DB transactions can hold locks/connections unnecessarily.
Mistake 5 — Assuming @Transactional Covers Kafka
It doesn't automatically make PostgreSQL + Kafka one atomic transaction.
Mistake 6 — Assuming Redis Rolls Back
Redis changes don't automatically roll back when PostgreSQL rolls back.
Mistake 7 — Wrong Transaction Boundary
Put transactions around the business operation, not arbitrary repository calls.
Mistake 8 — Calling External APIs Inside Transactions
@Transactional
public void payment() {
saveOrder();
paymentProvider.call(); // risky
updateOrder();
}
The external call may be slow or timeout while your DB transaction remains open.
14. Production Best Practices
This should be your strongest section.
Keep transactions short
BEGIN
│
├── Validate
├── DB operations
└── COMMIT
Avoid long-running business processes.
Define clear ownership
Each microservice should generally own its database.
Order Service → Order DB
Payment Service → Payment DB
Inventory Service → Inventory DB
Prefer Outbox for DB → Kafka
Business Data + Outbox
│
Same TX
│
▼
Kafka
Use idempotency
Every important distributed operation should have a unique business/transaction ID.
Use optimistic locking where appropriate
@Version
private Long version;
Use pessimistic locking selectively
Don't lock everything.
Avoid distributed locks unless necessary
Prefer database constraints and transactional design where possible.
Monitor transaction behavior
Monitor:
Transaction duration
DB connection pool
Lock waits
Deadlocks
Rollbacks
Kafka lag
Outbox backlog
Redis latency
15. Recommended Architecture
End the article with a complete architecture:
API
│
▼
┌───────────────┐
│ Spring Boot │
│ Service │
└───────┬───────┘
│
@Transactional
│
┌────────────┼────────────┐
▼ ▼ ▼
PostgreSQL Outbox Redis
│ │
│ Same TX
│ │
│ ▼
│ Kafka
│ │
│ ┌─────┼─────┐
│ ▼ ▼ ▼
│ Service Service Service
│ A B C
│
▼
Consistent State
And explain:
PostgreSQL provides local transactional consistency, Redis provides fast state/cache access, Kafka provides asynchronous event propagation, and the Outbox/Saga patterns provide reliable coordination across distributed services.
16. Conclusion
The key message of the article should be:
@Transactionalis powerful, but it is not a distributed transaction mechanism.
For production Spring Boot systems:
Local DB consistency
↓
@Transactional
↓
Concurrent update protection
↓
Optimistic/Pessimistic locking
↓
DB → Kafka reliability
↓
Outbox Pattern
↓
Service → Service consistency
↓
Saga Pattern
↓
Idempotency + Observability
Promote this post
Share & amplify
LinkedIn post generator
Generate a polished, professional LinkedIn post from this article. Edit before posting.

Comments (0)