レガシーな EC サイトや基幹業務システムを刷新する際、全体を一括で作り直す「ビッグバンリプレイス」のリスクを避け、機能を少しずつ新しい基盤へ移していく ストラングラーフィグパターン(Strangler Fig Pattern) の採用が一般化してきました。

リバースプロキシや API Gateway を前段に配置し、「商品検索」「会員認証」「カート」といったエンドポイント単位で段階的にリクエストを新システムへ振り分ける構成は、アーキテクチャ図としても非常にわかりやすく、チーム内での合意も比較的容易です。

しかし、いざ実装フェーズに入ると、多くの開発チームでピタッと手が止まる瞬間が訪れます。それが 「新旧システムが同時に動く期間、データベースとデータをどう同期・整合させるか」 という問題です。

ストラングラーフィグパターンにおける並行稼働とは、レガシーシステムと新システムを一時的に共存させ、リクエストを段階的に振り分けながら、データ整合性と切り戻し可能性を維持する過渡的アーキテクチャ(Transitional Architecture)です1。

以前公開した EC サイトのリプレイス判断基準に関する記事 では並行稼働フェーズの概要に触れ、Perl 製 EC の近代化に関する記事 では「新旧システムが一時的に共存する期間は、二重管理のコストがかかり無駄に見えるかもしれないが、全面リプレイスのダウンタイムリスクに比べればはるかに小さい」と書き残しました。

本記事では、その「二重管理」の中身に踏み込み、段階的移行の成否を分ける過渡期のデータ設計、二重書きの落とし穴、エンティティごとのマスタ配置、そして切り戻しを担保する手順を具体的に解説します。

なぜ「素朴な二重書き(Dual Write)」は破綻するのか

並行稼働を設計する際、最初に思い浮かびがちなのが「アプリケーションのコード内で、旧 DB と新 DB の両方に書き込めばよいのではないか」というアプローチです。いわゆる デュアルライト(Dual Write / 二重書き) です。

一見するとシンプルに見えるこの手法ですが、分散トランザクション(2相コミット: 2PC)を用いない限り、実務では極めて高い確率で破綻します23。その理由は主に 3 つあります。

1. パーシャルフェイル(部分的な書き込み失敗)

アプリケーションから「旧 DB に INSERT 成功 → 新 DB に INSERT 実行」という順で処理を行う場合、後者の新 DB への書き込みがネットワーク瞬断、タイムアウト、バリデーションエラーなどで失敗した瞬間に不整合が生じます。

旧 DB 側はすでにコミットが完了しているため、アプリケーション側から自動でロールバックすることはできません。この結果、旧システムにしか存在しないレコードが生まれ、データの乖離(Data Drift)が始まります。

2. 順序逆転(Race Conditions)

ほぼ同時に発生した 2 つの更新処理(例: ユーザーによる住所変更 A と、直後の再変更 B)が並行して実行された場合、スレッドのスケジューリングやネットワーク遅延により、書き込みの順序が入れ替わることがあります。

旧 DB には A → B の順で適用され最終値が B になったにもかかわらず、新 DB には B → A の順で届いてしまい最終値が A に上書きされるケースです。エラーログには何も残らないため、後からデータが食い違っていることに気づくという最も厄介な障害を引き起こします。

3. リトライによる副作用の増幅

新 DB への書き込みがタイムアウトした際、アプリケーション側で安易にリトライを行うと、さらに問題が複雑化します。「新 DB 側では書き込みが正常に完了していたが、完了応答のパケットだけがロストした」という状況でリトライを実行すると、重複レコードの作成や二重決済、二重引き当てといった深刻な副作用につながります。

このように、アプリケーション層での素朴な二重書きは、分散環境におけるアトミック性と順序性を保証できないため、並行稼働の基礎設計として採用すべきではありません。

エンティティの特性で決める「正(System of Record)」の配置と移転順序

データ不整合の罠を避けるための大原則は、「Single Writer Principle(単一書き込み原則)」 です。過渡期であっても、特定のエンティティに対する更新の「正(System of Record: SoR)」は常にどちらか一方のシステムに限定し、新旧両方から同時に書き込める状態(Multi-Writer)を絶対に作らない設計にします。

