ストラングラーフィグパターン入門 - レガシーを段階的に置き換える戦略

2026.04.10

公式ドキュメント

ストラングラーフィグパターンとは

ストラングラーフィグパターン(Strangler Fig Pattern)は、Martin Fowler が 2004 年に提唱したレガシーシステム移行のための設計パターンです。オーストラリアの熱帯雨林に生息する「絞め殺しイチジク(Strangler Fig)」という植物が、宿主の木を覆いながら徐々に置き換えていく様子に由来します。

このパターンの核心は、「レガシーシステムを一気に書き換える(ビッグバン置換)のではなく、新しいシステムを古いシステムの周りに少しずつ構築し、徐々に機能を移行していく」というアプローチにあります。最終的にレガシーシステムが完全に置き換わるまで、両者は共存します。

flowchart LR
 subgraph Phase1["フェーズ1: 開始"]
 L1["Legacy System"]
 end
 subgraph Phase2["フェーズ2: ファサード設置"]
 F2["Facade / Proxy"] --> L2["Legacy"]
 end
 subgraph Phase3["フェーズ3: 部分移行"]
 F3["Facade"] --> L3["Legacy<br/>(縮小)"]
 F3 --> N3["New Service A"]
 end
 subgraph Phase4["フェーズ4: 完全移行"]
 F4["Facade"] --> N4["New Services"]
 end

なぜストラングラーフィグパターンが必要か

ビッグバン置換の失敗

レガシーシステムを「全部書き換える」プロジェクトの多くは失敗します。主な理由は以下です。

  • 完成までの数年間、ビジネス価値を提供できない
  • 旧システムの暗黙仕様を見落とすリスク
  • 移行直前まで本番投入されないため不具合発覚が遅れる
  • スコープが膨張し続け、終わりが見えなくなる
  • 旧システムへの新機能追加が同時並行で発生

段階的アプローチの利点

ストラングラーフィグでは、移行を小さな単位に分割し、新機能を徐々に旧システムから剥がしていきます。各段階で本番にデプロイされるため、リスクが局所化され、ビジネス価値も継続的に提供できます。

基本原則

1. ファサードを介して全アクセスを集中させる

最初に、レガシーシステムへのすべてのアクセスを「ファサード(プロキシ・API ゲートウェイ)」経由に切り替えます。これが移行のためのコントロールポイントになります。

2. 機能単位で少しずつ移行する

機能を 1 つずつ新システムに実装し、ファサードでルーティングを切り替えます。一度にすべてを置き換えません。

3. 双方向のデータ整合性を保つ

新旧システムが同じデータを参照するため、データ同期戦略(共有 DB、イベントストリーム、変更データキャプチャ)が必要になります。

4. 撤退可能性を保証する

各ステップは元に戻せるように設計します。新機能に問題があれば即座にレガシーに戻せる仕組みが安心感を生みます。

5. レガシーへの新規開発を凍結する

移行中もレガシーに機能を追加し続けると終わりません。可能な限り新規追加は新システム側で行います。

構成要素の詳細

ファサード / プロキシ層

リバースプロキシ、API ゲートウェイ、あるいはアプリケーション内のルーティング層が該当します。Nginx、Envoy、Kong、AWS API Gateway などが使われます。

ルーティングルール

URL パス、HTTP メソッド、ヘッダー、ユーザー属性などに基づいて、リクエストを新旧どちらに振り分けるかを決定します。

データ層の戦略

  • 共有データベース: 移行初期は旧 DB を共有
  • 二重書き込み: 新旧両方に書き込む
  • 変更データキャプチャ(CDC): 旧 DB の変更を新 DB に伝播
  • イベントソーシング: 新システムをイベント駆動で構築

モニタリングとフィーチャートグル

新ルートへの切り替えはフィーチャートグルで制御し、メトリクスで異常を検知したら即座に旧ルートに戻します。

flowchart TB
 Client["Client"] --> FG["Feature Gate / Proxy"]
 FG -->|"フラグOFF"| Legacy["Legacy"]
 FG -->|"フラグON"| New["New Service"]
 FG --> Mon["Monitoring"]
 Mon -->|"異常検知"| FG

実装例

Nginx によるシンプルなファサード

upstream legacy_app {
 server legacy.internal:8080;
}

upstream new_users_service {
 server users-service.internal:3000;
}

server {
 listen 80;

 # 新システムへ移行済みのエンドポイント
 location /api/users {
 proxy_pass http://new_users_service;
 }

 # それ以外はすべてレガシーへ
 location / {
 proxy_pass http://legacy_app;
 }
}

アプリケーションレベルのファサード(TypeScript)

import express from "express";
import { isFeatureEnabled } from "./feature-flags";

const app = express();

app.use("/api/users", async (req, res, next) => {
 if (await isFeatureEnabled("new-users-service", req.user?.id)) {
 return proxyTo("http://users-service.internal:3000", req, res);
 }
 return proxyTo("http://legacy.internal:8080", req, res);
});

app.use("/", (req, res) => proxyTo("http://legacy.internal:8080", req, res));

段階的移行のフェーズ例

// フェーズ管理
type MigrationPhase = "shadow" | "canary" | "split" | "complete";

interface RouteConfig {
 endpoint: string;
 phase: MigrationPhase;
 newServiceUrl: string;
 legacyUrl: string;
 canaryPercentage?: number;
}

const routes: RouteConfig[] = [
 {
 endpoint: "/api/users",
 phase: "complete",
 newServiceUrl: "http://users-svc",
 legacyUrl: "http://legacy",
 },
 {
 endpoint: "/api/orders",
 phase: "canary",
 newServiceUrl: "http://orders-svc",
 legacyUrl: "http://legacy",
 canaryPercentage: 10,
 },
 {
 endpoint: "/api/inventory",
 phase: "shadow",
 newServiceUrl: "http://inventory-svc",
 legacyUrl: "http://legacy",
 },
];

Shadow モード(並行実行)

async function shadowExecute(req: Request): Promise<Response> {
 const legacyPromise = callLegacy(req);
 const newPromise = callNew(req).catch((err) => {
 logShadowError(err);
 return null;
 });

 const [legacyRes, newRes] = await Promise.all([legacyPromise, newPromise]);

 if (newRes) {
 compareResponses(legacyRes, newRes);
 }

 // ユーザーには常にレガシーの応答を返す
 return legacyRes;
}

メリット

  • リスクの局所化: 機能単位で本番投入し、問題があれば局所的にロールバック可能
  • 継続的な価値提供: 移行中もユーザーが新機能を享受できる
  • 段階的な学習: 新システムの設計を運用しながら洗練できる
  • 撤退可能性: いつでも元に戻せる安心感
  • チームの並行作業: 機能単位で分担できる

デメリット

  • 移行期間の長期化: 一気に終わらせる場合より総時間は長くなりがち
  • 二重メンテナンス: 新旧両方を保守する負担
  • **データ整合性の複雑さ: 同期戦略の設計と運用が必須
  • インフラコスト: ファサード層・両システムの並行稼働
  • 完了の判断が難しい: 「最後の数%」が長く残るリスク

ユースケース

  1. モノリスからマイクロサービスへの移行: 機能境界を切り出して個別サービス化
  2. 言語・フレームワークの刷新: PHP から Go へ、Java から Kotlin へ
  3. オンプレミスからクラウドへ: 機能別にクラウド移行
  4. データベースの置き換え: Oracle から PostgreSQL へ
  5. UI フレームワークの近代化: jQuery から React へ画面単位で置換

よくある落とし穴

ファサードがボトルネックになる

すべてのトラフィックが通る箇所なので、性能・可用性・スケーラビリティの設計が必須です。

移行が終わらない

「次の機能はまた来年」を続けると永遠に共存します。期限と完了基準をプロジェクト計画に明記します。

レガシーへの機能追加が止まらない

ビジネス側の要求で旧システムに機能を追加し続けると、移行対象が増え続けます。新規開発の凍結ルールを徹底します。

データの不整合

二重書き込みやイベント伝播の遅延でデータがズレることがあります。整合性チェックの仕組みを並行で走らせます。

ロールバック手順の不備

新ルートに不具合が出ても戻せないと、移行は失敗します。各段階でロールバック手順を文書化・訓練します。

他パターンとの比較

パターンアプローチ期間リスク適用シーン
Strangler Fig段階的に置き換え中〜長期大規模レガシーの近代化
Big Bang Rewrite一斉切り替え中期小規模、または完全な仕様変更
Branch by Abstractionコード内で抽象化して切替短〜中期内部実装の置換
Parallel Run新旧並行実行で結果比較中期クリティカルな業務ロジック
Anti-Corruption Layerレガシーとの境界に変換層継続的レガシーとの統合維持

ストラングラーフィグは Anti-Corruption Layer や Branch by Abstraction と組み合わせて使われることも多いです。

ベストプラクティス

1. 最初にファサードを設置する

何よりも先にすべての通信を経路化します。コントロールできなければ移行は始まりません。

2. 移行優先度を業務価値で決める

技術的に簡単な箇所からではなく、業務的に変更頻度が高い・障害が多い箇所から着手すると効果が早く現れます。

3. Shadow モードで検証する

新サービスを並行稼働させ、応答を比較することで本番投入前に挙動の違いを発見できます。

4. メトリクスで成果を可視化

「移行済みエンドポイント数」「レガシーへのトラフィック割合」をダッシュボードで追跡し、進捗を可視化します。

5. 完了基準を明文化する

どの状態になれば「移行完了」と言えるのかを最初に決めます。あいまいなままだと永久に続きます。

6. レガシーの理解に投資する

レガシーの仕様を読み解くリバースエンジニアリングは必須です。旧システムを軽視すると必ず罠にはまります。

ポイント: ストラングラーフィグパターンの成功の鍵は「ファサード(プロキシ)」の設計です。新旧システムへのルーティングを柔軟に制御できる仕組みを最初に構築しましょう。

ここまでの整理

ストラングラーフィグパターンは、レガシーシステムの近代化における信頼性の高い戦略の一つです。一括置換の派手さはありませんが、リスクを管理し、ビジネスを止めずに、確実にゴールへ近づける現実的なアプローチです。鍵となるのはファサードの設置、段階的なルーティング切り替え、データ整合性の戦略、そして「終わらせる意志」です。長く使われてきたレガシーを、次の10年に耐えられる形へ計画的に置き換えていきましょう。

運用面の追加トピック

Anti-Corruption Layer の併用

新システムが旧システムの概念に汚染されないよう、両者の境界に「Anti-Corruption Layer(ACL)」を設置します。Eric Evans が DDD で提唱した概念で、レガシーのデータモデルを新しいドメインの言葉に翻訳する変換層です。これがないと、新システムが「旧テーブル名のまま」のような歪な設計になりがちです。

// 旧システムのレスポンス
interface LegacyCustomerDto {
 CUST_ID: string;
 CUST_NM: string;
 RGST_DT: string; // YYYYMMDD
}

// 新システムのドメインモデル
class Customer {
 constructor(
 public readonly id: string,
 public readonly name: string,
 public readonly registeredAt: Date,
 ) {}
}

// Anti-Corruption Layer
class CustomerAntiCorruption {
 static fromLegacy(dto: LegacyCustomerDto): Customer {
 const y = Number(dto.RGST_DT.slice(0, 4));
 const m = Number(dto.RGST_DT.slice(4, 6)) - 1;
 const d = Number(dto.RGST_DT.slice(6, 8));
 return new Customer(dto.CUST_ID, dto.CUST_NM, new Date(y, m, d));
 }
}

Change Data Capture を使った同期

旧 DB の変更を新 DB にリアルタイム同期するには、Debezium のような Change Data Capture(CDC)ツールが有効です。アプリケーションコードを変更せずに、データベースの WAL/binlog から変更イベントを取り出し、Kafka 経由で新システムに流せます。これにより、データ移行と新機能開発を並行できます。

Branch by Abstraction との使い分け

Strangler Fig はシステム境界(プロキシ・URL)でルーティングを切り替えるのに対し、Branch by Abstraction はコード内部に抽象インターフェースを挟み、実装をすり替えます。後者はモノリス内部の置換に向き、前者はサービス境界での置換に向きます。両者を組み合わせて、まず内部で抽象化し、その後サービスとして切り出すというアプローチも有効です。

Parallel Run で正しさを検証

決済計算のような業務クリティカルな箇所では、新旧両方を実行して結果を比較する Parallel Run(GitHub の Scientist ライブラリが有名)が役立ちます。本番のリアルなトラフィックで旧と新の出力差分を測定し、ずれがゼロになってから新ロジックに切り替えます。

async function parallelRun<T>(
 legacy: () => Promise<T>,
 candidate: () => Promise<T>,
 compare: (a: T, b: T) => boolean,
): Promise<T> {
 const legacyResult = await legacy();
 candidate()
 .then((c) => {
 if (!compare(legacyResult, c)) {
 logMismatch(legacyResult, c);
 }
 })
 .catch(logCandidateError);
 return legacyResult;
}

組織への影響

技術的に正しくても、組織が旧システムの保守チームと新システムの開発チームに分断されると移行は遅れます。Conway の法則を考慮し、移行プロジェクトでは両チームを統合するか、強い橋渡し役を置くことが重要です。

「終わらせる」ためのチェックリスト

  • レガシーへのトラフィックが 0% に近づいているか
  • 新システムが本番運用で十分な期間安定しているか
  • レガシーの停止に依存する別システムがないか
  • データの最終マイグレーションは完了しているか
  • 旧システムのインフラ廃止計画が立っているか
  • ドキュメントとチーム知識は新システムに移管されたか

これらをすべて満たして初めて「移行完了」と宣言できます。

段階別の実践ガイド

フェーズ 0: 準備期

  • レガシーシステムの依存関係マップを作成
  • ビジネスドメインのコンテキスト境界を特定
  • 主要メトリクス(リクエスト数、エラー率、レイテンシ)のベースラインを取得
  • 移行後の理想形(To-Be アーキテクチャ)の素描を作成
  • ステークホルダーへの説明と合意形成

この段階で「何が動いているか分からない」という状態を解消することが、後のすべての工程の前提になります。

フェーズ 1: ファサード導入期

  • リバースプロキシまたは API ゲートウェイを設置
  • すべての外部トラフィックをファサード経由に切り替え
  • ファサード自体の可観測性(メトリクス・ログ・トレース)を整備
  • カナリアデプロイの仕組みを準備

ここでは新システムをまだ作りません。「コントロールできる状態」を作るのが目的です。

フェーズ 2: 最初の機能切り出し期

  • 最も価値が高く、依存が少ない機能を選定
  • 新サービスを実装し、Shadow モードで本番トラフィックを並行処理
  • 結果の差分を監視し、十分な期間問題がないことを確認
  • カナリーリリースで徐々にトラフィック比率を上げる
  • 最終的に 100% を新サービスに切り替え

最初の成功は組織の自信になります。慎重に選んで確実に成功させます。

フェーズ 3: 加速期

  • 並行して複数の機能を移行
  • 共通のテンプレートやプラットフォームを整備
  • データ同期戦略(CDC、共有 DB、二重書き込み)を統一
  • レガシー側への新規開発を凍結

ここで失敗パターンが「移行ペースが上がらない」「新規追加が止まらない」です。経営層を巻き込んで凍結ルールを徹底します。

フェーズ 4: 終了戦

  • 残った機能の置換難易度を評価
  • 場合によってはレガシーロジックをそのまま新環境に持ってくる「リフト&シフト」を許容
  • レガシーインフラの停止計画を策定
  • 旧 DB のアーカイブ、ドキュメント整理、運用引継ぎ

「終わり」を曖昧にしないことが、ストラングラーフィグ最大の課題です。

さらに踏み込んだトピック

組織と Conway の法則

ソフトウェアアーキテクチャは組織構造を反映するという Conway の法則があります。レガシーをマイクロサービスに分解しても、組織が縦割りなら境界が綺麗にならず、結局モノリスのような相互依存に逆戻りします。Inverse Conway Maneuver(理想のアーキテクチャに合わせて組織を設計し直す)を併用するのが理想です。

ブラウンフィールド開発との対比

Greenfield(新規開発)と Brownfield(既存改修)では難易度が桁違いです。ストラングラーフィグは Brownfield 開発の代表格であり、技術選定よりも「既存の制約とどう折り合いをつけるか」のスキルが問われます。ロックイン、暗黙仕様、廃止できない機能、文化的抵抗との戦いが日常になります。

失敗例から学ぶ

著名な失敗例として、Knight Capital の 2012 年の事故があります。古いコードを物理的に削除せずデプロイしたことで、リリースの一部サーバーが古いロジックで動き続け、45 分で 4.4 億ドルの損失を出しました。ストラングラーフィグでは「新旧が共存する状態」が長期間続くため、デプロイ手順とフィーチャートグルの厳格な管理が極めて重要であることを示す教訓です。

補助テクニックの紹介

Event Interception

レガシーがイベントを発行できない場合でも、データベース層でトリガーや CDC を使ってイベントを「合成」することができます。これにより、レガシー本体に手を入れずに新システムをイベント駆動で構築できます。

Asset Capture

レガシーの内部状態を定期的にスナップショットし、新システムに渡すアプローチです。リアルタイム同期が不要な参照系から始めるのに適しています。

Read Through Caching

新システムのデータベースを「キャッシュ」として位置づけ、見つからなければレガシーから取得して保存する方式です。徐々にデータが新システムに集まり、最終的に切り離せます。

まとめ

ストラングラーフィグパターンは銀の弾丸ではありません。長期間の組織的な努力を要し、技術以上に「終わらせる意志」「経営層の支援」「文化の変革」が成功の鍵を握ります。しかし、ビッグバン置換と比べて圧倒的にリスクが低く、ビジネスを止めずに進められる現実的な戦略です。レガシーは敵ではなく、長年ビジネスを支えてきた資産です。敬意を持って、計画的に、確実に新しい姿へと置き換えていきましょう。

注意: レガシーシステムの全機能を一度に移行しようとすると、ビッグバン置換と同じリスクを抱えます。機能単位で段階的に移行することが重要です

実践メモ: 移行の優先順位は「ビジネス価値が高い」かつ「技術的に移行しやすい」機能から始めるのが効果的です。

参考リソース

← 一覧に戻る
PR
PR
PR
PR