ZOZOTOWNカート・決済システムの大規模リプレイス 〜 モジュラモノリス設計で進めた現実的リプレイス戦略

ZOZOTOWNカート・決済システムの大規模リプレイス 〜 モジュラモノリス設計で進めた現実的リプレイス戦略

こんにちは、カート決済部カート決済基盤ブロックの多田と三浦です。
普段はZOZOTOWN内のカート機能や決済機能の開発、保守運用、リプレイスを担当しています。

これまでにカート決済サービスのリプレイスでは在庫データのクラウドリフトやクレジットカード決済の非同期化を実現しています。

techblog.zozo.com

techblog.zozo.com

本記事では、ZOZOTOWNにおける「カート投入から注文作成まで」のシステムを、Classic ASPからJavaへ移行した取り組みについてご紹介します。一部機能は既にリリース済みで、現在も段階的な移行を継続中です。
このプロジェクトは、モノリシックなアプリケーションからマイクロサービスアーキテクチャへと段階的に移行する取り組みの一環として進めてきました。その過程で、どこでサービスを分けるべきかという課題に直面しました。安易な分割は機能間の依存を複雑化させる一方で、分割を避けすぎるとモノリスのままとなり、保守性は向上しません。
こうした難しさに向き合う中で、私たちはシステムの責務やドメイン構造を明確にする手段として、イベントストーミングを導入することにしました。

きっかけは、社内で実施された「イベントストーミング会」でした。興味を持ったメンバーが何度か参加するうちに、「自分たちの担当領域のカート投入から注文作成でも試してみよう」という声が自然と上がるようになりました。ちょうど新たなメンバーがチームに加わったタイミングでもあり、ドメイン全体を俯瞰する手段としても有効だと考えました。また、新たな手法に挑戦すること自体がエンジニアとしてのスキル向上につながり、チーム全体で取り組むことはチームビルディングの面でもプラスになると判断しました。
イベントストーミングを実施した結果、いきなりマイクロサービスへ分割するのではなく、まずはモジュラモノリスとしてシステムを構築する方針を選択しました。
以下では、その判断に至った背景や、実際の取り組み内容、そして得られた気づきについて詳しくご紹介します。

イベントストーミング

イベントストーミング概要と目的

イベントストーミング(Event Storming)とは、主にソフトウェア開発や業務プロセスの設計で使われるワークショップ手法です。関係者全員が集まり、システムや業務の流れを「イベント(出来事)」を中心に可視化・整理します。

私たちは以下の目的でこの手法を導入しました。

  • コンテキスト境界を明確にする。
  • カート/注文フローの全体像を把握し、チーム間で認識を統一する。
  • ドメイン知識を持つメンバーと新規参入メンバーとの間で知識を共有し、リプレイスを円滑に進める。

コンテキスト境界とは

bounded context
A description of a boundary (typically a subsystem, or the work of a particular team) within which a particular model is defined and applicable.

境界づけられたコンテキスト(Bounded Context)とは、特定のモデルを定義・適用する範囲を明示的に示したものです。
代表的な境界の例は、サブシステムやチームなどです。

引用:公式DDD Reference

一方で「コンテキスト境界」は、この境界づけられたコンテキスト同士の境界線や接触部分を指します。それぞれのコンテキストがどこまでの責務を持ち、他のコンテキストとどのように連携するかを定める境界のことです。

例えば、モジュラモノリスでは「モジュール」単位を基本として、マイクロサービスでは「サービス」単位を基本としてコンテキスト境界を設定することが一般的です。ただし、必ずしも1:1対応するわけではなく、ドメインの特性や組織の状況に応じて柔軟に設定されます。

イベントストーミングの進め方

1.ブレインストーミングする

  • 各自、思いつく限りのドメインイベントを付箋にそれぞれ記載する。
  • 重複しても問題なく、まずは記載する。

ブレスト この段階でイベントに抜けや漏れがあっても気にする必要はありません。後のステップで整理・補完していくため、まずはシステム全体の流れや構造をざっくりと把握することを目的としています。

2.イベントを時系列に並べる

  • 時系列に並べる。
  • 同じイベントを重ねる(可能であれば、重複削除する)。
  • 不足しているイベントがあれば付箋を追加する。 タイムライン このステップの目的は、システムや業務プロセスの全体的な流れや構造を明確にすることです。
    イベントを時系列で整理することで、プロセスの前後関係や依存関係が可視化されます。これにより、関係者全員が同じ視点で全体像を把握できるようになり、抜けや重複、認識のズレなども発見しやすくなります。

3.カラーパズルをする

  • 時系列に並べたイベントの付箋に対して、アクターやコマンド、エンティティなどの情報を追加する。
  • 「アクター」「どのような操作や指示(コマンド)」をきっかけにイベントが発生したのかを色分けした付箋やラベルで整理する。
  • その時に関わるエンティティやシステムの状態なども併せて記載する。

