Skip to the content.

Handling failure & rollback when multiple microservices are involved.


1️⃣ Concept: Why it’s hard?

In monolith: Single DB transaction → rollback is easy (ACID).

In microservices: Each service has its own DB → No global DB transaction → Rollback is hard → We need Distributed Transaction Pattern.

🛠️ Industry solutions

✅ Saga Pattern (Most common) — recommended for your answer ✅ 2PC (Two-phase commit) — theoretical, but impractical in microservices (avoid suggesting it)


2️⃣ What to say (Your project - Real Example)

“In our project, we follow the Saga pattern using Kafka to manage distributed transactions. Let me take an example: Suppose we have an Order Service, Payment Service, and Inventory Service working together in a transaction.”

✅ Steps

Step Action
1 Order Service creates an order (status = PENDING)
2 Publishes OrderCreated event to Kafka
3 Inventory Service reserves items, publishes InventoryReserved
4 Payment Service deducts payment, publishes PaymentCompleted
5 Order Service receives all success → marks order COMPLETED

❌ If failure happens? (say Payment fails)

Step Action
Payment fails Publishes PaymentFailed event
Inventory Service Consumes event → releases reserved inventory
Order Service Cancels order (status = CANCELLED)

This is called a Compensating Transaction → reverse the earlier steps.


3️⃣ Words you should confidently say in interview

Here’s an exact sentence you can memorize and say:

“We handle such failures using the Saga pattern with Kafka. Each service publishes events and performs local transactions. If any step fails, compensating actions are triggered to undo previous steps and bring the system to a consistent state. For example, if payment fails, inventory is released and the order is cancelled. This ensures eventual consistency without needing distributed locks or global transactions.”


🚀 Bonus (Advanced)

We also ensure idempotency (via unique transaction IDs) so retrying compensating actions doesn’t cause duplicate processing.


Summary to say in one line

💡 “We use Saga pattern + Kafka + Compensating transactions to rollback and ensure eventual consistency in distributed workflows.”


(Order → Inventory → Payment → Rollback) is Saga pattern — and there are two styles of implementing Saga:


Saga Type Orchestration Choreography
Who controls the flow? Central controller (Orchestrator service) No central controller — services react to events
Communication Orchestrator calls services directly (via REST or messaging) Services emit/consume events via broker (like Kafka)
Example flow Orchestrator → Inventory → Payment → Order → Compensation Order emits OrderCreated → Inventory listens → emits InventoryReserved → Payment listens → emits PaymentCompleted
Complexity Simpler to understand (central brain) Fully decoupled but harder to debug
Tools Camunda, Temporal, Axon Kafka, RabbitMQ, EventBridge

🚀 Bonus Tip (Interview)

If interviewer asks:

Q — When would you choose Orchestration vs Choreography? Say this:


✅ How Orchestrator works (Saga Pattern - Orchestration)

👉 There is 1 central Orchestrator service 👉 It talks to other services (Order, Payment, Inventory, etc.) 👉 It decides the next step and handles failures

Think of it as:

Orchestrator Service → Calls Order Service
                     → Then calls Payment Service
                     → Then calls Inventory Service

If any step fails → it triggers compensating transactions.


📊 Example Scenario

We have:


1️⃣ Architecture Diagram

Client → Orchestrator
            |
            ↓
    Order Service → Payment Service → Inventory Service

2️⃣ Spring Boot Example (Code)

🛠️ OrchestratorController.java

@RestController
@RequestMapping("/orchestrator")
public class OrchestratorController {

    @Autowired
    private OrchestratorService orchestratorService;

    @PostMapping("/createOrder")
    public ResponseEntity<String> createOrder(@RequestBody OrderRequest request) {
        orchestratorService.startOrderWorkflow(request);
        return ResponseEntity.ok("Order process started");
    }
}

🛠️ OrchestratorService.java

@Service
public class OrchestratorService {

