📚

なぜEDA(Event-Driven Architecture)が必要なのか?同期処理の限界を超える3つの実践例

に公開

はじめに

マイクロサービス間の通信において、同期処理(REST API呼び出し)が常に最適解とは限りません。特に高トラフィックなECサイトでは、非同期処理が必須となる場面が多々あります。

本記事では、実際のECサイトのコードベースを例に、決済・配送・検索の3つの領域でEDA(Event-Driven Architecture)を採用した理由と、同期処理では解決できない課題について解説します。

同期処理の問題点

従来の同期処理では以下のような問題が発生します:

// 同期処理の例(問題のあるパターン)
public class OrderService {
    public void processOrder(Order order) {
        // 1. 決済処理(外部API呼び出し:2-3秒)
        Payment payment = paymentClient.processPayment(order);
        
        // 2. 配送手配(外部API呼び出し:1-2秒)
        Delivery delivery = deliveryClient.requestDelivery(order);
        
        // 3. 検索インデックス更新(DB更新:0.5秒)
        searchClient.updateProductIndex(order.getProductId());
        
        // 合計:3.5-5.5秒のレスポンス時間
    }
}

問題点:

  • レスポンス時間が長い(3.5-5.5秒)
  • 一つのサービスがダウンすると全体が停止
  • スケーラビリティの限界
  • ユーザー体験の悪化

EDAによる解決策

1. 決済処理:非同期が必須な理由

決済処理は外部決済ゲートウェイとの連携が必要で、処理時間が予測できません。

// EDA実装:OrderService
@Service
public class OrderService {
    @Autowired
    private KafkaTemplate<String, byte[]> kafkaTemplate;
    
    public void finishOrder(Long orderId, Long paymentMethodId) {
        // 即座にレスポンスを返す
        var message = EdaMessage.PaymentRequestV1.newBuilder()
                .setOrderId(orderId)
                .setUserId(order.userId)
                .setAmountKRW(totalAmount)
                .setPaymentMethodId(paymentMethodId)
                .build();
        
        kafkaTemplate.send("payment_request", message.toByteArray());
        // ここで処理完了(レスポンス時間:50ms以下)
    }
}
// PaymentService:非同期で決済処理
@Component
public class PaymentEventListener {
    @KafkaListener(topics = "payment_request")
    public void processPayment(byte[] message) throws Exception {
        var request = EdaMessage.PaymentRequestV1.parseFrom(message);
        
        // 外部決済API呼び出し(時間がかかっても他に影響しない)
        var payment = paymentService.processPayment(
            request.getUserId(),
            request.getOrderId(),
            request.getAmountKRW(),
            request.getPaymentMethodId()
        );
        
        // 結果を非同期で通知
        var result = EdaMessage.PaymentResultV1.newBuilder()
                .setOrderId(payment.orderId)
                .setPaymentId(payment.id)
                .setPaymentStatus(payment.paymentStatus.toString())
                .build();
        
        kafkaTemplate.send("payment_result", result.toByteArray());
    }
}

メリット:

  • ユーザーは即座にレスポンスを受け取れる
  • 決済処理の遅延が他のサービスに影響しない
  • 決済失敗時の再試行が容易

2. 配送処理:段階的処理が可能

配送は決済完了後に開始されるべきですが、同期処理では複雑になります。

// OrderService:決済結果を受けて配送要求
@KafkaListener(topics = "payment_result")
public void handlePaymentResult(byte[] message) throws Exception {
    var result = EdaMessage.PaymentResultV1.parseFrom(message);
    
    // 注文状態を更新
    var order = orderRepository.findById(result.getOrderId()).orElseThrow();
    order.paymentId = result.getPaymentId();
    order.orderStatus = OrderStatus.DELIVERY_REQUESTED;
    orderRepository.save(order);
    
    // 配送要求を非同期で送信
    var deliveryRequest = EdaMessage.DeliveryRequestV1.newBuilder()
        .setOrderId(order.id)
        .setProductName(product.getName())
        .setAddress(order.deliveryAddress)
        .build();
    
    kafkaTemplate.send("delivery_request", deliveryRequest.toByteArray());
}
// DeliveryService:配送処理と状態通知
@KafkaListener(topics = "delivery_request")
public void processDelivery(byte[] message) throws Exception {
    var request = EdaMessage.DeliveryRequestV1.parseFrom(message);
    
    var delivery = deliveryService.processDelivery(
        request.getOrderId(),
        request.getProductName(),
        request.getProductCount(),
        request.getAddress()
    );
    
    // 配送状態を通知
    var statusUpdate = EdaMessage.DeliveryStatusUpdateV1.newBuilder()
        .setOrderId(delivery.orderId)
        .setDeliveryId(delivery.id)
        .setDeliveryStatus(delivery.status.toString())
        .build();
    
    kafkaTemplate.send("delivery_status_update", statusUpdate.toByteArray());
}

メリット:

  • 決済→配送の順序が自然に保たれる
  • 各段階で適切な状態管理が可能
  • 配送業者のAPIが遅くても他に影響しない

3. 検索インデックス更新:最終的整合性で十分

商品情報の変更時、検索インデックスの更新は即座に完了する必要がありません

// CatalogService:商品登録時
public Product registerProduct(Long sellerId, String name, 
                              List<String> tags) {
    // 商品をDBに保存(即座に完了)
    Product product = new Product(sellerId, name, description, price, stockCount, tags);
    productRepository.save(product);
    
    // 検索インデックス更新は非同期で実行
    var message = EdaMessage.ProductTags.newBuilder()
            .setProductId(product.id)
            .addAllTags(tags)
            .build();
    
    kafkaTemplate.send("product_tags_added", message.toByteArray());
    
    return product; // 即座にレスポンス
}
// SearchService:非同期でインデックス更新
@KafkaListener(topics = "product_tags_added")
public void updateSearchIndex(byte[] message) throws Exception {
    var productTags = EdaMessage.ProductTags.parseFrom(message);
    
    // 検索キャッシュを更新(時間がかかっても問題なし)
    searchService.addTagCache(
        productTags.getProductId(), 
        productTags.getTagsList()
    );
}

@KafkaListener(topics = "product_tags_removed")
public void removeFromSearchIndex(byte[] message) throws Exception {
    var productTags = EdaMessage.ProductTags.parseFrom(message);
    
    searchService.removeTagCache(
        productTags.getProductId(), 
        productTags.getTagsList()
    );
}

メリット:

  • 商品登録のレスポンスが高速
  • 検索インデックスの更新処理が他の機能に影響しない
  • 最終的整合性で十分(数秒の遅延は許容範囲)

パフォーマンス比較

同期処理の場合

注文処理時間:3.5-5.5秒
スループット:200 req/sec(限界)
障害時:全体停止

EDA採用後

注文処理時間:50ms以下
スループット:2000+ req/sec
障害時:部分的な機能停止のみ

まとめ

EDAが威力を発揮するのは、同期処理が不要または有害な場面です:

  1. 決済処理:外部APIの処理時間が不定
  2. 配送処理:段階的な処理が必要
  3. 検索更新:最終的整合性で十分

これらの特徴を理解して適切にEDAを採用することで、高トラフィックにも耐えられるスケーラブルなシステムを構築できます。

重要なのは「すべてを非同期にする」ことではなく、「同期処理が不要な部分を見極める」ことです。適切な設計により、ユーザー体験を損なうことなく、システムの可用性とパフォーマンスを大幅に向上させることができます。

Discussion