EC や基幹業務システムで扱われる代表的な 3 つのエンティティ(会員・注文・在庫)は、更新頻度や整合性の要件が全く異なるため、エンティティごとに正マスタの配置と移行順序を撃ち分ける必要があります。

1. 会員データ(Users): 「新先行 + JIT パスワード移行」

会員プロフィールやログイン認証情報は、更新の衝突(コンフリクト)が起こりにくいデータです。同一ユーザーの情報が同時に別の場所から更新されるリスクが極めて低いため、比較的早い段階で新システム側を正(SoR)に昇格させることができます。

ここで課題になるのが 「パスワードのハッシュ移行」 です。旧システムのハッシュ関数(例: MD5 や SHA-256)から、新システムで推奨される強固なハッシュ関数(例: Argon2id や bcrypt)へ切り替える場合、データベース管理者は平文のパスワードを保持していないため、事前に一括変換して移行することができません。

この解決策として広く採用されているのが、Just-In-Time(JIT)パスワード移行(Lazy Migration) です4。

  1. 会員基本情報(ID、メールアドレスなど)は事前に旧 DB から新 DB へバックフィルしておく
  2. ユーザーが新システムでログインを試みた際、新 DB 内に新方式のパスワードハッシュが存在しなければ、入力された平文パスワードを使って裏側で旧システムの認証 API(または認証スクリプト)を呼び出す
  3. 旧システム側で認証が成功した場合、その場で新ハッシュ関数を用いてパスワードハッシュを計算し、新 DB に保存する
  4. 以降のログインは、新 DB 単独で認証が完結する

一方向ハッシュ化されたパスワードは、アルゴリズムを完全に同じにしない限り、複数システム間で相互に参照・共有することは不可能です。そのため、新システムで一度ログインし新アルゴリズムでハッシュ化された会員データは、旧システム側へハッシュのまま逆同期して戻すことができなくなります。この不可逆なデータ変換の発生タイミングは、後述する切り戻し設計において極めて重要な境界となります。

2. 注文データ(Orders): 「切り替え日時の境界分割(Cut-off Seam)」

注文データは、基本的に一度作成されたら行自体が大幅に書き換わることのないイミュータブル(Append-Only)な特性を持っています。また、注文から決済、出荷、キャンセル・返品受付といった一連のライフサイクルは、通常数日から数週間で完了します。

そのため、注文データは新旧間で複雑な双方向同期を組むのではなく、「切り替え日時 T による厳密な境界分割(Cut-off)」 を行うのが最も安全です。

  • 切り替え日時 T より前の注文: 旧システムが正(SoR)。出荷処理、キャンセル、返金、問い合わせ対応はすべて旧システムの管理画面で最後まで完結させる
  • 切り替え日時 T 以降の注文: 新システムが正(SoR)。新しい決済・注文管理フローで処理する

なお、ユーザーがマイページなどで過去の注文履歴を一覧表示したいという要件に対しては、過去注文データを「参照専用(Read-Only)」として新 DB に初期同期しておくか、参照専用の API 経由で旧データを取得して表示します。更新系トランザクションを旧システム側に閉じ込めておくことで、新旧を跨ぐ分散トランザクションを完全にゼロにできます。

3. 在庫データ(Inventory): 「最後まで旧集約、または独立サービス先行切り出し」

3 つの中で最も慎重な扱いが求められるのが在庫データです。在庫の引き当ては、高頻度な更新、厳密な排他制御(行ロック)、ACID トランザクションが必須であり、わずかな不整合も二重販売(オーバーセリング)や欠品という直接的な事業被害につながります。

在庫データに関しては、「全機能の移行が完了する最終段階まで、旧システムの DB に正(SoR)を集約し続ける」 か、あるいは移行の最初期に 「在庫引き当て機能だけを独立したマイクロサービスとして切り出し、新旧両方からそのサービスを呼び出す」 の二択にする必要があります。

過渡期において新フロントエンドや新注文システムから購入が発生した場合でも、新システム側には在庫テーブルを持たせず、旧システムの在庫引き当て API を同期呼び出しする構成をとります1。これにより、過渡期であっても在庫の Single Writer が維持され、引き当ての不整合を確実に防ぐことができます。

並行稼働を安全に乗り切るデータ同期パターン

