Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot-saga-pattern/references/reference.md
2025-11-29 18:28:34 +08:00

31 KiB

Spring Boot SAGA Pattern - Reference Documentation

Table of Contents

  1. Saga Pattern Overview
  2. Choreography-Based Saga
  3. Orchestration-Based Saga
  4. Spring Boot Integration
  5. Saga Frameworks
  6. Event-Driven Architecture
  7. Compensating Transactions
  8. State Management
  9. Error Handling and Retry
  10. Testing Strategies

Saga Pattern Overview

Definition

A Saga is a sequence of local transactions where each transaction updates data within a single service. Each local transaction publishes an event or message that triggers the next local transaction in the saga. If a local transaction fails, the saga executes compensating transactions to undo the changes made by preceding transactions.

Key Characteristics

Distributed Transactions: Spans multiple microservices, each with its own database.

Local Transactions: Each service performs its own ACID transaction.

Event-Driven: Services communicate through events or commands.

Compensations: Rollback mechanism using compensating transactions.

Eventual Consistency: System reaches a consistent state over time.

Saga vs Two-Phase Commit (2PC)

Feature Saga Pattern Two-Phase Commit
Locking No distributed locks Requires locks during commit
Performance Better performance Performance bottleneck
Scalability Highly scalable Limited scalability
Complexity Business logic complexity Protocol complexity
Failure Handling Compensating transactions Automatic rollback
Isolation Lower isolation Full isolation
NoSQL Support Yes No
Microservices Fit Excellent Poor

ACID vs BASE

ACID (Traditional Databases):

  • Atomicity: All or nothing
  • Consistency: Valid state transitions
  • Isolation: Concurrent transactions don't interfere
  • Durability: Committed data persists

BASE (Saga Pattern):

  • Basically Available: System is available most of the time
  • Soft state: State may change over time
  • Eventual consistency: System becomes consistent eventually

Choreography-Based Saga

Architecture

Each service produces and listens to events. Services know what to do when they receive an event.

Service A → Event → Service B → Event → Service C
    ↓                   ↓                   ↓
  Event              Event               Event
    ↓                   ↓                   ↓
Compensation    Compensation        Compensation

Event Flow

Success Flow:

  1. Order Service creates order → publishes OrderCreated event
  2. Payment Service listens → processes payment → publishes PaymentProcessed event
  3. Inventory Service listens → reserves inventory → publishes InventoryReserved event
  4. Shipment Service listens → prepares shipment → publishes ShipmentPrepared event

Failure Flow (Payment fails):

  1. Payment Service publishes PaymentFailed event
  2. Order Service listens → cancels order → publishes OrderCancelled event

Implementation Components

Event Publisher

@Component
public class OrderEventPublisher {
    private final StreamBridge streamBridge;
    
    public OrderEventPublisher(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }
    
    public void publishOrderCreatedEvent(String orderId, BigDecimal amount, String itemId) {
        OrderCreatedEvent event = new OrderCreatedEvent(orderId, amount, itemId);
        streamBridge.send("orderCreated-out-0", 
            MessageBuilder
                .withPayload(event)
                .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
                .build());
    }
}

Event Listener

@Component
public class PaymentEventListener {
    
    @Bean
    public Consumer<OrderCreatedEvent> handleOrderCreatedEvent() {
        return event -> processPayment(event.getOrderId());
    }
    
    private void processPayment(String orderId) {
        // Payment processing logic
    }
}

Event Classes

public record OrderCreatedEvent(
    String orderId,
    BigDecimal amount,
    String itemId
) {}

public record PaymentProcessedEvent(
    String paymentId,
    String orderId,
    String itemId
) {}

public record PaymentFailedEvent(
    String paymentId,
    String orderId,
    String itemId,
    String reason
) {}

Spring Cloud Stream Configuration

spring:
  cloud:
    stream:
      bindings:
        orderCreated-out-0:
          destination: order-events
        paymentProcessed-out-0:
          destination: payment-events
        paymentFailed-out-0:
          destination: payment-events
      kafka:
        binder:
          brokers: localhost:9092

Maven Dependencies

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

Gradle Dependencies

implementation 'org.springframework.cloud:spring-cloud-stream'
implementation 'org.springframework.cloud:spring-cloud-stream-binder-kafka'

