事業展開とともに歩むモジュラーモノリス移行
はじめに
こんにちは、J-CAT株式会社でVPoEをしている山田です。
インバウンド向けにプロダクトを展開するWabunkaでは、宿泊や総合手配など事業拡大に向けて新しい領域への挑戦を進めています。事業領域の拡大に伴い、将来的に1チームですべての開発を担うのは難しくなるため、複数チーム体制を前提とした開発に備える必要性が高まってきました。
既存事業との摩擦は最小限に抑えつつ、リリース速度を落とさずに成長させていくことが求められるため、モジュラモノリス化によるドメイン分離をスモールスタートしています。
今回は、移行計画を練るための整理手順と、モジュール間連携の実装方針、ディレクトリ設計について触れたいと思います。
移行するドメインの整理
移行期間中も開発が走り続けるため、段階的な移行を前提に、どのドメインから実施するかを決める必要がありました。まずは現存するドメインをおおまかにマッピングし、依存関係を整理しました。
軽くでも図にしておくと、各ドメイン間の関係や、依存の多寡が整理でき、最初に分離しやすい箇所の判断がしやすくなります。(矢印の方向は、AがBに依存している場合、A→Bという表現をしています)
社内で実際にさまざまな手配業務を担うチームリーダーの方と実施したイベントストーミングも、境界整理の助けになりました。
ドメインマッピングと依存関係の整理
基本的には、他から依存されていないものや、依存している数が少ないドメインほど分離しやすい傾向にあります。
また、チーム分割やソフトウェアアーキテクチャの議論で必ずと言っていいほど挙がるのがコンウェイの法則(システムの設計は、組織のコミュニケーション構造を反映してしまう)です。
今回はコンウェイの法則を念頭に、事業計画・展開ストーリーを踏まえたうえで、「分離による利益」と「分離のしやすさ」の二軸で評価し、最初に着手するドメインを決定しました。
評価には以下の項目を設けました。
- 分離による利益
- 今後の事業における重要度(高いほど生産性を重視)
- 今後の見込み開発量(大きいほど分離が生産性に寄与)
- 分離のしやすさ
- 依存数・被依存数
- ドメインを跨ぐ結合度
- 求められるデータの整合性(強整合 / 結果整合)
結果として、今後注力する事業領域だが、現状他との依存関係が小さく分離しやすい「通訳手配」ドメインをパイロットとしてモジュール化を進めていくこととしました。
パイロット選定
直近は別の大型プロジェクトが走っており通訳周りに変更が入らないため、一時的に重複コードを許容すしつつ、呼び出しインターフェースの裏で並行で実装を進め、完成した段階で切り替えるというシンプルな移行になりました。(他の場所は不必要に密結合してしまっており、なかなか労力が必要な箇所もある)
モジュール間の連携における実装方針
今回のモジュラモノリス化は、将来のマイクロサービス化も見据えて構築しています。そのため、モジュール間の連携ルールを事前に整備しました。
これにより、依存関係の不透明化や単一障害点(SPOF)の発生を抑え、将来のサービス分割の障壁を下げることを狙っています。
まずはベースとして、以下を基本方針としました。
- モジュール間の独立性確保
- モジュールの循環依存はなし
- 複数モジュールを跨ぐオーケストレーションはBFF(Backend for Frontend)に集約
- 呼び出し先が壊れていることを想定した実装(SPOFの回避)
- データ整合性は結果整合で実現(分散トランザクションにつながる実装は基本なし)
- データ所有権の明確化
- データの正当性はそのオーナーモジュールが担保
- 呼び出し側は利用に必要な最小限のデータモデルを独自に定義(インターフェースの変更による影響を局所化)
- スキーマ駆動
- 連携は同期・非同期に関わらず、Protocol Buffers による定義に限定
- 変更は後方互換に限定
移行後のディレクトリ設計
もともとクリーンアーキテクチャのモノリスだったこともあり、各モジュール内のディレクトリ構造は、基本的にそのままの形を踏襲しました。
モジュラモノリス化にあたって、外部モジュールから受け取ったデータを、呼び出し側モジュール内で扱うために、 domain/module/
配下には内部のDTO (Data Transfer Object) を定義しています。あくまでも、当該モジュール内で利用するフィールドに絞ったデータモデルとし、usecase内でのデータハンドリングに利用されるような形です。
pkg/
├── modules/
│ ├── interpreter/
│ │ ├── domain/
│ │ │ ├── model/
│ │ │ └── module/ # 他モジュールへのGatewayインターフェース&DTOを宣言
│ │ ├── application/
│ │ ├── infra/
│ │ │ └── moduleimpl/ # 他モジュールの呼び出し実装
│ │ ├── presentation/
│ │ └── interpreterservice.go # 外部呼び出し用インターフェース実装
│ └── reservation/
│ └── ...
└── core/ # モジュール共通機能
外部からの呼び出しがあるインターフェース実装は、すべて interpreterservice.go
に配置しています。この interpreterservice
は実質presentation層に当たるものですが、外部公開エンドポイントがどこに置いてあるかわかりやすいようにモジュール直下に置いています。
なお、このインターフェースや受け渡すデータ型の宣言は、別のGo moduleとして管理しているProtocol Buffers用のリポジトリにて管理されています。これにより、モジュール間の直接的な参照は基本的に起こらないような建て付けになっています。
モジュール間連携の概念図
最後に
簡単ではありますが、モジュラモノリスへの移行計画から、どのような構造に決定したのかまでを紹介しました。移行はまだ始まったばかりで、事業の拡大とともに段階的に実施していきます。
J-CATでは、事業の動きを見据えながら、こうしたプロダクトの成長を一緒に作り上げていける仲間を募集しております。これからさらに盛り上がっていく観光領域で、技術を武器に事業を成長させていくことにワクワクできる方をお待ちしております。
Discussion