    @Autowired
    private RestTemplate restTemplate;  // Simple for demo (or FeignClient)

    public void startOrderWorkflow(OrderRequest request) {
        try {
            // 1. Call Order Service
            String orderResponse = restTemplate.postForObject("http://ORDER-SERVICE/api/order", request, String.class);

            // 2. Call Payment Service
            String paymentResponse = restTemplate.postForObject("http://PAYMENT-SERVICE/api/payment", request, String.class);

            // 3. Call Inventory Service
            String inventoryResponse = restTemplate.postForObject("http://INVENTORY-SERVICE/api/inventory", request, String.class);

            System.out.println("All steps completed successfully");

        } catch (Exception ex) {
            // If any failure happens → trigger compensating actions
            compensate(request);
        }
    }

    private void compensate(OrderRequest request) {
        // Example compensating transactions
        restTemplate.postForObject("http://ORDER-SERVICE/api/order/cancel", request, String.class);
        restTemplate.postForObject("http://PAYMENT-SERVICE/api/payment/refund", request, String.class);
        restTemplate.postForObject("http://INVENTORY-SERVICE/api/inventory/release", request, String.class);

        System.out.println("Compensating transactions executed due to failure");
    }
}

3️⃣ Each service should expose APIs like

Order Service:

@PostMapping("/api/order/cancel")
public ResponseEntity<String> cancelOrder(@RequestBody OrderRequest request) {
    // Cancel order logic here
    return ResponseEntity.ok("Order cancelled");
}

Payment Service:

@PostMapping("/api/payment/refund")
public ResponseEntity<String> refundPayment(@RequestBody OrderRequest request) {
    // Refund logic here
    return ResponseEntity.ok("Payment refunded");
}

Inventory Service:

@PostMapping("/api/inventory/release")
public ResponseEntity<String> releaseInventory(@RequestBody OrderRequest request) {
    // Release reserved items logic here
    return ResponseEntity.ok("Inventory released");
}

✅ Key Features of Orchestration (to SAY in Interview)

“In Orchestration-based Saga, one central service (the Orchestrator) manages the workflow and calls other services synchronously/asynchronously. It controls the sequence of steps and triggers compensating transactions on failure.”


🚀 Advanced (Real-world)

In real systems → Instead of RestTemplate, we often use: ✅ Spring Cloud OpenFeign → For service-to-service calls ✅ Resilience4j Circuit Breaker → For fault tolerance ✅ Distributed Tracing (Zipkin) → For observability


🥇 Your Project Mapping

If in your project, you want to convert Choreography to Orchestration → You can create one Orchestrator service and call


Perfect — now let’s make it production grade, like what interviewers expect when they ask “Have you handled distributed transactions, failure, retries, timeouts etc?” We will upgrade to:

✅ Feign Client (instead of RestTemplate) ✅ Resilience4j Circuit Breaker + Retry (for fault tolerance) ✅ Clean Code Structure


🛠️ Spring Boot Orchestrator with Feign + Resilience4j (Advanced)


1️⃣ pom.xml dependencies

<!-- OpenFeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- Resilience4j (Circuit Breaker + Retry) -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

In application.yml, enable Feign + CircuitBreaker support

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true

2️⃣ Feign Clients (Clean service calls)

🛠️ OrderClient.java

@FeignClient(name = "order-service", url = "http://ORDER-SERVICE/api/order")
public interface OrderClient {

    @PostMapping
    String createOrder(@RequestBody OrderRequest request);

    @PostMapping("/cancel")
    String cancelOrder(@RequestBody OrderRequest request);
}

🛠️ PaymentClient.java

@FeignClient(name = "payment-service", url = "http://PAYMENT-SERVICE/api/payment")
public interface PaymentClient {

    @PostMapping
    String makePayment(@RequestBody OrderRequest request);

    @PostMapping("/refund")
    String refundPayment(@RequestBody OrderRequest request);
}

🛠️ InventoryClient.java