Orchestration-Based Saga

Architecture

A central Saga Orchestrator coordinates the entire transaction flow, sending commands to services and handling responses.

         Saga Orchestrator
         /     |      \
    Service A  Service B  Service C

Orchestrator Responsibilities

  1. Command Dispatch: Sends commands to services
  2. Response Handling: Processes service responses
  3. State Management: Tracks saga execution state
  4. Compensation Coordination: Triggers compensating transactions on failure
  5. Timeout Management: Handles service timeouts
  6. Retry Logic: Manages retry attempts

Axon Framework Implementation

Saga Class

@Saga
public class OrderSaga {
    
    @Autowired
    private transient CommandGateway commandGateway;
    
    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        String paymentId = UUID.randomUUID().toString();
        ProcessPaymentCommand command = new ProcessPaymentCommand(
            paymentId, 
            event.getOrderId(), 
            event.getAmount(), 
            event.getItemId()
        );
        commandGateway.send(command);
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentProcessedEvent event) {
        ReserveInventoryCommand command = new ReserveInventoryCommand(
            event.getOrderId(), 
            event.getItemId()
        );
        commandGateway.send(command);
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentFailedEvent event) {
        CancelOrderCommand command = new CancelOrderCommand(event.getOrderId());
        commandGateway.send(command);
        end();
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(InventoryReservedEvent event) {
        PrepareShipmentCommand command = new PrepareShipmentCommand(
            event.getOrderId(), 
            event.getItemId()
        );
        commandGateway.send(command);
    }
    
    @EndSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCompletedEvent event) {
        // Saga completed successfully
    }
}

Aggregate for Order Service

@Aggregate
public class OrderAggregate {
    
    @AggregateIdentifier
    private String orderId;
    
    private OrderStatus status;
    
    public OrderAggregate() {
    }
    
    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        apply(new OrderCreatedEvent(
            command.getOrderId(), 
            command.getAmount(), 
            command.getItemId()
        ));
    }
    
    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        this.status = OrderStatus.PENDING;
    }
    
    @CommandHandler
    public void handle(CancelOrderCommand command) {
        apply(new OrderCancelledEvent(command.getOrderId()));
    }
    
    @EventSourcingHandler
    public void on(OrderCancelledEvent event) {
        this.status = OrderStatus.CANCELLED;
    }
}

Aggregate for Payment Service

@Aggregate
public class PaymentAggregate {
    
    @AggregateIdentifier
    private String paymentId;
    
    public PaymentAggregate() {
    }
    
    @CommandHandler
    public PaymentAggregate(ProcessPaymentCommand command) {
        this.paymentId = command.getPaymentId();
        
        if (command.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            apply(new PaymentFailedEvent(
                command.getPaymentId(),
                command.getOrderId(),
                command.getItemId(),
                "Payment amount must be greater than zero"
            ));
        } else {
            apply(new PaymentProcessedEvent(
                command.getPaymentId(),
                command.getOrderId(),
                command.getItemId()
            ));
        }
    }
}

Axon Configuration

axon:
  serializer:
    general: jackson
    events: jackson
    messages: jackson
  eventhandling:
    processors:
      order-processor:
        mode: tracking
        source: eventBus
  axonserver:
    enabled: false

Maven Dependencies for Axon

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.9.0</version>
</dependency>

Spring Boot Integration

Application Configuration

@SpringBootApplication
@EnableScheduling
public class SagaApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SagaApplication.class, args);
    }
}

Kafka Configuration

@Configuration
public class KafkaConfig {
    
    @Bean
    public NewTopic orderTopic() {
        return new NewTopic("order-events", 3, (short) 1);
    }
    
    @Bean
    public NewTopic paymentTopic() {
        return new NewTopic("payment-events", 3, (short) 1);
    }
    
    @Bean
    public NewTopic inventoryTopic() {
        return new NewTopic("inventory-events", 3, (short) 1);
    }
}

Properties Configuration

# Application
spring.application.name=saga-service

# Kafka
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=saga-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=*
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer

# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/sagadb
spring.datasource.username=saga
spring.datasource.password=saga
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# Actuator
management.endpoints.web.exposure.include=health,metrics,prometheus
management.endpoint.health.show-details=always

