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

1265 lines
31 KiB
Markdown

# Spring Boot SAGA Pattern - Reference Documentation
## Table of Contents
1. [Saga Pattern Overview](#saga-pattern-overview)
2. [Choreography-Based Saga](#choreography-based-saga)
3. [Orchestration-Based Saga](#orchestration-based-saga)
4. [Spring Boot Integration](#spring-boot-integration)
5. [Saga Frameworks](#saga-frameworks)
6. [Event-Driven Architecture](#event-driven-architecture)
7. [Compensating Transactions](#compensating-transactions)
8. [State Management](#state-management)
9. [Error Handling and Retry](#error-handling-and-retry)
10. [Testing Strategies](#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):
- **A**tomicity: All or nothing
- **C**onsistency: Valid state transitions
- **I**solation: Concurrent transactions don't interfere
- **D**urability: Committed data persists
**BASE** (Saga Pattern):
- **B**asically **A**vailable: System is available most of the time
- **S**oft state: State may change over time
- **E**ventual 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
```java
@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
```java
@Component
public class PaymentEventListener {
@Bean
public Consumer<OrderCreatedEvent> handleOrderCreatedEvent() {
return event -> processPayment(event.getOrderId());
}
private void processPayment(String orderId) {
// Payment processing logic
}
}
```
#### Event Classes
```java
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
```yaml
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
```xml
<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
```groovy
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
```java
@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
```java
@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
```java
@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
```yaml
axon:
serializer:
general: jackson
events: jackson
messages: jackson
eventhandling:
processors:
order-processor:
mode: tracking
source: eventBus
axonserver:
enabled: false
```
### Maven Dependencies for Axon
```xml
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.0</version>
</dependency>
```
---
## Spring Boot Integration
### Application Configuration
```java
@SpringBootApplication
@EnableScheduling
public class SagaApplication {
public static void main(String[] args) {
SpringApplication.run(SagaApplication.class, args);
}
}
```
### Kafka Configuration
```java
@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
```properties
# 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
```java
public record OrderCreatedEvent(
String orderId,
Instant createdAt,
BigDecimal amount
) implements DomainEvent {}
```
**Integration Events**: Communication between bounded contexts
```java
public record PaymentRequestedEvent(
String orderId,
String paymentId,
BigDecimal amount
) implements IntegrationEvent {}
```
**Command Events**: Request for action
```java
public record ProcessPaymentCommand(
String paymentId,
String orderId,
BigDecimal amount
) {}
```
### Event Versioning
```java
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
```java
@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
```java
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
```java
@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
```java
@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
```java
@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:
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```java
@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
```sql
-- 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
```java
@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
```java
@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.
```java
@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.
```java
@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.
```java
@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
- [Microservices.io - Saga Pattern](https://microservices.io/patterns/data/saga.html)
- [Axon Framework Documentation](https://docs.axoniq.io/reference-guide/)
- [Spring Cloud Stream Reference](https://spring.io/projects/spring-cloud-stream)
- [Eventuate Tram Documentation](https://eventuate.io/docs/manual/eventuate-tram/latest/)
- [Camunda Platform](https://docs.camunda.org/)
- [Apache Camel Saga EIP](https://camel.apache.org/components/latest/eips/saga-eip.html)