@FeignClient(name = "inventory-service", url = "http://INVENTORY-SERVICE/api/inventory")
public interface InventoryClient {

    @PostMapping
    String reserveInventory(@RequestBody OrderRequest request);

    @PostMapping("/release")
    String releaseInventory(@RequestBody OrderRequest request);
}

3️⃣ OrchestratorService.java (With Circuit Breaker)

@Service
@RequiredArgsConstructor
public class OrchestratorService {

    private final OrderClient orderClient;
    private final PaymentClient paymentClient;
    private final InventoryClient inventoryClient;

    @CircuitBreaker(name = "orderService", fallbackMethod = "orderFallback")
    @Retry(name = "orderService")
    public String startOrderWorkflow(OrderRequest request) {
        try {
            // 1. Order Creation
            orderClient.createOrder(request);

            // 2. Payment
            paymentClient.makePayment(request);

            // 3. Inventory
            inventoryClient.reserveInventory(request);

            return "Order Workflow Completed Successfully";

        } catch (Exception e) {
            compensate(request);
            throw new RuntimeException("Workflow failed, compensation triggered");
        }
    }

    // Compensation logic
    private void compensate(OrderRequest request) {
        try { orderClient.cancelOrder(request); } catch (Exception ignored) {}
        try { paymentClient.refundPayment(request); } catch (Exception ignored) {}
        try { inventoryClient.releaseInventory(request); } catch (Exception ignored) {}

        System.out.println("Compensating transactions done.");
    }

    // Circuit breaker fallback method
    public String orderFallback(OrderRequest request, Throwable t) {
        compensate(request);
        return "Fallback triggered due to: " + t.getMessage();
    }
}

4️⃣ application.yml (Resilience4j settings)

resilience4j:
  circuitbreaker:
    instances:
      orderService:
        slidingWindowSize: 5
        failureRateThreshold: 50
        waitDurationInOpenState: 5s

  retry:
    instances:
      orderService:
        maxAttempts: 3
        waitDuration: 2s

📝 What to SAY in interview (with confidence)

“In my project, we followed the Saga Pattern (Orchestration). Our Orchestrator service coordinates workflows across microservices (Order, Payment, Inventory). We used FeignClient for clean service-to-service communication, Resilience4j Circuit Breaker + Retry for fault tolerance, and compensating transactions to maintain consistency in case of failures. We also integrated distributed tracing for observability.”


🚀 Pro Tip for Interview (Your Project)

You can say confidently:

✅ “In our system, similar to our payment/account services, we use Kafka, REST APIs for communication, and we deploy microservices independently using Kubernetes GitOps. For complex transactions, we use Saga Pattern with compensating transactions to ensure eventual consistency.”


Let’s break this clearly and cleanly so that in the interview — you can explain it in 1 min confidently.


❓ What is a Compensating Transaction?

➡️ It is an operation that UNDOES the effect of a previously successful operation ➡️ We use it when we cannot use database rollback (ACID transaction) because we are dealing with distributed microservices


⚡ Why do we need it?

In microservices, each service has its own DB You can’t rollback all DBs together like a normal SQL transaction.

So what do we do? → We COMPENSATE


⚙️ Real-world example (Your project relatable)

Let’s say: 1️⃣ You created an Order 2️⃣ You made a Payment 3️⃣ You reserved Inventory

But Inventory reservation failed ❌

Now we need to “undo” → ✅ Refund the Payment ✅ Cancel the Order

These undo actions are called Compensating Transactions


🛠️ In Your Project Terms

You can say in interview:

“In our project, when we have services like payment, account, checkout — if one step fails (for example payment success but inventory fails), we trigger compensating transactions like refundPayment(), cancelOrder(), releaseInventory() — so system comes back to original state. This is how we maintain consistency across services using Saga Pattern.”


💡 Typical Compensating Transaction Examples

