📚
なぜ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が威力を発揮するのは、同期処理が不要または有害な場面です:
- 決済処理:外部APIの処理時間が不定
- 配送処理:段階的な処理が必要
- 検索更新:最終的整合性で十分
これらの特徴を理解して適切にEDAを採用することで、高トラフィックにも耐えられるスケーラブルなシステムを構築できます。
重要なのは「すべてを非同期にする」ことではなく、「同期処理が不要な部分を見極める」ことです。適切な設計により、ユーザー体験を損なうことなく、システムの可用性とパフォーマンスを大幅に向上させることができます。
Discussion