エンティティごとの正マスタが決まったら、過渡期における新旧間のデータ同期方式を選択します。

同期パターン整合性の保証方式実装・運用の複雑さ主な適用先
パターンA: 片方向同期 + 差分突合バッチ結果整合性(非同期・バッチ)低〜中(追加インフラ不要)中小規模の業務システム、EC
パターンB: Transactional Outbox + CDCイベント駆動による結果整合性高(Kafka/Debezium などの運用が必要)大規模トラフィック、高頻度更新システム
パターンC: 共有データベース(Shared DB)強整合性(同一 DB へのアクセス)低(ただしスキーマ密結合のリスク大)短期間の移行、同一言語での書き換え

中小規模のシステムや、専任のインフラチームがいない環境では、高度なメッセージング基盤を組むよりも 「パターンA: 片方向同期 + 初期バックフィル + 差分突合バッチ」 が最も堅実で費用対効果の高い選択肢となります。

パターンA: 片方向同期と差分突合バッチ(現実解)

片方向の非同期同期も、書き込みが片側で落ちうる点では二重書きと変わらないように見えます。しかし両者は決定的に違います。片方向同期の失敗は「正(SoR)から見て反映が遅れている」状態にすぎず、正を基準に再送・突合すれば必ず収束します。二重書きが致命的なのは、どちらが正しいかを決める基準そのものが存在せず、乖離したときの収束先を定義できない点にあります。

正(SoR)となっているシステム側から、他方のシステムへ向けて片方向のみデータを流し込みます。

  1. 初期バックフィル: 移行開始時に、旧 DB のスナップショットを新 DB へ一括投入する
  2. 片方向レプリケーション: 旧システムで発生した更新を、定期バッチまたは非同期イベントで新システムへ反映する
  3. 差分突合バッチ(Reconciliation): 毎晩深夜に、主キーや更新日時、レコードのハッシュ値を新旧間で突き合わせ、万が一乖離があれば自動修復またはアラート通知を行う

パターンB: Transactional Outbox パターンと CDC(大規模向け)

更新頻度が高く、秒単位での準リアルタイム同期が求められる場合は、Transactional Outbox パターン と Change Data Capture(CDC) を組み合わせます56。

アプリケーションは、ビジネスデータの更新と同一のローカルデータベーストランザクション内で、outbox テーブルに変更イベントを INSERT します。

-- ビジネスデータの更新と同一トランザクションで実行
BEGIN;

UPDATE users
SET email = 'new-email@example.com', updated_at = NOW()
WHERE id = 'usr_123';

INSERT INTO outbox (id, aggregate_type, aggregate_id, type, payload)
VALUES (
  'evt_abc123',
  'User',
  'usr_123',
  'UserEmailUpdated',
  '{"email": "new-email@example.com"}'
);

COMMIT;

データベースのトランザクションログ(PostgreSQL の WAL、MySQL の binlog)を Debezium などの CDC エンジンが直接監視し、コミットされたイベントだけを確実にメッセージブローカー(Kafka など)へストリーミング配信します78。

アプリケーション層での Dual Write を完全に排除できるため、パーシャルフェイルを起こさずに安全な非同期同期を実現できます。

締め処理(日次売上確定・棚卸)が壊れる境界と対策

並行稼働中に最も破綻しやすいのが、深夜の日次売上締めや在庫棚卸といった「集計バッチ処理」です。

非同期レプリケーションを採用している場合、わずか数秒のレプリケーション遅延(Replication Lag)であっても問題を引き起こします。例えば、23時59分50秒に新システム側で発生した注文が、数秒の遅延によって旧 DB にまだ届いていない状態で、旧システムの 23時59分59秒の締めバッチが走ってしまうケースです。この注文は当日の日次売上から漏れ、翌日分として計上されてしまい、会計上の売上不一致が発生します。

この破綻を防ぐための鉄則は次の 2 つです。

  • 締め処理の責任システムを一箇所に固定する: 新旧両方のデータベースから中途半端に部分集計した数字を後から足し合わせるようなバッチは絶対に作らない
  • 同期遅延のゼロ確認(Watermark): 締めバッチを起動する前に、レプリケーションの遅延がゼロであること(または締め時刻以降のタイムスタンプのデータが届いていること)をプログラムで検証してからバッチ処理を開始する