カラーパズル

  • アクター(Actor):イベントを引き起こす主体。システムのユーザーや外部サービス、管理者などが該当する。
  • コマンド(Command):アクターがシステムに対して行う操作や指示。例として「商品をカートに追加する」「注文を確定する」などの具体的なアクションが含まれる。
  • エンティティ(Entity):システム内で管理される情報やオブジェクト。例えば「カート」「注文」「商品」などが該当する。

このステップの目的は、イベント同士の関係性や、業務・システム内での役割分担、データの流れをより明確にすることです。色分けによって視覚的にも分かりやすくなり、複雑なビジネスルールを直感的に理解できるようになります。

4.コンテキスト境界を定める

  • 業務イベントやユースケースの流れをもとに、責任や関心ごとが明確に分かれるポイントで境界を設定する。
  • 既存システムの強い依存(ストアドプロシージャーによるトランザクション管理)がある領域は、現段階では無理に分割せず、将来的な分離を見据えて大きめにまとめる。
  • 頻繁に一緒に変更される機能や連携の強い領域は同じコンテキスト内に収めることで、保守性や開発効率を高める。

最終的に以下のようにコンテキスト境界を定めました。 コンテキスト境界

イベントストーミングをやってみて

ワークショップ形式で実施したことで、ドメイン知識を吸収しやすいと感じました。長年カート決済システムに携わってきたメンバーが持つ暗黙知や個人の頭の中にあったビジネスルールを新規参入メンバーを含むチーム全体で共有・明文化できました。また、各メンバーがそれぞれ異なる視点で捉えていたコンテキスト境界を図解することで、具体的な合意形成ができました。抽象的だった「責務の分担」が視覚的に整理され、モジュール設計の方向性について納得感のある議論ができたのは大きな収穫でした。
一方で、オンラインでの開催はどうしても発言者が限られがちで、対面に比べて議論が活発になりにくい印象を受けました。特に複雑なテーマでは、オフラインで1日かけてじっくり議論したほうが、多くの意見を引き出しやすく、全体の理解も進みやすいと感じました。

また、既存システムを題材にした場合、全体像を把握するのに想定以上の時間がかかりました。実際、週1回1時間のペースで進めて約2か月かかりました。なお、この期間はイベントストーミングに専念していたわけではなく、メンバーはそれぞれ他の業務と並行して取り組んでいたため、じっくり時間をかけて進めた形になります。

モジュラモノリスを採用した理由

イベントストーミングによってカート決済フローの全体像とコンテキスト境界を整理できたことで、次にアーキテクチャの選択に移りました。
マイクロサービスとの比較検討の結果、私たちがモジュラモノリスを採用した理由をご説明します。

メリット・デメリット比較

マイクロサービス モジュラモノリス
メリット スケーラビリティ・弾力性が高い
・ 高負荷なサービスのみスケール可能
テスタビリティが高い
・ 共通ライブラリのアップデート時のテスト範囲が狭い
サービス間の処理の引越しがしやすい
・ コードの移動のみで対応可能
・ インタフェースの変更が不要
デメリット 開発中に変更があった際にサービス間の処理の引越しがしにくい
・ API設計のし直し
・ オーケストレータからのリクエスト修正
・テスト用モック修正
スケーラビリティ・弾力性が低い
・ 一部モジュールのみの迅速な更新・スケーリングが難しい

マイクロサービスは運用面での恩恵が大きい一方で、開発初期の柔軟性や変更対応ではモジュラモノリスが優れていることが分かります。

当初はイベントストーミングを通じてマイクロサービスへの細分化を進める予定でした。しかし実施してみると、注文コンテキストが想定以上に大きくなりました。特に注文テーブルの責務をどのサービスが担当すべきかという課題に直面しました。
また、データの独立性やトランザクション境界を考慮したマイクロサービスの境界を定めることは困難でした。

そこで、以下の理由からモジュラモノリスを採用する方針に転換しました。

  1. 開発スピードと工数の観点:マイクロサービス化には相応の開発工数が必要だが、モジュラモノリスなら段階的な移行が可能で、リプレイスのスピードを優先可能。

  2. 組織体制との整合性:マイクロサービスではサービスごとに専任チームが望ましいが、現在のチーム体制では各サービスとチームを1対1で対応させることが難しい状況。

  3. サービス分割の投資対効果:決済処理など一部機能をマイクロサービス化することも検討した。しかし、利用者がZOZOTOWN内部に限定されることや、専用データベースへの移行が困難なことから、現時点での分割メリットは限定的と判断。

イベントストーミングで得られた業務理解を活かし、まずはモジュラモノリスとしてコンテキスト境界に基づいたモジュール分割を実施しました。今後、チーム体制の変化や事業の成長に応じて、段階的にマイクロサービス化を検討していくことも可能な設計を目指しています。

モジュラモノリスの実装

ZOZOのカート決済基盤のプロジェクト構成

使用している技術スタックは以下の通りです。

  • 言語:Java
  • フレームワーク:Spring Boot
  • ビルドツール:Gradle

カート決済基盤で採用しているモジュラモノリスの構成を簡略化すると以下の通りです。

.
┣ module 
┃  ┣ orchestrator
┃  ┣ cart
┃  ┣ order
┃  ┣ payment
┃  ┗ {module-name}
┗ core  --- 各モジュールで共通して利用するクラスの実装。