Service Normal Action Compensating Action
Order Service createOrder() cancelOrder()
Payment Service makePayment() refundPayment()
Inventory Service reserveInventory() releaseInventory()

📝 Interview one-liner (memorize)

“Compensating Transactions are explicit undo actions we call to bring system back to consistent state when a distributed transaction partially fails — because microservices can’t share DB transactions.”


❓ What if Compensating Transaction ALSO fails?

➡️ Yes — compensating APIs can also fail. Example: refundPayment() fails because payment gateway is down.

In such cases: We don’t panic — we ensure 3 things:


✅ 1. Retry Mechanism

We retry the compensation API (with exponential backoff). Because many failures are transient (temporary network, service downtime)

Spring Boot example (using @Retryable)

@Retryable(value = { RemoteServiceNotAvailableException.class }, maxAttempts = 3, backoff = @Backoff(delay = 2000))
public void refundPayment(String paymentId) {
    // call refund API
}

✅ 2. Manual Intervention (Dead Letter Queue / Manual Review)

If retry also fails → We record the failed compensation in a Dead Letter Queue (DLQ) or a Failed_Compensation Table Ops team can later manually fix those few rare cases.

Your project uses Kafka — So you can say:

“We can send failed compensation messages to a Dead Letter Kafka topic — so that ops can replay or handle manually later”


✅ 3. Idempotency

Even if we retry multiple times → We make sure the compensation action happens at most ONCE (no double refund)

In your Spring Boot services → This means refundPayment() API should be idempotent


📌 Interview GOLDEN answer (memorize this 👇)

“In case compensating APIs fail, we use Retry with exponential backoff, and if still fails, log it into Dead Letter Queue or Compensation table for manual intervention. Also, all compensating actions are idempotent to avoid side effects.”


🛠️ In Your Project (Your words)

“Since we have Kafka and elastic logs — we can push failed compensations to Kafka DLQ and monitor logs in Elastic. Our APIs are idempotent (e.g., refundPayment() does not refund twice even if retried). We ensure eventual consistency and operational visibility.”


⚡ Final Notes

Failure Scenario Our Strategy
Temporary failure Retry
Permanent failure Log to DLQ / Manual fix
Avoid duplicates Idempotent API

✅ 1. Normal flow of compensation ✅ 2. Retry ✅ 3. DLQ handling ✅ 4. Idempotency


📂 Example Scenario

Service: PaymentService Compensation action: refundPayment(paymentId)

Let’s say we trigger this via Kafka compensation topic Topic name: compensate-payment


⚙️ 1. Kafka Consumer for Compensation

@Component
public class PaymentCompensationListener {

    @Autowired
    private PaymentService paymentService;

    @KafkaListener(topics = "compensate-payment", groupId = "payment-group")
    public void handleCompensatePayment(String paymentId) {
        paymentService.refundPayment(paymentId);
    }
}

⚙️ 2. PaymentService with Retry + Idempotency

@Service
public class PaymentService {

    @Autowired
    private PaymentRepository paymentRepository;  // db table with payment + refund status

    @Retryable(value = {RemoteServiceNotAvailableException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000))
    public void refundPayment(String paymentId) {
        Payment payment = paymentRepository.findById(paymentId)
                                           .orElseThrow(() -> new IllegalArgumentException("Payment not found"));

        // ✅ IDEMPOTENCY check — already refunded?
        if (payment.isRefunded()) {
            System.out.println("Payment already refunded. Skipping...");
            return;
        }

        // Call external payment gateway refund API
        boolean success = externalRefundAPI(paymentId);

        if (success) {
            payment.setRefunded(true);
            paymentRepository.save(payment);
        } else {
            throw new RemoteServiceNotAvailableException("Refund API failed");
        }
    }

    private boolean externalRefundAPI(String paymentId) {
        // simulate external API call
        return Math.random() > 0.2; // 80% success, 20% fail
    }
}

⚙️ 3. Kafka Dead Letter Queue (DLQ)

