前置き
前回記事の続きになります。
アプリケーション層に混在しているリトライ処理を、責務に応じてインフラ層へ段階的に移行させるための詳細な手順を解説します。
これは、アーキテクチャをクリーンに保ちながら安全に進化させるための非常に重要なプラクティスですので、是非ともフレームワークとして、マイクロサービス化を実施している皆さんお使いください。
大きく、以下の4つのステップに分けられます。
必ず、可逆性を担保させながら移行させてください。
ステップ1:アプリケーション層における責務の分離 (学習フェーズ) 🧐
この段階の目的は、アプリケーションコード内に明確な境界線を引き、将来インフラ層へ移行するロジックを特定・隔離することです。
事前条件
エピックサーガのフェーズを通っており、そこですでに
「サービスをどこで切り分けたらいいのか?」 の学習は十分に済んでおり、
各サービス境界は定義済であり、結果整合性の定義まで完了している。
①. 2つのリトライ処理用モジュールの作成
アプリケーション内に、リトライ処理を担うモジュールを明確に2つ作成します。
クリーンアーキテクチャの同心円でいう所の、インターフェース・アダプター(Interface Adapters)層に、これら2つのモジュールを配置します。
BusinessRetryModule
アプリケーション層が担うべき、ビジネスロジックに依存するリトライ処理をここに実装します。(例:「在庫ロック中エラー」の場合、5分後に再試行する)
InfraRetryModule (移行対象)
通信エラーなど本来はインフラ層が担うべき、機械的なリトライ処理をここに実装します。
(例:HTTP 503エラーの場合、3回まで即時再試行する)
最悪、間違ってビジネスリトライ責務をここに置いてしまっても、
まだインフラ層には移動していないので、全然まだ間に合うリスクです。
②. 明確な依存関係の構築
オーケストレーターのUseCaseは、まずInfraRetryModuleを呼び出します。(下図の①)
InfraRetryModuleは、その内部で実際の外部サービス呼び出しを行います。(図の②)
もしビジネス的なエラーが返ってきた場合、InfraRetryModuleはそれをそのままユースケースに返し、ユースケースがBusinessRetryModuleを使ってリトライを判断します。(図の④)
この設計により、「これは将来インフラ層に行くべきコードだ」という意図が、コードの構造自体に明確に表現されます。
より詳しい説明は、下記の【補足】トピックへ
ステップ2:ストラングラー・フィグ・パターンの準備 (プロキシ導入) 🔌
この段階の目的は、アプリケーションと外部サービスの間に「割り込み」、トラフィックを横取りするための準備をすることです。
つまり、アプリケーションコードに手を加えることなく、通信を制御するための介入点を設けることです。
①. サービスメッシュ/APIゲートウェイの導入
まだ導入されていない場合、IstioやLinkerdのようなサービスメッシュ、またはAPIゲートウェイを導入します。
この時点では、まだリトライ機能などは有効にせず、単純にトラフィックを透過的にプロキシするだけの設定し、特別なルールは適用しません。
Istioの例
VirtualServiceとDestinationRuleを作成しますが、中身はシンプルにトラフィックをそのまま流すだけの定義にします。
②. 通信経路の変更
これまでオーケストレーター(タイムトラベルならマイクロサービスA)が、直接マイクロサービスB(呼び出される側のサービス)を呼び出していた通信経路(user-serviceのDNS名)を、
サービスメッシュのサイドカープロキシを経由するようにネットワークを設定します。
これにより、アプリケーションコードを変更することなく、インフラ層で通信を制御できるようになります。
Kubernetes環境では、Podにサイドカーをインジェクトするだけで、これが自動的に行われます。
この変更後、システム全体の動作に変化がないことを確認します。
ステップ3:インフラ層への段階的移行 🌿
この段階で、アプリケーション層のInfraRetryModuleの責務を、少しずつストラングラーパターンを使ってインフラ層のサービスメッシュに移行していきます。
①. 観測性の確保
まず、サービスメッシュのダッシュボード(Grafana, Kialiなど)で、現在のリクエスト成功率、レイテンシ、エラーの内訳(HTTP 503の発生頻度など)を正確に把握します。
これが移行前(Before)の状態です。
②. インフラ層でのリトライ機能の有効化
サービスメッシュの設定を変更し、特定の条件(例:503 Service Unavailableレスポンス)
の場合に、自動的にリトライを実行するようにします。
まずは1回のリトライから始めるなど、設定は徐々に強化しましょう。
Istioの例
VirtualServiceを更新し、HTTP 503エラーに対するリトライを追加します。
③. 観測と検証
ダッシュボードを監視し、以下の点を確認します。
アプリケーションからのリクエスト数が変わらないのに、サービス間の実際のリクエスト数が増加しているか(リトライの証拠)。
これまでアプリケーションのログに出ていた「HTTP 503エラー」のログが減少し、代わりにサービスメッシュのログにリトライの記録が残っているか。
全体としてのエラー率が低下し、成功率が改善しているか。
④. アプリケーション層のロジックを無効化(または削除)
インフラ層がInfraRetry責務を完全に代替できると確信できるようになったら、
アプリケーションのInfraRetryModuleに実装されていた同様のリトライロジックをコメントアウトまたは削除します。
個人的には、コメントアウトのみしておいて、次の⑤をクリアしたら、
完全に削除するという手順をお勧めします。
ここで削除をしないで残しておくと、重複不要コードとなり、負債と化します。
⑤. テストと監視
この状態でシステムを動かし、期待通りにインフラ層がリトライを実行しているか、全体の挙動に問題がないかを監視データ(メトリクス、ログ)に基づいて慎重に検証します。
⑥. 繰り返し
他の機械的なエラーパターン(例:接続タイムアウト)についても、同様に上記の②~⑤を繰り返して、InfraRetryの責務を一つずつ、安全にインフラ層へ「絞め殺し」ていきます。
ステップ4:最終的な状態 (責務分離の完了) ✨
全ての移行が完了すると、理想的な状態が実現します。
アプリケーションコード
InfraRetryModuleは空になったか、あるいは完全に削除されています。
アプリケーションのコードは、ビジネスロジックに依存するリトライ(BusinessRetryModule)
のみに責任を持ち、非常にシンプルでクリーンになります。
インフラストラクチャ
サービスメッシュが、全てのサービスに共通する技術的な通信の信頼性を透過的に提供します。
この手順を踏むことで、初期の学習フェーズから、責務が明確に分離された成熟したアーキテクチャへと、安全かつ段階的に進化させることが可能になります。
事後条件 - 次のパラレルサーガへ
インフラ層でのみ、密に連携し、アプリケーション層の
ここまで完了させて、おとぎ話サーガのフェーズは最終形となり、次の非同期なパラレルサーガに安全に移行できるようになります。
補足 - レスポンスが導く責務の分離
呼び出されたマイクロサービスが、呼び出し元のオーケストレーターに返すレスポンスには、大きく分けて2種類のエラー情報が含まれています。
この呼び出されたサービスが返すレスポンスこそが、
InfraRetryModuleとBusinessRetryModuleに何を実装すべきかを教えてくれる、
最も重要な「生きたドキュメント」
なのです。
①. インフラリトライ系のエラー (技術的なエラー) Infrastructural
これは、呼び出されたサービス自身が、自身の依存先(DBなど)との間で発生した一時的な技術的問題を通知するものです。
レスポンスの例
・HTTP 503 Service Unavailable (DB接続プールが一時的に枯渇)
・HTTP 504 Gateway Timeout (内部でのAPI呼び出しがタイムアウト)
オーケストレーターの解釈
「呼び出したサービス内部で何か一時的な技術トラブルがあったようだ。
レスポンスを見る限り、手紙の中身(ビジネスリクエスト)は問題ないはずなので、時間を置かずにすぐ再送すれば成功するかもしれない。」と考えます。
実装へのフィードバック
このようなエラーが頻繁に発生する場合、それは
「InfraRetryModuleに実装し、将来的にはサービスメッシュに移管すべき典型的なパターンだ」
という強力なシグナルになります。
②. ビジネスリトライ系のエラー (ビジネス的なエラー) 📝
これは、リクエストが技術的には正常に処理されたものの、ビジネス上のルールや制約によって受け入れられなかったことを通知するものです。
レスポンスの例
・HTTP 409 Conflict (レスポンスボディ: {"errorCode": "INVENTORY_LOCKED", "retryAfter": "300s"})
・HTTP 422 Unprocessable Entity (レスポンスボディ: {"errorCode": "INSUFFICIENT_FUNDS", "nextAttemptDate": "2025-10-11"})
オーケストレーターの解釈
「呼び出したサービスさんによると、手紙の中身(ビジネスリクエスト)自体は理解できたが、今のビジネス状況では処理できないらしい。エラーの詳細(在庫ロック中、資金不足など)に基づいて、戦略的な判断(例:5分待つ、 明日再試行するetc) が必要だ。」
実装へのフィードバック
このようなエラーとその解決ロジックは、
「BusinessRetryModuleに実装し、アプリケーション層が永続的に責任を持つべきビジネスプロセスの一部だ」
ということが明確になります。
結論
おとぎ話サーガの初期段階では、オーケストレーターは呼び出すマイクロサービスからの
多様なエラーレスポンスを受け取り、それを分析する立場にいます。
そして、
「このエラーは機械的にリトライできるな」というパターンをInfraRetryModuleに
「このエラーはビジネス判断が必要だ」というパターンをBusinessRetryModule
へと蓄積していきます。
このエラーレスポンスの分析プロセスこそが、ビジネス系リトライとインフラ系リトライとの責務の境界を発見し、アーキテクチャを進化させるためのコンパスとなるのです。