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., TransferDebitedCompliancePassed).​
  • 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"
Bash

Repeat for account-debitcomplianceaccount-creditnotification services. 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>
XML

Saga State Entity:

public class TransferSaga {
    @Id private String transferId;
    private SagaState state = SagaState.STARTED; // PENDING, DEBITED, COMPLIANCE_OK, etc.
    private String failureReason;
}
Java




REST 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);
    }
}
Java




Event 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());
    }
}
Java




Step 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());
    }
}
Java

Step 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

  1. Start Kafka, Postgres (one per service), Kong.
  2. mvn spring-boot:run for each service.
  3. curl -X POST http://kong:8000/transfers -d '{"from":"ACC001","to":"ACC002","amount":100}'
  4. 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.

Categories: API

0 Comments

Leave a Reply

Verified by MonsterInsights