Implementation Example: Orchestration-based Saga

public class OrderSagaOrchestrator {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final ShippingService shippingService;
    
    public void startOrderSaga(String orderId) {
        Order order = orderRepository.findById(orderId);
        
        try {
            // Step 1: Process payment
            PaymentResult paymentResult = paymentService.processPayment(
                order.getCustomerId(), 
                order.getTotalAmount()
            );
            order.setPaymentId(paymentResult.getPaymentId());
            orderRepository.save(order);
            
            // Step 2: Reserve inventory
            try {
                inventoryService.reserveInventory(order.getItems());
            } catch (Exception e) {
                // Compensate payment
                paymentService.refundPayment(paymentResult.getPaymentId());
                throw e;
            }
            
            // Step 3: Schedule shipping
            try {
                ShippingResult shippingResult = shippingService.scheduleShipping(
                    order.getCustomerId(),
                    order.getShippingAddress(),
                    order.getItems()
                );
                order.setShippingId(shippingResult.getShippingId());
                order.setStatus(OrderStatus.PROCESSING);
                orderRepository.save(order);
            } catch (Exception e) {
                // Compensate inventory
                inventoryService.releaseInventory(order.getItems());
                // Compensate payment
                paymentService.refundPayment(paymentResult.getPaymentId());
                throw e;
            }
            
        } catch (Exception e) {
            order.setStatus(OrderStatus.FAILED);
            order.setFailureReason(e.getMessage());
            orderRepository.save(order);
        }
    }
}

When to Use

The Saga pattern is useful when:

  • You need to maintain data consistency across multiple services
  • Traditional distributed transactions (2PC) are not feasible
  • You’re working with a microservices architecture

Challenges

  • Designing compensation logic for each step
  • Handling partial failures
  • Managing saga state and recovery
  • Dealing with concurrency issues

5. Event-Carried State Transfer

This pattern uses events to propagate state changes to other services, reducing the need for synchronous API calls.

Implementation Example

// Product Service
public class ProductService {
    public void updateProduct(UpdateProductRequest request) {
        // Update product in database
        Product product = productRepository.findById(request.getProductId());
        product.setName(request.getName());
        product.setDescription(request.getDescription());
        product.setPrice(request.getPrice());
        product.setInventoryCount(request.getInventoryCount());
        productRepository.save(product);
        
        // Publish event with complete product state
        eventBus.publish(new ProductUpdatedEvent(
            product.getId(),
            product.getName(),
            product.getDescription(),
            product.getPrice(),
            product.getInventoryCount(),
            product.getCategories()
        ));
    }
}

// Search Service
public class SearchService {
    @EventHandler
    public void on(ProductUpdatedEvent event) {
        // Update search index with product data from event
        SearchDocument document = new SearchDocument();
        document.setId(event.getProductId());
        document.setTitle(event.getName());
        document.setDescription(event.getDescription());
        document.setPrice(event.getPrice());
        document.setAvailable(event.getInventoryCount() > 0);
        document.setCategories(event.getCategories());
        
        searchRepository.save(document);
    }
}

When to Use

Event-carried state transfer is valuable when:

  • Services need to maintain their own copy of data owned by another service
  • You want to reduce synchronous API calls between services
  • You need to update multiple read models when data changes

Challenges

  • Managing large event payloads
  • Handling schema evolution
  • Ensuring eventual consistency

Implementing Event-Driven Architecture: Best Practices

1. Design Events Carefully

Events should be:

  • Meaningful: Represent something significant in the domain
  • Complete: Contain all necessary information for consumers
  • Immutable: Never change after creation
  • Versioned: Support schema evolution

Example of a well-designed event:

{
  "eventId": "e8f8d73b-3819-4a7a-8edb-2983c69e673a",
  "eventType": "OrderCreated",
  "version": "1.0",
  "timestamp": "2025-02-08T10:30:45.123Z",
  "data": {
    "orderId": "ord-12345",
    "customerId": "cust-6789",
    "items": [
      {
        "productId": "prod-101",
        "quantity": 2,
        "unitPrice": 29.99
      },
      {
        "productId": "prod-202",
        "quantity": 1,
        "unitPrice": 49.99
      }
    ],
    "totalAmount": 109.97,
    "shippingAddress": {
      "street": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "zipCode": "62704",
      "country": "USA"
    }
  },
  "metadata": {
    "source": "order-service",
    "correlationId": "req-abcdef",
    "traceId": "trace-123456"
  }
}