Saga Frameworks

Axon Framework

Type: Orchestration-based

Features:

  • Event sourcing support
  • CQRS pattern implementation
  • Saga state management
  • Automatic compensation
  • Built-in retry mechanisms

Use When:

  • Complex domain logic
  • Event sourcing is beneficial
  • CQRS pattern is needed
  • Mature framework is required

Eventuate Tram Sagas

Type: Orchestration-based

Features:

  • Database-per-service support
  • Transactional messaging
  • Saga orchestration DSL
  • Multiple messaging platforms

Use When:

  • Existing JPA-based services
  • Transactional outbox pattern needed
  • Multiple message brokers support required

Camunda

Type: BPMN-based orchestration

Features:

  • Visual workflow design
  • BPMN 2.0 standard
  • Human tasks support
  • Complex workflow modeling

Use When:

  • Business process modeling needed
  • Visual workflow design preferred
  • Human approval steps required
  • Complex orchestration logic

Apache Camel Saga EIP

Type: Enterprise Integration Pattern

Features:

  • Saga EIP implementation
  • Multiple protocol support
  • Route-based compensation
  • Integration with multiple systems

Use When:

  • Enterprise integration scenarios
  • Multiple protocol support needed
  • Existing Camel infrastructure

Event-Driven Architecture

Event Types

Domain Events: Represent business facts that happened

public record OrderCreatedEvent(
    String orderId,
    Instant createdAt,
    BigDecimal amount
) implements DomainEvent {}

Integration Events: Communication between bounded contexts

public record PaymentRequestedEvent(
    String orderId,
    String paymentId,
    BigDecimal amount
) implements IntegrationEvent {}

Command Events: Request for action

public record ProcessPaymentCommand(
    String paymentId,
    String orderId,
    BigDecimal amount
) {}

Event Versioning

public record OrderCreatedEventV1(
    String orderId,
    BigDecimal amount
) {}

public record OrderCreatedEventV2(
    String orderId,
    BigDecimal amount,
    String customerId,
    Instant timestamp
) {}

// Event Upcaster
public class OrderEventUpcaster implements EventUpcaster {
    @Override
    public Stream<IntermediateEventRepresentation> upcast(
        Stream<IntermediateEventRepresentation> eventStream) {
        
        return eventStream.map(event -> {
            if (event.getType().getName().equals("OrderCreatedEventV1")) {
                return upcastV1ToV2(event);
            }
            return event;
        });
    }
}

Event Store

@Entity
@Table(name = "saga_events")
public class SagaEvent {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String sagaId;
    
    @Column(nullable = false)
    private String eventType;
    
    @Column(columnDefinition = "TEXT")
    private String payload;
    
    @Column(nullable = false)
    private Instant timestamp;
    
    @Column(nullable = false)
    private Integer version;
}

Compensating Transactions

Design Principles

Idempotency: Execute multiple times with same result

public void cancelPayment(String paymentId) {
    Payment payment = paymentRepository.findById(paymentId)
        .orElse(null);
    
    if (payment == null) {
        // Already cancelled or doesn't exist
        return;
    }
    
    if (payment.getStatus() == PaymentStatus.CANCELLED) {
        // Already cancelled, idempotent
        return;
    }
    
    payment.setStatus(PaymentStatus.CANCELLED);
    paymentRepository.save(payment);
    
    // Refund logic here
}

Retryability: Safe to retry on failure

@Retryable(
    value = {TransientException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000, multiplier = 2)
)
public void releaseInventory(String itemId, int quantity) {
    // Implementation
}

Compensation Strategies

Backward Recovery: Undo completed steps

@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentFailedEvent event) {
    // Step 1: Cancel shipment preparation
    commandGateway.send(new CancelShipmentCommand(event.getOrderId()));
    
    // Step 2: Release inventory
    commandGateway.send(new ReleaseInventoryCommand(event.getOrderId()));
    
    // Step 3: Cancel order
    commandGateway.send(new CancelOrderCommand(event.getOrderId()));
    
    end();
}

Forward Recovery: Retry failed operation