各業務機能(注文、カート、決済など)ごとにモジュールを分けており、各モジュールは基本的に単一責務を持つように設計しています。また、coreモジュールには複数のモジュールで共通して使用する要素を集約しています。具体的には、日時や文字列操作などのユーティリティクラスや、各モジュールで共通して利用する処理などです。これにより、コードの重複を避けつつ、再利用性を高めています。

orchestrator モジュールの役割

全体の構成の中でもorchestratorモジュールは、ユースケースの流れを制御する中核的な役割を担っています。複数モジュールにまたがる業務フローを統括する責任を持っているのがorchestratorです。具体的には、cartやorder、paymentといった各モジュールのアプリケーション層に依存し、それらを組み合わせて「1つの業務シナリオ」としてまとめる役割を果たします。orchestratorには以下の利点があります。

  • 各モジュールはあくまで自律的な責務を持つ。
  • orchestratorが連携のハブとなることでモジュール間の依存が複雑にならない。
  • 最終的にマイクロサービスに分離する際にも、orchestratorの統合・調整機能はそのまま活用でき、BFF(Backend for Frontendアーキテクチャ)と連携しやすい設計になっている。

build.gradleによる依存関係の制御

重要なポイントとして、モノリスに逆戻りしないための依存関係の制御があります。各モジュールには個別のbuild.gradleを配置し、依存対象を明示的に制限しています。以下はorderモジュールのapplicationserviceにおける一例です。

project(':modules:order') {
    project('applicationservice') {
        dependencies {
            implementation project(':core')
            implementation project(':modules:order:domain')
            implementation project(':modules:payment:domain')
        }
    }
}

このように記述することで依存関係を制限し、下記のような設計の意図に反する依存を防いでいます。

  • orderのアプリケーション層はcoreや自身のドメイン層、必要最低限の他モジュール(この場合はpaymentのドメイン)にのみ依存する。
  • 他のcartやorchestratorのようなモジュールへの不要な依存は不可。

モジュラモノリスやってみたメリット/デメリット

メリット

  1. 開発やテストがしやすい

    • プロセスが1つなので、環境構築やローカル開発がマイクロサービスに比べて非常に簡単。
    • 統合テストも容易で、テストコードのカバレッジを上げやすい。
    • モジュラモノリスにより柔軟で早い開発サイクルが実現できている。
      • ストラングラーフィグパターン*でリプレイスを進めているため、新旧システムで二重開発が発生しがちになる。しかし、モジュラモノリスの構造により早いサイクルでリリースができており、二重開発を可能な限り避けながら段階的に移行できている。

    *ストラングラーフィグパターンとは、リプレイス対象のシステムの機能を段階的に新しいシステムに置き換えていき、すべての機能を置き換えた後、最終的に移行元のシステムを停止する戦略である。

  2. パフォーマンスやトレーシングが楽

    • ログやトレースの収集も1つのAPIで完結し一括管理しやすい。
    • ネットワーク通信が不要なため、レイテンシが最小化される。
  3. 命名や実装の差異を早期に発見・統一できる

    • すべてのコードが1つのプロジェクト内にまとまっているため、命名の不一致や実装の重複に気づきやすい。
    • まとめてリファクタリングできる。

デメリット

  1. デプロイ単位が分割できない

    • 全体で1つのアプリケーションとして動作するため、小さな変更でも全体のビルド・テスト・デプロイが必要。
    • 特定モジュールだけを素早く更新する柔軟性には欠ける。
  2. 同じValue Object(以下VO)*が複数モジュールに現れる

    • cartモジュールとorderモジュールの両方で商品金額を扱う場合など、同じ構造のVOが各モジュール内で独立して定義される。
      • モジュール間の直接依存を避ける設計方針で、将来的にマイクロサービスへ切り出す際の独立性を保つための意図的な選択。

*Value Objectとは、値そのものを表現するオブジェクトで、同じ値であれば同一とみなす不変オブジェクトです。例えば、商品金額や日付などがValue Objectにあたります。

今後の展望

モジュラモノリスは1つのアプリケーションとして構築されているため、マイクロサービスと比べてCI実行時のテスト時間が長くなりがちです。
並列にテストを実行することで時間短縮を図っていますが、さらなる短縮と開発体験の向上に向けた取り組みを継続していきます。

また、現在は各モジュールで同じ意味を持つVOであっても、モジュールごとに命名の異なるケースが一部存在しています。
設計上の意図や責務の違いによる分離は尊重しつつも、ドメインモデル間の一貫性を高めるために、こうしたVOの見直しやリファクタリングも進めていく予定です。

今後も、段階的な改善を重ねながら、将来的なマイクロサービス化も視野に入れた柔軟なアーキテクチャを目指していきます。

まとめ

本記事では、カート決済基盤をモジュラモノリスで構築するまでの過程をご紹介しました。複雑でモノリシックなアプリケーションの分割を検討している方がいれば、参考になれば幸いです。今後リプレイスを推進していき注文フローのリプレイス完遂をしていきたいと考えています。

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー