この記事の要点
• ストラングラーフィグパターンはレガシーシステムを段階的に新システムに置き換える手法
• ファサード(プロキシ)を設置して新旧システムを共存させながら移行する
• ビッグバン置換と異なりリスクを最小化しながら近代化を実現
ストラングラーフィグパターンとは
ストラングラーフィグパターン(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;
}
メリット
- リスクの局所化: 機能単位で本番投入し、問題があれば局所的にロールバック可能
- 継続的な価値提供: 移行中もユーザーが新機能を享受できる
- 段階的な学習: 新システムの設計を運用しながら洗練できる
- 撤退可能性: いつでも元に戻せる安心感
- チームの並行作業: 機能単位で分担できる
デメリット
- 移行期間の長期化: 一気に終わらせる場合より総時間は長くなりがち
- 二重メンテナンス: 新旧両方を保守する負担
- データ整合性の複雑さ: 同期戦略の設計と運用が必須
- インフラコスト: ファサード層・両システムの並行稼働
- 完了の判断が難しい: 「最後の数%」が長く残るリスク
ユースケース
- モノリスからマイクロサービスへの移行: 機能境界を切り出して個別サービス化
- 言語・フレームワークの刷新: PHP から Go へ、Java から Kotlin へ
- オンプレミスからクラウドへ: 機能別にクラウド移行
- データベースの置き換え: Oracle から PostgreSQL へ
- 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 年残ったレガシーが、次の 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
新システムのデータベースを「キャッシュ」として位置づけ、見つからなければレガシーから取得して保存する方式です。徐々にデータが新システムに集まり、最終的に切り離せます。
まとめ(追記)
ストラングラーフィグパターンは銀の弾丸ではありません。長期間の組織的な努力を要し、技術以上に「終わらせる意志」「経営層の支援」「文化の変革」が成功の鍵を握ります。しかし、ビッグバン置換と比べて圧倒的にリスクが低く、ビジネスを止めずに進められる現実的な戦略です。レガシーは敵ではなく、長年ビジネスを支えてきた資産です。敬意を持って、計画的に、確実に新しい姿へと置き換えていきましょう。
注意: レガシーシステムの全機能を一度に移行しようとすると、ビッグバン置換と同じリスクを抱えます。機能単位で段階的に移行することが重要です。
実践メモ: 移行の優先順位は「ビジネス価値が高い」かつ「技術的に移行しやすい」機能から始めるのが効果的です。
参考リソース
- Martin Fowler - StranglerFigApplication
- Microsoft Azure Architecture Center - Strangler Fig pattern
- AWS Prescriptive Guidance - Strangler Fig pattern
- Martin Fowler - BranchByAbstraction