「切り戻し(ロールバック)」を担保する逆同期とリストア戦略

ストラングラーフィグパターンの最大の利点は「問題が起きたらいつでも旧システムへ戻せる」という安心感にあります9。しかし、トラフィックを新システムへ切り替えた後、新システムで発生した更新データを旧システムへ戻す 「逆同期」 が設計されていなければ、安全な切り戻しは成立しません。

1. 逆同期(新→旧)とエコーループ防止

ここで Single Writer 原則との関係を整理しておきます。原則が求めるのは「1 つのエンティティについて、ある時点の正(SoR)が常に片側だけであること」であり、逆同期はカットオーバーによって正が新システムへ移った結果、同期の向きが反転したものです。エンティティごとに移行フェーズが異なるため、システム全体を俯瞰すると旧→新と新→旧が同時に流れているように見えますが、エンティティ単位で見れば正は一意のままです。

新システムへ本番トラフィックを切り替えた(Cutover)瞬間から、今度は「新システムで発生したデータ更新を旧システムへ書き戻す逆同期」を即座に稼働させ、旧システムを、いつでもリクエストを受け付けられる「ホットスタンバイ」状態に保ちます。

ここで注意すべきが エコーループ(Echo Loop / ピンポン同期) です。「旧 DB の変更 → 新 DB へ同期」と「新 DB の変更 → 旧 DB へ逆同期」が同時に動いていると、同期ジョブによる書き込み自体が新たな変更イベントとして検知され、データが新旧間を無限に往復してしまいます。

逆同期を実装する際は、同期ジョブによる書き込みトランザクションに origin_source: "sync_job" といったコンテキストメタデータを付与し、同期による変更はレプリケーションの検知対象から除外するフィルタリングが必須となります。

2. 切り戻し不能点(Point of No Return: PNR)の合意

どれほど入念に逆同期を設計しても、「技術的に旧システムへ戻せなくなるデッドライン(Point of No Return: PNR)」 は必ず存在します。

代表例が、前述した「新ハッシュ関数で保存されたパスワード」や、「新システム特有の複雑なデータ構造(旧システムのテーブル定義にはマッピングできない新決済種別や新ポイントルール)」です。

一度これらの不可逆なデータが新システムで発生した後は、旧システムへトラフィックを戻しても、ユーザーがログインできなくなったり注文データが壊れたりします。

ここで注意したいのは、会員データを「新先行 + JIT パスワード移行」で進める設計は、PNR を移行の早い段階へ前倒しする判断でもあるという点です。新システムで最初のユーザーがログインした瞬間から不可逆な変換が始まるため、「いつでも旧システムへ戻せる」期間は実質そこで終わります。切り戻しの猶予を長く取りたい場合は、会員の切り替えを移行の後半に回し、それまで認証を旧システムに残すという選択肢と天秤にかけてください。

移行計画を立てる際は、「何が起きたら PNR に到達するか」 をステークホルダー全員であらかじめ合意しておく必要があります。PNR に到達する前に重大障害が発生した場合は即座に旧システムへ切り戻し(Rollback)、PNR 到達以降に障害が発生した場合は、旧システムへ戻すのではなく新システム上で緊急パッチを適用して前進修復(Fix Forward)するという明確な運用基準を定めておきます。

3. 最悪時のテーブル絞り込みリストア

想定外のデータ破損が発生し、最悪の手段としてデータベースを移行前のスナップショットからリストアせざるを得ない事態も想定しておく必要があります。

しかし、数 GB から数百 GB におよぶデータベース全体を一括でリストアしようとすると、数時間から数日単位のサービス停止が発生し、事業に致命的な打撃を与えます。

そのため、リストア手順を準備する際は、全量復旧を前提とせず、「不整合が発生した対象テーブル(会員テーブルや特定マスタなど)に絞り込んでリストアし、差分を復旧する」 手順を確立しておくことが重要です。その際、RDBMS の外部キー制約(Foreign Key)によって不整合レコードの投入が阻害されないよう、関連テーブル群をまとめたリストア境界の設計や、一時的な制約無効化手順も含めて事前に検証しておきます。

FAQ: ストラングラーフィグのデータ移行に関するよくある質問

Q1: なぜアプリケーション層の Dual Write(二重書き)は避けるべきなのか?