If all retries fail → Spring Kafka can automatically send message to DLQ topic

Configure in application.yml:

spring:
  kafka:
    consumer:
      enable-auto-commit: false
    listener:
      ack-mode: record
      retry:
        topics:
          - topic: compensate-payment
            dlt-topic-suffix: ".DLT"
            max-attempts: 3

This creates compensate-payment.DLT → all failed compensations go there automatically.


⚙️ 4. DLQ Listener (For Ops / Manual Fix)

@Component
public class DLQListener {

    @KafkaListener(topics = "compensate-payment.DLT", groupId = "payment-dlq-group")
    public void handleFailedCompensation(String paymentId) {
        // Log to Elastic, alert ops
        System.out.println("Compensation FAILED for paymentId = " + paymentId);
        // push to Elastic, or create JIRA, or alert team etc
    }
}

🏆 In Interview (Your answer)

“In our project, for compensations like refundPayment(), we have Kafka based listeners. We retry 3 times with exponential backoff. If all retries fail, message is pushed to Kafka Dead Letter topic (DLQ). Our DLQ listener logs to Elastic for ops visibility. Also, refundPayment() is idempotent — so even if retried, no duplicate refunds happen.”


📊 Architecture Diagram (Clean)

compensate-payment topic
          |
          v
 PaymentCompensationListener
          |
    refundPayment()
       /     \
 (Success)   (Fail 3x)
     |           |
     v           v
 Done      compensate-payment.DLT
                   |
              DLQListener → Elastic Logs

⚡️ Summary

Strategy Implementation
Retry @Retryable
Idempotent Check DB before refund
DLQ Kafka auto DLT
Ops visibility DLQ Listener + Elastic

🏦 Real-world Payment Compensation Scenarios

Scenario Example How we handle
✅ Compensation Success Refund succeeded in PhonePe / GPay Mark refunded in DB
❌ Compensation Failure (known) PhonePe says: Refund failed Retry, else push to DLQ, log for manual ops
❓ Uncertain / Data Loss / Timeout / Unknown status PhonePe hangs (waiting mode) / no response / timeout Retry, audit log, mark as PENDING_MANUAL_REVIEW, ops intervene

🛠️ Let’s put it in your project context

Scenario 1

PaymentService → tries refund API → PhonePe says “Refund failed” → We retry → still fails → We push to DLQ → Log to Elastic → Ops manually handles → Maybe raise support ticket to PhonePe


Scenario 2

PaymentService → tries refund API → PhonePe hangs (timeout) → We don’t know success/fail → After retry + timeout, mark status = “REFUND_PENDING_MANUAL_REVIEW” in DB → Push to DLQ → Ops gets alert and manually reconciles


⚡️ What happens in PhonePe/GPay real life?

They too have automatic reconciliation jobs If transaction is stuck/unknown → They tell merchant (us) to check via status API after X mins → Or in worst case, merchant (we) refunds from our wallet manually


✍️ What to say in Interview? (Sample Answer)

“In our project, when compensating transactions (like payment refunds) fail — we first retry. If retries exhaust, we push the event to Kafka DLQ and log to Elastic. Our DLQ listener marks such transaction as PENDING_MANUAL_REVIEW and alerts Ops team. They manually reconcile via payment gateway dashboard or support. This ensures eventual consistency and no money is stuck silently.”


📌 Advanced Concepts you can mention

Concept When to mention
Idempotency “Even if PhonePe retries, same refund is safe”
Outbox pattern “For reliable message delivery to Kafka even if DB txn fails”
Retry + Backoff + DLQ “To gracefully degrade on failure”
Eventual Consistency “We accept temporary inconsistency, ops reconciles later”
Audit Logs “We log every state transition for full traceability”

🏆 Key takeaway for you (and interview)

“In distributed systems, we can’t guarantee 100% rollback instantly everywhere — so we mix retries, DLQ, manual intervention and eventual consistency to maintain system correctness.”