Question 3 · Section 17

How to implement distributed transactions in microservices

In microservices there is no single database, so regular transactions (ACID) do not work.

Language versions: English Russian Ukrainian

Junior Level

In microservices there is no single database, so regular transactions (ACID) do not work.

Two approaches:

  1. Saga — a sequence of local transactions with compensations
  2. Two-Phase Commit (2PC) — distributed transaction protocols (rarely used)

Saga is more popular than 2PC because it does not block resources for the entire transaction and works with heterogeneous databases, while 2PC requires two-phase commit support from all participants.

Product order:
1. Order Service: create order (local transaction)
2. Payment Service: charge money (local transaction)
3. Inventory Service: reserve product (local transaction)

If step 2 fails -> cancel order (compensation)

Goal: Eventual consistency — data is consistent, but not instantly.

When NOT to implement distributed transactions

If you can redesign so that one service owns all the data — avoid distributed transactions entirely. This is the most reliable solution.


Middle Level

Implementing Saga

1. Choreography (event-driven):

// Order Service
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    eventPublisher.publish(new OrderCreatedEvent(order.getId()));
}

// Payment Service
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
    try {
        paymentService.charge(event.orderId());
        eventPublisher.publish(new PaymentCompletedEvent(event.orderId()));
    } catch (Exception e) {
        eventPublisher.publish(new PaymentFailedEvent(event.orderId(), e));
    }
}

// Order Service — compensation
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
    orderService.cancelOrder(event.orderId());
    eventPublisher.publish(new OrderCancelledEvent(event.orderId()));
}

2. Orchestration (coordinator):

@Component
public class OrderOrchestrator {
    public void processOrder(Order order) {
        try {
            orderService.create(order);
            paymentService.charge(order);
            inventoryService.reserve(order);
        } catch (PaymentException e) {
            orderService.cancel(order);
            throw e;
        } catch (InventoryException e) {
            paymentService.refund(order);
            orderService.cancel(order);
            throw e;
        }
    }
}

Common mistakes

  1. Missing idempotency:
    Event delivered twice -> payment will be charged twice!
    Solution: check orderID before processing
    
  2. Lost events:
    Event publisher crashed -> event not delivered
    Solution: transactional outbox pattern
    

Senior Level

Transactional Outbox Pattern

Outbox — a special table in the same database where, in the same transaction as business data, an event is written. CDC (Change Data Capture, Debezium) reads the database logs and sends events to Kafka.

// Problem: how to guarantee event delivery?
// Solution: Outbox table in the same transaction

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    outboxRepository.save(new OutboxEvent(
        "OrderCreated", order.getId(), payload
    ));
    // Both are saved in the same transaction!
}

// CDC (Change Data Capture) or polling reads outbox
// and publishes to Kafka
@Component
public class OutboxPublisher {
    @Scheduled(fixedDelay = 1000)
    public void publishPending() {
        List<OutboxEvent> events = outboxRepository.findPending();
        for (OutboxEvent event : events) {
            kafkaTemplate.send(event.getTopic(), event.getPayload());
            outboxRepository.markPublished(event.getId());
        }
    }
}

Architectural Trade-offs

Approach Pros Cons
Saga Flexible, scalable Complex compensation
2PC Strict consistency Locks, slow
Eventual Simple No guarantees

Edge Cases

1. Concurrent Sagas:

Saga1: Order A -> reserve inventory item1
Saga2: Order B -> reserve inventory item1
Competition -> one of the orders will fail
Solution: optimistic/pessimistic locking

2. Saga timeout:

// Illustrative example (Axon Framework). Real API depends on the library.
// Protection against "hung" Sagas
@SagaConfiguration
public class OrderSagaConfig {
    @Bean
    public SagaConfiguration<OrderSaga> orderSaga() {
        return SagaConfiguration.sagaManagedBy(OrderSaga.class)
            .sagaTimeout(Duration.ofMinutes(5));
    }
}

Production Experience

Debezium CDC + Kafka:

Database -> Debezium -> Kafka -> Event Consumers

Outbox table -> Debezium CDC -> Kafka topic -> Saga handlers

This guarantees:
- At-least-once delivery
- Event ordering
- Recovery after failures

Best Practices

✅ Use Transactional Outbox
✅ Implement idempotency at each step
✅ Add Saga timeout
✅ Log every step
✅ Monitor Saga duration

❌ Don't use 2PC unless necessary
❌ Don't ignore concurrent Sagas
❌ Don't forget compensations

Interview Cheat Sheet

Must know:

  • Two main approaches: Saga and Two-Phase Commit (2PC)
  • Saga is more popular: does not block resources, works with different databases
  • Transactional Outbox guarantees event delivery (write in the same transaction as business data)
  • CDC (Debezium) reads outbox and publishes to Kafka
  • Idempotency is mandatory at every step
  • Concurrent Sagas require locking mechanisms
  • Saga timeout protects against hung transactions
  • Best approach — avoid distributed transactions through redesign

Common follow-up questions:

  • Why is Saga better than 2PC? 2PC blocks resources for the entire transaction and requires support from all participants. Saga is asynchronous and flexible.
  • How to guarantee event delivery? Transactional Outbox + CDC — writing the event in the same transaction as business data.
  • What is eventual consistency? Data will become consistent over time, but not instantly.
  • How to handle concurrent Sagas? Optimistic/pessimistic locking at the resource level.

Red flags (DO NOT say):

  • “2PC is the best solution for microservices” — no, too slow and blocking
  • “Outbox is not needed, Kafka guarantees delivery” — Kafka guarantees delivery, but not atomicity with the database
  • “Eventual consistency = data can be incorrect” — no, it will become correct
  • “Distributed transactions are not needed, everything can be in one service” — not always possible

Related topics:

  • [[1. What is Saga pattern and when to use it]]
  • [[2. What is the difference between choreography and orchestration in Saga]]
  • [[4. What are compensating transactions]]
  • [[13. What is Database per Service pattern]]
  • [[16. What is the difference between synchronous and asynchronous communication]]