A1: アプリケーション層での二重書きは、2相コミット(2PC)を用いない限りアトミック性を保証できません。片側書き込み失敗時のパーシャルフェイルや、ネットワーク遅延による書き込み順序の逆転(Race Conditions)によって、エラーログに残らないサイレントなデータ不整合(Data Drift)を引き起こすためです。

Q2: 新旧システムが並行稼働している期間中、在庫データはどちらを正(System of Record)とすべきか?

A2: 在庫引き当ては二重販売(オーバーセリング)を防ぐための厳密な排他制御と ACID トランザクションが必要なため、新旧間で分散させず、移行プロセスの最終段階まで旧システムに正を一元集約するか、先行して独立した在庫専用 API サービスへ切り出して Single Writer を維持すべきです。

まとめ: 移行計画に「データの正と境界」を書き込む

ストラングラーフィグパターンによる段階的移行の成否は、API Gateway のルーティング設定やフロントエンドの分離といった目に見える部分ではなく、過渡期における地味で堅実な 「データの正(マスタ)の決定」「不可逆境界の把握」「切り戻し手順の確立」 で決まります。

移行計画を具体化する際は、以下のチェックリストを用いてチーム内の認識を点検してみてください。

  • 会員・注文・在庫などの主要エンティティごとに、過渡期の正(System of Record)がどちらか明確に定義されているか?
  • アプリケーション層での素朴な二重書きを排除し、片方向同期または Transactional Outbox パターンを採用しているか?
  • パスワードのハッシュ変更など不可逆な変換を特定し、切り戻し不能点(PNR)の到達条件が合意されているか?
  • 日次締め処理や在庫引き当てのトランザクション境界が新旧を跨いで破壊されていないか?
  • 万が一の障害発生時に、新→旧への逆同期やテーブル絞り込みリストアによる復旧手順が定義されているか?

レガシーシステムの段階的移行や過渡期のデータ設計、実装境界の切り分けについてお悩みの方は、ぜひお気軽に コンタクトフォーム からご相談ください。貴社のシステム構成と業務フローに合わせた現実的な移行計画の策定を伴走支援いたします。

Footnotes

  1. Transitional Architecture Ian Cartwright, Rob Horn, James Lewis (Thoughtworks), 2021年4月12日。本記事では2箇所で参照している。並行稼働を「移行完了時には破棄される過渡的アーキテクチャ」と定義する箇所と、在庫の同期呼び出しのような一時的な同期機構を設計へ明示的に組み込む箇所の、いずれも同記事が説く「移行完了時に不要となる足場(scaffolding)を意図して設計する」という原則に依拠する。 ↩ ↩2

  2. Transactional outbox pattern Amazon Web Services (AWS Prescriptive Guidance), 2022年。アプリ層での二重書き(Dual Write)が引き起こす分散整合性の欠如とパーシャルフェイルのメカニズムを解説。 ↩

  3. Pattern: Transactional outbox Chris Richardson (microservices.io)。RDBMS のローカル ACID トランザクションと Outbox テーブルを組み合わせた整合性保証パターンの原典。 ↩

  4. Migrate user Lambda trigger Amazon Web Services (Cognito Developer Guide), 2024年。初回ログイン時の平文パスワード検証と新ハッシュ再計算による透過的 JIT 移行仕様。 ↩

  5. Pattern: Transaction log tailing Chris Richardson (microservices.io)。データベーストランザクションログ(WAL/binlog)監視による CDC レプリケーション手法。 ↩

  6. Pattern: Polling publisher Chris Richardson (microservices.io)。Outbox テーブルのポーリングによるメッセージ発行とトレードオフ。 ↩

  7. Debezium Architecture Debezium Community (Red Hat), 2024年。ログベース Change Data Capture のアーキテクチャと Kafka Connect 連携。 ↩

  8. Outbox Event Router Debezium Community (Red Hat), 2024年。Outbox テーブルレコードを Kafka トピックへルーティングする SMT の仕様。 ↩

  9. Decompose a monolithic application into microservices by using the Strangler Fig pattern Amazon Web Services (AWS Prescriptive Guidance), 2023年。Transform / Coexist / Eliminate の 3 フェーズライフサイクルと並行稼働。 ↩