Implementing the Saga pattern in Spring Boot microservices involves coordinating local transactions across services using events, with compensating actions for failures, as shown in a banking funds transfer scenario behind Kong Gateway. This approach ensures eventual consistency in distributed systems without two-phase commit.
Banking Scenario: Funds Transfer Saga
In a large banking application, a funds transfer spans Account Debit, Compliance Check, Account Credit, and Notification services. The client calls POST /transfers via Kong Gateway, which routes to the Transfer Orchestrator service (built in Spring Boot). This starts the Saga: debit source account → check compliance → credit destination → notify customer. Failures trigger compensations like crediting back the source.
Tech Stack Overview
- Spring Boot 3.x with Spring Cloud Stream for Kafka integration and Spring Data JPA for per-service databases.
- Kong Gateway as API Gateway for routing, rate limiting, and auth on all Saga APIs.
- Kafka for asynchronous events (e.g.,
TransferDebited,CompliancePassed). - PostgreSQL instances per service for local transactions.
- Saga Orchestrator embedded in Transfer Service using state machine pattern.
Step 1: Kong Gateway Setup
Deploy Kong with Docker and configure services/routes for microservices running on ports 8081-8084.
curl -X POST http://kong:8001/services \
--data "name=transfer-service" \
--data "url=http://transfer-service:8081"
curl -X POST http://kong:8001/services/transfer-service/routes \
--data "paths[]=/transfers"BashRepeat foraccount-debit,compliance,account-credit,notificationservices. Kong handles mTLS, JWT auth, and traffic splitting.
Step 2: Shared Event DTOs
Define common events across services (in a shared Maven module).
public record TransferRequestedEvent(String transferId, String fromAccount, String toAccount, BigDecimal amount) {
}
public record TransferDebitedEvent(String transferId, boolean success, String reason) {
}
// Similar for CompliancePassedEvent, TransferCreditedEvent, etc.Java
Step 3: Transfer Orchestrator Service (Spring Boot)
pom.xml dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>XMLSaga State Entity:
public class TransferSaga {
@Id private String transferId;
private SagaState state = SagaState.STARTED; // PENDING, DEBITED, COMPLIANCE_OK, etc.
private String failureReason;
}
JavaREST Controller (exposed via Kong):
@RequestMapping("/transfers")
public class TransferController {
@Autowired private KafkaTemplate<String, Object> kafkaTemplate;
@PostMapping
public ResponseEntity<String> initiateTransfer(@RequestBody TransferRequest request) {
String transferId = UUID.randomUUID().toString();
// Persist initial saga state
saveSagaState(transferId, SagaState.STARTED);
// Publish first event
kafkaTemplate.send("transfer-events", transferId,
new TransferRequestedEvent(transferId, request.from(),
request.to(), request.amount()));
return ResponseEntity.accepted().body(transferId);
}
}JavaEvent Consumers (Orchestrator Logic):
public void handleDebitResult(TransferDebitedEvent event) {
if (event.success()) {
updateSagaState(event.transferId(), SagaState.DEBITED);
kafkaTemplate.send("transfer-events", event.transferId(),
new ComplianceCheckRequestedEvent(event.transferId()));
} else {
compensateAndFail(event.transferId(), event.reason());
}
}JavaStep 4: Account Debit Service
Consumes TransferRequestedEvent, debits locally, publishes TransferDebitedEvent.
public class AccountDebitService {
@Transactional
public void debitAccount(TransferRequestedEvent event) {
// Local ACID transaction: debit account
accountRepository.debit(event.fromAccount(), event.amount());
kafkaTemplate.send("transfer-events", event.transferId(),
new TransferDebitedEvent(event.transferId(), true, null));
}
// Compensation: credit back on saga failure event
@Transactional
public void compensateCreditBack(TransferFailedEvent event) {
accountRepository.credit(event.fromAccount(), event.amount());
}
}
JavaStep 5: Other Services
- Compliance Service: Validates AML/KYC rules, publishes
CompliancePassedEvent. - Account Credit Service: Credits destination, publishes
TransferCreditedEvent. - Notification Service: Sends email/SMS on success/failure.
Saga Flowchart
The diagram below shows the orchestrated Saga flow routed through Kong Gateway.(see the generated image above)
Client requests hit Kong → Transfer Orchestrator starts Saga → Kafka events trigger each service → Compensations on failure paths.
Running the System
- Start Kafka, Postgres (one per service), Kong.
mvn spring-boot:runfor each service.curl -X POST http://kong:8000/transfers -d '{"from":"ACC001","to":"ACC002","amount":100}'- Monitor Kafka topics and saga states for progress/failures.
This implementation scales to banking volumes with Kong handling millions of API calls and Kafka ensuring at-least-once delivery for Saga events.
My previous experience
In my previous role on the platform team at one of Australia’s major banks, APIs were developed following the database-per-service pattern, where each Spring Boot microservice owned its dedicated MongoDB instance to enforce strict data isolation and local transaction boundaries—crucial for Saga participants like account debits or compliance checks. This meant the Transfer Orchestrator service persisted Saga states in its own MongoDB collection using Spring Data MongoDB repositories, while the Account Debit service handled debits against a separate MongoDB with account balances, avoiding cross-service queries that could introduce coupling or contention. During development, teams leveraged Spring Boot’s @Document annotations and reactive MongoDB drivers for high-throughput banking workloads, ensuring that even compensating actions (like crediting back a debited amount) remained fully local and ACID-compliant within the service, which made Saga debugging far more predictable in production incidents.
Conclusion
Wrapping up this deep dive into implementing the Saga pattern with Spring Boot for banking microservices, you’ve now got a battle-tested blueprint: Kong Gateway routing resilient APIs, Kafka-orchestrated events across database-per-service boundaries, and compensating logic to ensure eventual consistency in high-stakes funds transfers. This isn’t theoretical—it’s the kind of production-grade setup that scales to millions of transactions while keeping distributed failures manageable.
Master these fundamentals by connecting the dots with prior posts: start with Mastering the API Gateway Pattern: Comprehensive 2025 guide for Kong-like gateway mastery, layer in Microservices: Database per service with Spring Boot & MongoDB for the isolation powering Saga participants, and explore CQRS extensions in How to Build a CQRS‑Based CRUD API with FastAPI and MongoDB for polyglot evolutions. Deploy this stack today, monitor your first Saga in action, and level up your distributed systems game.
0 Comments