@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentTransientFailureEvent event) {
    if (event.getRetryCount() < MAX_RETRIES) {
        // Retry payment
        ProcessPaymentCommand retryCommand = new ProcessPaymentCommand(
            event.getPaymentId(),
            event.getOrderId(),
            event.getAmount()
        );
        commandGateway.send(retryCommand);
    } else {
        // Compensate
        handlePaymentFailure(event);
    }
}

Semantic Lock Pattern

Prevent concurrent modifications during saga execution:

@Entity
public class Order {
    @Id
    private String orderId;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @Version
    private Long version;
    
    private Instant lockedUntil;
    
    public boolean tryLock(Duration lockDuration) {
        if (isLocked()) {
            return false;
        }
        this.lockedUntil = Instant.now().plus(lockDuration);
        return true;
    }
    
    public boolean isLocked() {
        return lockedUntil != null && 
               Instant.now().isBefore(lockedUntil);
    }
    
    public void unlock() {
        this.lockedUntil = null;
    }
}

State Management

Saga State

@Entity
@Table(name = "saga_state")
public class SagaState {
    
    @Id
    private String sagaId;
    
    @Enumerated(EnumType.STRING)
    private SagaStatus status;
    
    @Column(columnDefinition = "TEXT")
    private String currentStep;
    
    @Column(columnDefinition = "TEXT")
    private String compensationSteps;
    
    private Instant startedAt;
    private Instant completedAt;
    
    @Version
    private Long version;
}

public enum SagaStatus {
    STARTED,
    PROCESSING,
    COMPENSATING,
    COMPLETED,
    FAILED,
    CANCELLED
}

State Machine with Spring Statemachine

@Configuration
@EnableStateMachine
public class SagaStateMachineConfig 
    extends StateMachineConfigurerAdapter<SagaStatus, SagaEvent> {
    
    @Override
    public void configure(
        StateMachineStateConfigurer<SagaStatus, SagaEvent> states) 
        throws Exception {
        
        states
            .withStates()
            .initial(SagaStatus.STARTED)
            .states(EnumSet.allOf(SagaStatus.class))
            .end(SagaStatus.COMPLETED)
            .end(SagaStatus.FAILED);
    }
    
    @Override
    public void configure(
        StateMachineTransitionConfigurer<SagaStatus, SagaEvent> transitions) 
        throws Exception {
        
        transitions
            .withExternal()
                .source(SagaStatus.STARTED)
                .target(SagaStatus.PROCESSING)
                .event(SagaEvent.ORDER_CREATED)
            .and()
            .withExternal()
                .source(SagaStatus.PROCESSING)
                .target(SagaStatus.COMPLETED)
                .event(SagaEvent.ALL_STEPS_COMPLETED)
            .and()
            .withExternal()
                .source(SagaStatus.PROCESSING)
                .target(SagaStatus.COMPENSATING)
                .event(SagaEvent.STEP_FAILED)
            .and()
            .withExternal()
                .source(SagaStatus.COMPENSATING)
                .target(SagaStatus.FAILED)
                .event(SagaEvent.COMPENSATION_COMPLETED);
    }
}

Error Handling and Retry

Retry Configuration

@Configuration
@EnableRetry
public class RetryConfig {
    
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000L);
        retryTemplate.setBackOffPolicy(backOffPolicy);
        
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3);
        retryTemplate.setRetryPolicy(retryPolicy);
        
        return retryTemplate;
    }
}

Circuit Breaker with Resilience4j

@Configuration
public class CircuitBreakerConfig {
    
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofMillis(1000))
            .slidingWindowSize(2)
            .build();
        
        return CircuitBreakerRegistry.of(config);
    }
}

@Service
public class PaymentService {
    
    private final CircuitBreaker circuitBreaker;
    
    public PaymentService(CircuitBreakerRegistry registry) {
        this.circuitBreaker = registry.circuitBreaker("payment");
    }
    
    public PaymentResult processPayment(PaymentRequest request) {
        return circuitBreaker.executeSupplier(
            () -> callPaymentGateway(request)
        );
    }
}

Dead Letter Queue

@Configuration
public class DeadLetterQueueConfig {
    
    @Bean
    public NewTopic deadLetterTopic() {
        return new NewTopic("saga-dlq", 1, (short) 1);
    }
}

@Component
public class SagaErrorHandler implements ConsumerAwareErrorHandler {
    
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    @Override
    public void handle(Exception thrownException, 
                      List<ConsumerRecord<?, ?>> records,
                      Consumer<?, ?> consumer, 
                      MessageListenerContainer container) {
        
        records.forEach(record -> {
            kafkaTemplate.send("saga-dlq", record.key(), record.value());
        });
    }
}

Testing Strategies

Unit Testing Saga

@Test
void shouldCompensateWhenPaymentFails() {
    // Given
    OrderSaga saga = new OrderSaga();
    FixtureConfiguration<OrderSaga> fixture = new SagaTestFixture<>(OrderSaga.class);
    
    String orderId = UUID.randomUUID().toString();
    String paymentId = UUID.randomUUID().toString();
    
    // When
    fixture
        .givenNoPriorActivity()
        .whenPublishingA(new OrderCreatedEvent(orderId, BigDecimal.TEN, "item-1"))
        .expectDispatchedCommands(new ProcessPaymentCommand(paymentId, orderId, BigDecimal.TEN));
    
    // Then - payment fails
    fixture
        .whenPublishingA(new PaymentFailedEvent(paymentId, orderId, "item-1", "Insufficient funds"))
        .expectDispatchedCommands(new CancelOrderCommand(orderId));
}

Integration Testing with Testcontainers

@SpringBootTest
@Testcontainers
class SagaIntegrationTest {
    
    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.4.0")
    );
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        "postgres:15-alpine"
    );
    
    @DynamicPropertySource
    static void overrideProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Test
    void shouldCompleteOrderSagaSuccessfully() {
        // Test implementation
    }
}

Testing Idempotency

@Test
void compensationShouldBeIdempotent() {
    String paymentId = "payment-123";
    
    // Execute compensation first time
    paymentService.cancelPayment(paymentId);
    Payment firstResult = paymentRepository.findById(paymentId).orElseThrow();
    
    // Execute compensation second time
    paymentService.cancelPayment(paymentId);
    Payment secondResult = paymentRepository.findById(paymentId).orElseThrow();
    
    // Should produce same result
    assertThat(firstResult).isEqualTo(secondResult);
    assertThat(secondResult.getStatus()).isEqualTo(PaymentStatus.CANCELLED);
}

Monitoring and Observability

Micrometer Metrics

@Component
public class SagaMetrics {
    
    private final Counter sagaStarted;
    private final Counter sagaCompleted;
    private final Counter sagaFailed;
    private final Timer sagaDuration;
    
    public SagaMetrics(MeterRegistry registry) {
        this.sagaStarted = Counter.builder("saga.started")
            .description("Number of sagas started")
            .register(registry);
            
        this.sagaCompleted = Counter.builder("saga.completed")
            .description("Number of sagas completed successfully")
            .register(registry);
            
        this.sagaFailed = Counter.builder("saga.failed")
            .description("Number of sagas failed")
            .register(registry);
            
        this.sagaDuration = Timer.builder("saga.duration")
            .description("Saga execution duration")
            .register(registry);
    }
    
    public void recordSagaStart() {
        sagaStarted.increment();
    }
    
    public void recordSagaCompletion(Duration duration) {
        sagaCompleted.increment();
        sagaDuration.record(duration);
    }
    
    public void recordSagaFailure() {
        sagaFailed.increment();
    }
}

Distributed Tracing

@Configuration
public class TracingConfig {
    
    @Bean
    public Tracer tracer() {
        return new Tracer.Builder()
            .spanReporter(new ZipkinSpanReporter())
            .build();
    }
}

@Service
public class OrderService {
    
    @Autowired
    private Tracer tracer;
    
    public void createOrder(OrderRequest request) {
        Span span = tracer.newTrace().name("create-order").start();
        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
            // Order creation logic
            span.tag("orderId", request.getOrderId());
        } finally {
            span.finish();
        }
    }
}

Health Checks

@Component
public class SagaHealthIndicator implements HealthIndicator {
    
    private final SagaStateRepository sagaStateRepository;
    
    @Override
    public Health health() {
        long stuckSagas = sagaStateRepository.countStuckSagas(
            Duration.ofMinutes(30)
        );
        
        if (stuckSagas > 10) {
            return Health.down()
                .withDetail("stuckSagas", stuckSagas)
                .build();
        }
        
        return Health.up()
            .withDetail("stuckSagas", stuckSagas)
            .build();
    }
}

Performance Considerations

Batch Processing

@Service
public class BatchSagaProcessor {
    
    @Scheduled(fixedDelay = 5000)
    public void processPendingSagas() {
        List<SagaState> pendingSagas = sagaStateRepository
            .findByStatus(SagaStatus.PROCESSING, PageRequest.of(0, 100));
        
        pendingSagas.forEach(this::processSaga);
    }
}

Parallel Execution

@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentProcessedEvent event) {
    // Execute inventory and notification in parallel
    CompletableFuture.allOf(
        CompletableFuture.runAsync(() -> 
            commandGateway.send(new ReserveInventoryCommand(event.getOrderId()))
        ),
        CompletableFuture.runAsync(() -> 
            commandGateway.send(new SendNotificationCommand(event.getOrderId()))
        )
    ).join();
}

Database Optimization

-- Index for saga state queries
CREATE INDEX idx_saga_state_status ON saga_state(status);
CREATE INDEX idx_saga_state_started_at ON saga_state(started_at);

-- Index for event store queries
CREATE INDEX idx_saga_events_saga_id ON saga_events(saga_id);
CREATE INDEX idx_saga_events_timestamp ON saga_events(timestamp);

Security Best Practices

Message Authentication

@Configuration
public class MessageSecurityConfig {
    
    @Bean
    public MessageSigningInterceptor messageSigningInterceptor() {
        return new MessageSigningInterceptor(secretKey);
    }
}

public class MessageSigningInterceptor implements ProducerInterceptor<String, Object> {
    
    @Override
    public ProducerRecord<String, Object> onSend(ProducerRecord<String, Object> record) {
        String signature = computeSignature(record.value());
        Headers headers = record.headers();
        headers.add("signature", signature.getBytes(StandardCharsets.UTF_8));
        return record;
    }
}

Audit Logging

@Aspect
@Component
public class SagaAuditAspect {
    
    @Around("@annotation(SagaOperation)")
    public Object auditSagaOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        String sagaId = extractSagaId(joinPoint);
        String operation = joinPoint.getSignature().getName();
        
        auditLog.info("Saga operation started: sagaId={}, operation={}", 
            sagaId, operation);
        
        try {
            Object result = joinPoint.proceed();
            auditLog.info("Saga operation completed: sagaId={}, operation={}", 
                sagaId, operation);
            return result;
        } catch (Exception e) {
            auditLog.error("Saga operation failed: sagaId={}, operation={}, error={}", 
                sagaId, operation, e.getMessage());
            throw e;
        }
    }
}

Common Pitfalls and Solutions

Pitfall 1: Lost Messages

Problem: Messages get lost due to broker failures.

Solution: Use persistent messages and acknowledgments.

@Bean
public ProducerFactory<String, Object> producerFactory() {
    Map<String, Object> config = new HashMap<>();
    config.put(ProducerConfig.ACKS_CONFIG, "all");
    config.put(ProducerConfig.RETRIES_CONFIG, 3);
    config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    return new DefaultKafkaProducerFactory<>(config);
}

Pitfall 2: Duplicate Processing

Problem: Same message processed multiple times.

Solution: Implement idempotency with deduplication.

@Service
public class DeduplicationService {
    
    private final Set<String> processedMessageIds = ConcurrentHashMap.newKeySet();
    
    public boolean isDuplicate(String messageId) {
        return !processedMessageIds.add(messageId);
    }
}

Pitfall 3: Saga State Inconsistency

Problem: Saga state doesn't match actual service states.

Solution: Use event sourcing or state reconciliation.

@Scheduled(fixedDelay = 60000)
public void reconcileSagaStates() {
    List<SagaState> processingSagas = 
        sagaStateRepository.findByStatus(SagaStatus.PROCESSING);
    
    processingSagas.forEach(saga -> {
        if (isActuallyCompleted(saga)) {
            saga.setStatus(SagaStatus.COMPLETED);
            sagaStateRepository.save(saga);
        }
    });
}

Additional Resources