Upgrade to Pro — share decks privately, control downloads, hide ads and more …

0から始めるモジュラーモノリス-クリーンなモノリスを目指して

 0から始めるモジュラーモノリス-クリーンなモノリスを目指して

最近では、マイクロサービスの過剰な分割により、かえって複雑性や運用コストが増してしまう問題を受けて、モジュラーモノリスを再評価・採用する事例が増えています。
また、AIにコードを書かせたり修正を依頼する場面も増え、AIが理解すべきコンテキストを最小限に保ち、意図通りの出力を得るためには、責務が明確に分離されたモジュール構造が重要になります。この意味でも、明確な境界を持つモジュール設計を前提としたモジュラーモノリスは、今後のAI開発との親和性が高いアーキテクチャだと感じています。
しかし、モジュラーモノリスも適切に設計しなければ、モノリスの柔軟性もマイクロサービスの独立性も活かせず、結果的に複雑さだけが増してしまうという落とし穴があります。
今回はそうした背景を踏まえて、いくつかのポイントをピックアップして整理しました。
モジュラーモノリスを検討中の方や、モノリスでもクリーンな構造を目指したい方にとって、少しでも参考になれば幸いです。

Avatar for sushi-cat

sushi-cat

July 27, 2025
Tweet

Other Decks in Programming

Transcript

  1. • モジュラーモノリスでは、サービスをどのようなモジュールとして分割するかが⼀番重要なポイントになりますが、この部分だけでも様々 な思想やアプローチが考えられるため、整理した上で別途そのテーマを扱おうと思います。(今回はやや技術‧実践的な話が多めです) • ケースバイケースの事例に対する設計⽅針や経験則に基づく個⼈的な⾒解については⻘い枠線で囲って記載しています。 • DDDやマイクロサービスに関連する考え⽅が多く含まれており、それらに由来する設計パターンや⽤語が頻出します。 最近では、マイクロサービスの過剰な分割により、かえって複雑性や運⽤コストが増してしまう問題を受けて、モジュ ラーモノリスを再評価‧採⽤する事例が増えています。 また、AIにコードを書かせたり修正を依頼する場⾯も増え、AIが理解すべきコンテキストを最⼩限に保ち、意図通りの出

    ⼒を得るためには、責務が明確に分離されたモジュール構造が重要になります。この意味でも、明確な境界を持つモ ジュール設計を前提としたモジュラーモノリスは、今後のAI開発との親和性が⾼いアーキテクチャだと感じています。 しかし、モジュラーモノリスも適切に設計しなければ、モノリスの柔軟性もマイクロサービスの独⽴性も活かせず、結果 的に複雑さだけが増してしまうという落とし⽳があります。 今回はそうした背景を踏まえて、いくつかのポイントをピックアップして整理しました。 モジュラーモノリスを検討中の⽅や、モノリスでもクリーンな構造を⽬指したい⽅にとって、少しでも参考になれば幸い です。 はじめに⭐
  2. • モジュラーモノリスとは? ◦ モノリスとマイクロサービス ◦ モジュラーモノリスのアプローチ ◦ モジュラーモノリスのトレードオフ • モジュールの分割

    ◦ DDDの境界付けられたコンテキストによるモ ジュールの分割 ◦ モジュールの独⽴性 ◦ 全体のモジュールを把握する ◦ モジュールのカプセル化 ◦ DBレイヤーのモジュール性 • 構成と依存制御 ◦ ディレクトリ構成のイメージ ◦ モジュール性の維持 ⽬次 • モジュール間の通信 ◦ モジュール間の通信パターン • メソッド呼び出し • ネットワーク呼び出し • イベント通知 ◦ 同⼀プロセス内 ◦ ネットワーク越し ◦ その他の考慮事項 • フロー制御‧トランザクション ◦ フロー制御 ◦ トランザクション • DBのモジュール横断(おまけ) • 最後に
  3. モノリスとマイクロサービス モノリスのアプリケーションでは、シンプルでよく親しまれているレイヤードアーキテクチャが多く採⽤されてきました。⼀ 番の特徴としては、シンプルさによる開発スピードが挙げられます。 しかし、このアプローチではシステムが複雑化しチームが増加するにつれ以下のような課題が表⾯化してきます。 • コードサイズの肥⼤化によるメンテナンスコストの増⼤ • 複数⼈による並⾏作業の衝突 • ビジネスロジックが複雑に絡み合い変更コストの増加

    • スケーラビリティの限界や、リソースの効率的な配分が困難になる ⼀⽅で、マイクロサービスを採⽤することで、これらの問題を解消することができますが、新たな課題が発⽣します • 横断的なログ監視、デバッグなど分散アーキテクチャならではの複雑性や開発コストの増加 • サービス間の通信によるネットワーク管理 • サービスの境界が誤っていた場合の修正コストや影響が⼤きいこと。 • 設計時にサービス間の連携⽅式や境界を慎重に考慮する必要があり、そのため設計が複雑化しやすい モジュラーモノリスの話を進める前によく⽐較されるモノリスとマイクロサービスについて軽く触れていきます。
  4. 従来のモノリスではレイヤードアーキテク チャが多く採⽤され、⽔平⽅向の(技術的) 関⼼ごとの分離によるレイヤーによってn層 に分割されます。 これに対してモジュラーモノリスでは、垂 直⽅向に独⽴して動作する単位で機能ご と、もしくは機能のまとまりでモジュール 化を⾏います。 モジュラーモノリスのアプローチ UI

    Layer Application Layer Domain Layer Infrastructure Layer 機 能 A 機 能 B 機 能 C 水平方向の関心ごとの分離は技術的な観点での分割にすぎません。しかし、 実際のシステム開発では、ビジネス要件に応 じた機能の追加・変更が求められるため、ビジネス機能を中心とした垂直分割が重要になります。 これにより、システムが成長しても、コードが整理された状態で影響を最小限に抑えた変更や追加が可能になります。また、近 年のAI開発においては与えるコンテキストを最小限化 することができ、AIのアウトプット品質向上に繋げることができます。
  5. メリット • インフラ‧デプロイ管理‧運⽤の容易性 ◦ マイクロサービスと⽐較しCI/CDパイプラインの構築‧管理が容易 ◦ ログ監視‧追跡がシンプルになり、デバッグ容易性が向上する ◦ バージョン管理の⼀元化によるモジュール間の異なるバージョン依存リスクの低減 ◦

    ネットワーク通信によるオーバーヘッドが発⽣しない分、分散システムよりも効率的 • 開発のシンプルさ ◦ 単⼀のコードベース開発による実装の⼀貫性、ローカル環境の構築容易性 ◦ コード再利⽤性 ◦ モジュール化による影響範囲の限定、並⾏開発の促進 • マイクロサービスへの移⾏容易性 ◦ 独⽴したモジュールにより、システムの成⻑に合わせて段階的にマイクロサービスへの移⾏が容易 etc... モジュラーモノリスのトレードオフ
  6. モジュラーモノリスのトレードオフ 注意するべき点 • スケーラビリティ ◦ システム全体が1つのアプリケーションのため、特定モジュールのみのスケールアウトが難しい ◦ モジュールの結合度により、1つのモジュールのパフォーマンス低下やバグが全体に影響する • インフラ‧デプロイ管理‧運⽤の容易性

    ◦ リクエスト数やデータ量、処理の複雑さによってはパフォーマンスが低下する • 技術的リスク ◦ モジュール間依存の複雑化によるメンテナンス難易度の上昇 ◦ マイクロサービス同様にモジュール分割や依存関係と向き合う必要がある ◦ モジュールごとに異なる技術(⾔語やFW)の採⽤が難しい • 組織体制 ◦ チームが巨⼤化した際に並⾏開発の衝突が発⽣する可能性がある
  7. 当然ですが、アーキテクチャにはどれもメリット‧デメリットが存在します。 その中でもモジュラーモノリスは、システムが⼤きくなるまでの間は、⽣産性と保守‧運⽤のバランスが優 れた選択肢です。 注意点として挙げている中の多くがシステムの成⻑によって顕著になる問題ですが、プロダクトのフェーズ に合わせて段階的にマイクロサービスへ移⾏することで柔軟に対応が可能です。 モジュラーモノリスのトレードオフ モジュールA モジュラーモノリス期 モジュールB モジュールC

    モジュールD モジュールA モジュールB モジュールC モジュールD 段階的なマイクロサービス化 モジュールD ※モジュラーモノリスに限らず他のアーキテクチャも同様ですが、適切にモジュール(コンテキスト‧サービス)の分割や 設計を⾏わないと⼗分なメリットを得られずに、実装コストだけが増加することに注意が必要です。
  8. モジュール化の方法の1つにDDDの境界付けられたコンテキスト を利用したアプローチがあります。 ※DDDは広い範囲をカバーしているため、ピンポイントで軽く触れる程度に留めます。 境界付けられたコンテキストは、そのドメインモデルが適用される特定のコンテキストを設定することで、ドメインモデルが適用される範 囲を明確にし、他のドメインやシステムとのインタフェースの境界を定義します。 これにより、異なるコンテキスト間でのモデルの混同を避け、 各コンテキストが独立してビジネス要求にあわせて並行開発性を向上さ せることができます。 (モジュールの独立性向上 )

    例として、ECアプリケーションにおける「 商品」は、場面により異なる情報が必要となり、呼び方や振る舞いも変わります。 注文時には「 購入品」 となり、価格や個数、軽減税率対象かどうかといった情報が必要になります。一方、配送時には「 荷物」 とな り、重量や冷蔵商品か、倉庫の場所などの情報が必要になります。 このようによく似たモデルでも明確に異なる二つのモデルを境界付けて明確に分けることで意味合いの混同を防ぐことができ、言語的 な境界にもなります。 DDDの境界付けられたコンテキストによるモジュールの分割
  9. このアプローチはマイクロサービスでもよく利用されるアプローチのため、モジュラモノリスの段階から取り入れることでマイクロサービ スへの移行にも対応しやすくなります。 ( 参考 : マイクロサービス境界の特定 ) 境界付けられたコンテキストの見極めは非常に難しく、イベントストーミングなどの手法を利用して継続的に時間をかけて判断していく 必要があります。 後述でモジュール間の通信方式の紹介がありますが、実装コード上でパターンを無理やり適用することで逆に複雑度が増してしまい、

    開発体験の悪化に繋がってしまいます。 モジュラーモノリスでは、マイクロサービスと比較して境界を見直しやすいので、定期的に 見直しや調整を行っていくこと重要です。 ( 詳しいDDDの設計手法はIDDD本などをご参考ください ) 冒頭でも記載しましたが、 実際の開発では技術的要因よりもビジネス (業務)的要因でシステムを変更する場合がほとんど です。そ のためモジュール化はビジネス機能を中心に考えることでモジュール化のメリットをより大きく得られ、 DDDの境界付けられたコンテキ ストのような戦略的設計はとてもマッチするように思います。 とはいえ、他のアーキテクチャでも同様ですが 1つの方法に固執せず、時には機能特性や技術的要因、チーム体制によるモジュール 分割などプロジェクトの状況に合わせて柔軟に方針を決定することが重要です。 ※ どのようなアプローチでもモジュールの独立性だけは考慮しないとモジュラーモノリスのメリットを得ることが難しくなります。 DDDの境界付けられたコンテキストによるモジュールの分割
  10. 各モジュールは他のモジュールからの依存を最⼩限にし、独⾃の責務を持つことがとても重要です。 これにより、変更による影響範囲(依存)や認知負荷を最⼩限に留め、各モジュールで単⼀の責務の実装コードを集約することが できます。 モジュールの独⽴性 module B module C module A

    module D この例ではモジュールAは多くのモジュールに依存し、独⽴性が損 なわれている状態となっています。 また、モジュール分解が適切でなくモジュールAの中にモジュールC の関⼼事が⼊り込み、不要な依存が発⽣し、循環依存も発⽣してし まっています。 これではモジュール内の変更が他モジュールへ波及しやすく、開発 時も他モジュールの実装の詳細をキャッチアップしながら開発する 必要があります。 module A module B module C module D C 複雑な依存 最低限の依存 コンテキストの特定などにより、モジュールの境界を明確化し責務や関⼼ ごとを適切に分離させることで、依存を最低限に保ちます。 さらに、イベントなどを利⽤することで緩い結合度での依存を実現できま す。 この状態ではモジュールの影響を最⼩限に留め、開発時も必要最低限(モ ジュールの関⼼事)のみ知っていれば開発できる状態になります。 ※必要最低限の依存は許容し、重要な依存としてチーム間コミュニケーションで注意する Event 最低限必要な 依存のみ残す
  11. モジュラーモノリスは、単にモジュールとしてプロジェクトを分割するだけではメリットを⼗分に受けられません。 よくある状況として、多数あるモジュールの中で無秩序に依存が発⽣する状況で、こうなると無駄に複雑化した中での開発を強いられてし まいます。 これを防ぐためにも、どのモジュールがどのモジュールを依存しているか。モジュールの依存は適切か。を評価、確認できるよう全体像を 確認できるような図などを⽤意しメンテナンスしていくことをお勧めします。 全体のモジュールを把握する A コンテキスト C コンテキスト

    D コンテキスト B コンテキスト U D DDDによるコンテキスト分解のアプローチを取り⼊れている場合や、コン テキストとモジュールがほぼ対応している場合は、コンテキストマップを⽤ 意しておくことでそれぞれの依存が正しいかの評価や、依存の関係を俯瞰 して確認することができます。 ※この場合、コンテキストマップの作成からモジュールの決定という順番になる コンテキスト以外のアプローチでモジュールを⽤意している場合も、コンテ キストマップの形式にとらわれず何かしらの関係性がわかるような図を⽤ 意しておくことで定期的なモジュール粒度‧依存の⾒直しに役⽴てられま す。 コンテキストマップ
  12. モジュールのカプセル化 Controller Service Model Repository Controller Service Model Repository DATA

    DATA モジュール化を⾏なっていても、実装で他モジュールの内部ロジックやデータに直接参照をしてしまうと、機能変更の影響を内 部に留められず、強い依存関係が発⽣してしまいます。 そのため、モジュールは内部の詳細をカプセル化し、他のモジュールには公開インターフェースのみを提供することで、依存関 係の影響を軽減するよう構成することが重要です。 ModuleA ModuleB この例では別モジュール内を直接参照し強い依存関係が発⽣ しています。これにより⼀⽅の機能が変更されるたび、呼び 出し側の実装に影響が発⽣してしまいます。 Controller Service Model Repository Public API Service Model Repository DATA DATA ModuleA ModuleB Event Bus method call publish subscribe 内部の実装は他モジュールか らはアクセスさせないように 構成。 ※受け取った結果はACL層で モジュール内のオブジェクト に変換を⾏うことにより、ド メインが汚染されることを防 ぐことができます。 モジュールへのアクセスは各機能の公開インターフェースを介して⾏うこ とで、in/outが変更されない限り内部実装の変更を抑制できます。 ※公開インターフェースは概念的な表現であり、実態はイベントハンドラなど 様々な形式のエントリポイントを提供します。 ※通常Serviceなどから別モジュールのメソッド呼び出しはInterfaceで抽象化を⾏います カプセル化されたレイヤー
  13. 冒頭のECアプリケーションを例にモジュールモノリス構成を検討していきます DBレイヤーのモジュール性 商品 モジュール 配送 モジュール 注文 モジュール カート モジュール

    単⼀アプリケーション DB 単⼀アプリケーション + 単⼀DBインスタンスの構成 アプリケーションがモジュール化され、シンプルな構成に なっています。 しかし、この構成ではDBレイヤーでモジュール性が⽋如 している点に注意する必要があります。 ※ルールでテーブル間依存を縛る開発体制は、少⼈数では問題ないか もしれませんが、⼈数が増えるにつれ維持するのが困難になってくる 場合が多いです。
  14. モジュールごとに独⽴したスキーマやデータベースを持つことで、モジュール間の依存を減らし、特定の機能やモ ジュールの変更が他の部分に与える影響を最⼩限に抑えることができます。 DBレイヤーのモジュール性 商品 注文 配送 カート 商品スキーマ 注文スキーマ 配送スキーマ

    カートスキーマ 注文 モジュール 商品 モジュール 配送 モジュール カート モジュール 注文 モジュール 商品 モジュール 配送 モジュール カート モジュール 個⼈的⾒解ですが開発初期ではDBを複数構築するよりも、スキーマによる分割で⼗分なケースが多いと考えています。 まずはスキーマで分割し、スケーラビリティなどに問題がある場合は、そのモジュールだけ別DBに切り分けるくらいのモチベーションで もモジュールの独⽴性、マイクロサービスへの移⾏容易性は⼗分確保できます。 ※どちらの場合もモジュールごとに固有のDBコネクションを保持し、それぞれで アクセス可能なテーブル‧DBを制限することで、安全なDB接続を構成できます。
  15. 場合によってはApplicationレイヤーからモジュール分割を⾏い、APIエンドポイントを共通のモ ジュールとして分離するアプローチもあります。 apiモジュールはBFFのような振る舞いを持たせることで、UI都合のモジュール内処理やモジュール 間の依存を解消することができます。 また、APIから各モジュールの呼び出しはinterfaceを通して⾏うことで責務の明確化を⾏うことが できます。 しかし、UI都合の処理をバックエンドに持ち込むと、コードが肥⼤化し、UIの変更がドメインロ ジックに波及しやすくなり、コードの健全性や保守性に悪影響を与え、関⼼の分離が曖昧になって しまうなどの課題が⽣じやすくなります。 そのような場合は別デプロイメントとしてBFFの導⼊を検討することで、モジュール単位での開発

    ‧保守が可能となり、チーム間の認知負荷の軽減や責任範囲の明確化にもつながります。 src/ ├──api/ # プレゼンテーション層 │ ├── controller/ │ ... │ ├── moduleA/ │ ├── domain/ │ ├── application/ │ ├── interface/ # 公開インタフェース │ ... │ ├── moduleB/ │ ├── domain/ │ ├── application/ │ ├── interface/ │ ... ... ディレクトリ構成のイメージ Client BFF Module A Module B Module C Module D HTTP リクエスト 個⼈的には、PoCなどでない限り、設計の明確化‧開発効 率‧チーム間の依存関係整理といった観点から、基本的に はBFFの採⽤を検討した⽅が良いと考えています。 また、フロントエンド側のエンジニアが主導で⼿を加えら れる設計である点も、現実的な開発体制においては健全で す。
  16. また、モジュール内でinternal‧externalと明確に内部実装と公開すべきイ ンタフェースの定義場所を明確化することで認知負荷の改善や、依存関係 の整理、静的コード解析ツールによる依存チェック※1による制約の追加な どの導⼊が容易になります。 ※1… 他モジュールから内部実装を直接参照させない この構成は単なるディレクトリ整理ではなくプロジェクト(ビルド単位) としても分離することができ、これにより他モジュールへ依存している場 合でも最⼩限の範囲でビルドすることができます。(公開インタフェースの 資材さえあればビルド可能)

    余談ですが、sharedなどいわゆる共有ライブラリのような形での公開をす ると、どのモジュールがどのモジュールへ依存しているか、その依存は意 図したものか、などが追いづらくなってしまうので、あくまでモジュール 単位で公開範囲を制御することが重要です。 src/ │ ├── moduleA/ │ │ │ ├── internal/ │ │ ├── adapter/ │ │ ├── application/ │ │ ├── domain/ │ │ ... # 内部実装 │ │ │ ├── external/ │ ... ├── dto/ │ ├──service/ │ … # 公開インタフェース │ ├── moduleB/ │ │ │ ├── internal/ │ │ ... # 内部実装 │ │ │ ├── external/ │ … # 公開インタフェース ... ディレクトリ構成のイメージ
  17. モジュール間の通信パターン 実際の開発ではモジュール間の依存を完全に0にすることは難しく、依存が少なからず発⽣してしまいます。 設計段階でどういったケースではどのパターンを利⽤するか⽅針を明確化しないと実際の開発時に設計が崩れてしまいメンテナンス性を 下げる要因に繋がってしまいます。 マイクロサービス間の通信は基本ネットワークを介しますが、モノリスだと同⼀プロセスで通信を⾏えるメリットがあります。 パターン 同期/⾮同期 依存度 実装コスト 特徴

    メソッド呼び出し 同期 強 低 同⼀プロセス内での関数‧メソッドの直接呼び出しを⾏い、カプセル化の 観点からInterface経由で参照します。最も⾼速、シンプル、型安全な反 ⾯、モジュール間で強い依存が発⽣します。 ネットワーク呼び出し 同期 or ⾮同期 中 中 それぞれ独⽴したプロセス間でHTTP/gRPCなどのエンドポイント経由で のアクセスになり、メソッド呼び出し同様に公開インタフェースに依存す るがプロセス分離により依存は若⼲緩和されます。 イベントの発⾏‧購読 ⾮同期※1 低 ⾼ メッセージブローカーやイベントバスによる⾮同期連携を⾏います。発⾏ 元は誰が購読しているか把握する必要がなく疎結合で弱い依存で通信を⾏ えます。 このページでのモジュール間依存には、クライアント(APIリクエスト元など)のUI都合により発⽣する依存は含まれません。 UI都合などによる依存はクライアントもしくはBFFレイヤーなどへの責務を切り分けることが検討できます。 代表的なモノリス上のモジュール間通信パターン ※1 同期的な連携も可能
  18. モジュール間の通信 - メソッド呼び出し モジュール間通信で最もシンプルな⼿法が、メソッド(関数)呼び出しによる同期的な連携です。 同⼀プロセス内で完結し、⾼速かつ型安全に処理が⾏える⼀⽅、依存関係が強くなりやすいという特徴も持ちます 特徴 ‧ 同期処理:基本は呼び出し元は呼び出し先の処理が終わるまで待機 ‧ 型安全性が⾼い:コンパイル時にインターフェースが保証される

    ‧ 実装がシンプル:そのまま関数やサービスを呼び出せる ‧ ⾼い結合度:呼び出し先の実装に強く依存しやすい 処理の⼀貫性を保ちたいユースケースや同⼀コンテキスト内でのモジュール間連携、変更頻度が少なく依存の影響を受けても許容できる ケースに向いている⽅式になります。 実装時には循環参照が発⽣しないように依存⽅向を設計し、境界を跨ぐ場合は抽象インターフェースを使ってアクセスを⾏います。
  19. モジュール間の通信 - メソッド呼び出し // Userモジュールでの記述 // 呼び出される側(ユーザー)の公開インターフェース public interface UserService

    { User getUser(UUID userId); } // Orderモジュールでの記述 // 呼び出し元(注文)モジュールのServiceクラス @RequiredArgsConstructor public class OrderService { // インターフェースに依存 ( DI ) private final UserService userService; // ユーザーモジュールを利用した注文作成メソッド public void createOrder(UUID userId) { User user = userService.getUser(userId); ・・・後続処理 } } UserService UserServiceImpl OrderService method call 他モジュールから参照可能なInterface 実際の処理をする実装クラス User Module Order Module Public API 設計によっては、Port/Adapterパター ンなどによるACLレイヤー経由で外部 のモジュールを参照することで、より 明確に外部のモジュールとの境界を明 確にできます。 ACLレイヤーではPurlic APIで定義され たIN/OUTのモデルと内部ドメインモデ ルの変換などの責務を持ちます。 カプセル化により、呼び出し元モジュールは、呼び出し先モジュールで公開 されたAPIを参照‧メソッド呼び出しを⾏います。 以下は、外部モジュールを呼び出すサービスクラスのシンプルな 実装例になります。
  20. 注意点 部分的な適⽤をする場合は設計‧実装‧運⽤の各フェーズでコストが⾼くなるため、あくまで例外的な⼿段として の利⽤に留めておき、基本的には、プロセス内での呼び出し(メソッド呼び出しやオンメモリのイベント通知)に よる通信の⽅が、モジュラーモノリスのメリットを最⼤限に活かすことができます。 モジュール間の通信 - ネットワーク呼び出し 通常、モジュラーモノリスは同⼀プロセス内での呼び出しが基本ですが、 特定のモジュールを疎結合に保ちたい場合や、将来的なサービス分離を⾒据えた構成として、 HTTPやgRPCなどのネットワーク越しの呼び出しをあえて取り⼊れるケースもあります。

    特徴 プロセス越しの通信:同⼀アプリケーション内でも、HTTPやgRPC経由で通信 疎結合性の向上:メソッド呼び出しよりも依存度を抑えられる(契約ベースの連携) 将来的なサービス分離が容易:ネットワーク越しの通信にしておくことで、独⽴デプロイへの移⾏がスムーズ 通信コストと耐障害設計が必要:レイテンシ、失敗時のリトライやタイムアウトなどの考慮が必須 ※補⾜として、モジュールごとに独⽴したアプリケーションとして起動し、同⼀サーバー上で異なるポートでリクエストを 受ける構成や、Kubernetes上でモジュールごとにPodを分離する構成を採⽤するケースもあります。 これにより、モジュール間の独⽴性やデプロイの柔軟性を⾼めることができ、将来的なマイクロサービス化への段階的な移 ⾏にもつなげやすくなります。
  21. 通常、マイクロサービス間でイベント通知を⾏う場合は、ネットワーク越しの⾮同期通信やメッセージブローカーを利⽤する必要があ ります。⼀⽅、モノリスであれば同⼀プロセス内でのイベント通知が可能になり、型安全で軽量に実現することができます。 また、同⼀プロセス上で擬似的なイベント通知を⾏うことで、通知後の処理を同期的に⼀つのトランザクションに含めることができ、 コード上では独⽴性を保ちつつ、データ整合性の保証をトランザクションに委ねる設計も可能になります。 特徴 ‧⾮同期処理 発⾏側は「通知したら終わり」なので、リアルタイム性を求めない処理を後回しにできる ※擬似的なイベント通知では同期処理も可能 ‧疎結合 発⾏元(Publisher)は、受信者(Subscriber)の存在を知らなくてよい

    ← DDDではここが本質 イベントを購読しているモジュールを追加‧削除しても、発⾏元のコードには⼀切変更が不要 ログ出⼒、メール通知、監査記録、ポイント加算など、1つの処理に複数の副作⽤がある場合 イベント購読側をモックに置き換えるだけで発⾏元のロジックを独⽴してテスト可能 モジュール間の通信 - イベント通知 モジュール間の連携を⾮同期で疎結合に⾏う⽅法として、ドメインイベントやアプリケーションイベントを 使った通知⽅式があります。
  22. モジュール間の通信 - イベント通知 // 決済サービス(決済モジュール) @Component @RequiredArgsConstructor public class PaymentService

    { // イベント発火サービス private final EventPublisher eventPublisher; public void execute(Payment payment) { // 支払い処理ロジック(省略) executeTransaction(payment); // 支払い完了イベントを発行 eventPublisher.publish( new PaymentCompletedEvent(payment.getOrderId()) ); } } // 決済完了イベントのハンドラサービス (注文モジュール) @RequiredArgsConstructor public class OrderEventListener { @EventListener public void handle(PaymentCompletedEvent event) { // 注文ステータスを「支払い済み」に更新(省略) updateOrderStatus(event.getOrderId()); } } 左記のコードは、ECアプリケーションで決済が完了した際に注 ⽂ステータスを更新する処理の例です。 決済サービスは、決済完了時にイベント発⾏を⾏うことに留め、 それ以降の処理はイベントを購読している注⽂サービスが検知後 に処理します。 これにより、決済サービスの開発者は決済完了時にどのモジュー ルをどのように呼び出すかを気にすることなく、決済処理のビジ ネスロジックのみに集中することができます。 また、将来的に通知などの機能を追加する際もイベントリスナー を追加するだけで実現でき、既存サービスへの変更を加える必要 がなくなります。 ※モジュールの責務を集中することで変更を局所化 その他にもイベントを利⽤することで、トランザクションの分離 (⼀部失敗しても決済完了の処理を完了できる)、⾮同期による パフォーマンス改善、依存の低減などのメリットもあります。 ※publisher、handlerのネットワーク越しなのか、同⼀プロセス上なの かの技術的部分は抽象化しておくと保守性が⾼まります。
  23. モジュール間の通信 - イベント通知(同⼀プロセス内) 同⼀プロセス内でのイベント通知の仕組みを実現するには、いくつかの⽅法があります。 例えば、フレームワークやライブラリのイベント機構を利⽤する⽅法や、⾃前で EventDispatcher のような仕組みを実装する⽅法があります。 どの⽅式を採⽤する場合でも、イベント通知部分を正しく抽象化しておけば、実装の切り替えによる影響を最⼩限に抑えることができます。 ※余談ですが、DDDの著書であるIDDDのサンプルコードでは⾃前でイベント通知の仕組みが実装されています。 ※代表的な同⼀プロセス内イベント通知のライブラリには、JVM向けにはSpring

    Event、Go⾔語向けにはWatermill (gochannel)などがあります。 同⼀プロセス (オンメモリ) module A publish handle module B Event Dispatcherの構成次第で、publish後にhandleされる処理を同期的に実⾏するか、⾮同期的で実⾏するか、さらにそれを同⼀トランザク ション内で実⾏するかどうかを選択できます。 また、同期的な同⼀トランザクションでの処理を選択すれば、実装コード上では疎結合の状態を保ちながらも、イベント駆動を採⽤し た際に課題となる補償トランザクションの実装を後回しにできる、もしくは簡素化できるという利点があります。 ⼀⽅で、イベントの購読処理がトランザクションに含まれることで、失敗時に発⾏元の処理全体が巻き戻るなどイベント駆動本来の「疎 結合」「⾮同期性」「失敗の局所化」といった利点を損なう可能性があるトレードオフも存在します。 また、将来的に⾮同期処理や分散環境に移⾏したくなった場合に、このトランザクション境界が⾜枷になる可能性もあるため、責務の 分離と冪等性の意識が重要です。 Sync / Async (In/Out of Transaction)
  24. モジュール間の通信 - イベント通知(ネットワーク越し) モジュール間の⾮同期通信やイベント通知をネットワーク越しで⾏う場合、メッセージブローカーや分散キャッシュなどのミドルウェア を利⽤した構成が⼀般的です。 Redis のようなミドルウェアを 同⼀ホスト上で動作させ、OSカーネル内で通知を完結させるような構成も技術的には可能ですが、スケーリング性や クラウドネイティブな運⽤の観点からは、現代ではあまり選ばれない構成です。 そのため、ここでは

    ミドルウェアが別ホストに存在する前提で説明を進めます。 プロセス/ノードの独⽴性を確保できる ‧個別にスケールイン/スケールアウト、再起動、フェイルオーバーが可能 ‧運⽤⾯‧可⽤性の柔軟性が向上 将来的なマイクロサービス分割への布⽯ ‧段階的なサービス分離がしやすい ⾮同期/イベント駆動の分散処理の容易性 ‧Kafkaなどのメッセージブローカーにより複数モジュールに同時通知が可能 ‧⾼スループットな⾮同期処理や、リトライ‧DLQなどの堅牢なエラーハンドリング設計が可能 ‧ミドルウェアで提供されている仕組み(重複排除や順序補償など)を活⽤できる あくまで個⼈的な⾒解にはなりますが、同⼀プロ セス内でのイベント通知は、軽量に実装できる反 ⾯、DLQ や Outbox パターンによる再送制御、ス ケーラビリティや堅牢なリトライ設計の⾯では、 オンメモリ通知では限界を感じる場⾯が多いと感 じています。 そのため、ネットワーク越しのイベント連携の仕 組みは、共通の基盤として再利⽤しやすく、クラ ウド環境を活⽤すればインフラ構築も効率化でき るため、最初に必要な投資として割り切ってネッ トワーク越しのイベント通知を選択するケースが 多いです。 ※ドメインイベントのように、モジュール(コンテ キスト)内に閉じた通知のみ、同⼀プロセス内で通 知を⾏うという選択をすることもあります。 特徴
  25. モジュール間の通信 - イベント通知(ネットワーク越し) ネットワーク越しのイベント通知を実現するためには、メッセージブローカーやPub/Subサービスを利⽤する構成が⼀般的です。 イベントの発⾏側はブローカーを通じてイベントを送信し、購読側はそのイベントを受信して処理を⾏います。 また、キューを構成に取り込むことでイベントの⼀時的なバッファとして機能し、受信者が処理可能なタイミングで取り出せるため、負荷分散や再試 ⾏が⽤意になります。 module A Publisher

    publish push/pull module B Consumer Message broker Apache Kafka 、RabbitMQ、AWS(SNS,SQS)、GCP(Pub/Sub)など ※受信⽅法はブローカーの種類によって異なります。(例 : Kafkaはpull、SNSはpush) module A module B module C module D AWS(SNS、SQS)を利⽤した例 SNS SQS publish pull pull pull 左記の例では、SNSへpublishしたイベントをそれぞれのモジュール ごとに⽤意したキューへファンアウトすることで、モジュールごと に独⽴したイベント処理や拡張性を実現できます。 モジュラーモノリスにおいては、すべてのモジュールが同⼀プロセ ス内で動作するため、ポーリング処理やイベントハンドリングなど の汎⽤的な仕組みを各モジュールで再利⽤することが可能です。
  26. なお、イベントによる通知(イベント駆動設計)においては、以下のような設計‧運⽤上の課題の認識も必要になります。 ‧補償トランザクション 分散トランザクションが使えない場合に、失敗時のロールバック処理を別途設計する必要があります。 トレードオフが発⽣しますが、同⼀プロセスによる同⼀トランザクションの処理で緩和できます。 ‧Outbox パターン (トランザクション整合性を保ちながらイベントを外部に送信するための仕組み) ライブラリとの併⽤や各モジュールでの再利⽤により実装⼯数の削減が期待できます。 ‧DLQ(Dead Letter

    Queue) (処理不能なメッセージを隔離して後処理できるようにする⼿法) SQSなどの既存の仕組みを利⽤することができます。 ‧イベントの種類 ドメインイベント(モジュール内)とインテグレーションイベント(外部公開)の設計の分離 ‧スキーマバージョン管理 イベントフォーマットの変更に安全に対応する仕組み(例:バージョン付与、スキーマレジストリの利⽤) モジュール間の通信 - イベント通知におけるその他の考慮点 本資料はモジュラーモノリス構成に焦点を当てているため、これらの詳細は割愛します。より複雑なマイクロサービス環境で本格的に導⼊する際には 必須となるため、モジュラーモノリスの段階でどこまで考慮するかが重要になります。
  27. モジュール間の通信 - フロー制御 これまでに紹介した通信⽅式は、モジュールAからモジュールBへの直接的な呼び出しや、イベントを介した通知によってモジュール間で 連携する構成が中⼼でした。 ⼀⽅で、モジュール間にまたがる整合性が要求される⼀連の処理フロー(注⽂ → 在庫引当 → 請求

    → 通知)を⼀か所で統合的に制御し たい場合には、 「処理の流れ⾃体を専⽤の構成で制御する Orchestrator(オーケストレーター)」というアプローチもあります。 Orchestrator 注文 在庫 通知 請求 1 2 エラーの発⽣ 3 4 5 補償トランザク ション スキップ オーケストレーターが、各モジュールの呼び出 しを順番通りに⾏い、どこかのステップでエ ラーが発⽣した場合は、補償トランザクション の実⾏を⾏います。 このような Orchestrator による処理の流れの制御は、マイクロサービスアーキテク チャにおいても広く採⽤され、 特にサービス間にまたがるビジネス処理を安全に実 ⾏するための Sagaパターンでは、 Orchestrator が各サービスの呼び出しと補償処 理を中央で管理するオーケストレーション型Sagaとして活⽤されることが多くなっ ています。 なお、モジュラーモノリスのような単⼀プロセス環境でも、それぞれが独⾃のカプ セル化されたロジック‧データセットを持つような設計の場合、モジュール間の独 ⽴性と整合性を⼀定レベル保つために有効です。 注意点として、ビジネスロジックがオーケストレーターに集中すると⼀気に複雑化 や肥⼤化を招き、神クラスになってしまうリスクがあるため採⽤する前にモジュー ル境界の⾒直しや実装⽅針などを慎重に検討しましょう。 ※他にもMediatorなどのアプローチがありますがこれらは必須ではないため、採⽤する際は オーバーエンジニアリングにならないかを評価してから採⽤しましょう。 Request
  28. モジュール間の通信 - トランザクション トランザクション境界の理想は「1モジュール1トランザクション」です。モジュール内で完結できる処理であれば、副作⽤や整合性をモ ジュールに閉じ込められるため、設計上も運⽤上も安全性が⾼まります。 しかし、実際には複数のモジュールを横断して⼀貫性を担保したいケースも存在します。そのような場合は、 • 同⼀プロセス内であえてモジュール横断でトランザクションを束ねるか • 補償トランザクション(イベント駆動など)で整合性を⾮同期に担保するか

    • フロー制御によって制御するか といった選択肢を状況に応じて使い分ける必要があります。 特にモジュラーモノリスでは、後者のように複数モジュールの処理を同⼀プロセス内でトランザクションを処理できるのも利点の⼀つで す。ただし、その分モジュール間の境界が曖昧になりやすく、依存度が⾼くなってしまいがちなため注意が必要です。 個⼈的には、どちらか⼀⽅に偏らず場⾯に応じて柔軟に使い分けるべきだと思っています。これに関してはケースバイケースになっ てしまいますが、判断の軸にはデータの⼀貫性の厳密さや、ユーザー応答の同期性、モジュールの独⽴性などが挙げられます。さら に私はこれらに加えて対応⾃体の複雑性を重要視しています。簡単な機能なのに無駄に複雑化され実装に時間を要してしまう場合、 モジュラーモノリスによる開発容易性が得られていないサインとして受け取り、その複雑性は今抱えるべき問題なのか、先送りでき るのかなどを意識することが重要だと考えています。 また、それでも複雑性が解消されない場合は根本のモジュール(コンテキスト)分解が誤っていないか疑い、必要に応じてリファクタ リングし、モノリス状態でPDCAを回すことが⼤切だと思っています。
  29. ‧API Composition クライアントまたはBFFが各サービスに個別に問い合わせを⾏い、統合しレスポンスする ‧CQRS(Read Model) 検索に必要なデータをあらかじめ集約‧整形した形で別のデータストア(ElasticsearchやキャッシュDBなど)に保持し、その Read Modelに対して検索を⾏う ‧GraphQL Federation

    各サービスが所有する情報をGraphQLスキーマを通じて統合的に問い合わせ、1つのエンドポイントから集約レスポンスを返す 分散したデータソースにまたがる情報を⼀度に取得する必要があるユースケースでは、マイクロサービスアーキテ クチャにおいて以下のような代表的なアプローチが存在します。 DBのモジュール横断(おまけ) モジュラーモノリスにおいても、もしDBをモジュール単位で分割している場合、これらのアプローチの検討が必要 になることがあります。
  30. DBのモジュール横断(おまけ) プロダクトがまだモノシリックな段階であり、マイクロサービスへの分割が必要ない場合先ほどのアーキテクチャ は過剰となり、開発や運⽤の複雑性を不必要に⾼めてしまう可能性があります。 そのような場合には、単⼀のデータベース内で複数スキーマを⽤いて論理的にデータを分離したアプローチを採⽤ することで、必要に応じてスキーマを跨いだJOINやビューを⽤いて、モジュールを横断した検索要件を満たしなが ら、開発や運⽤の複雑性を必要なタイミングまで先送りにすることができます。 商品スキーマ 注文スキーマ 配送スキーマ 注文

    モジュール 商品 モジュール 配送 モジュール カート モジュール カートスキーマ 原則はそれぞれ個別のスキーマに 対してのみ読み書き可能 検索専用 モジュール このアプローチを採⽤する場合は、複数スキーマにア クセス可能な検索専⽤のモジュールを定義し、それ以 外のモジュールはそれぞれのスキーマにのみ依存する 設計などで、重要なビジネスロジックを持つモジュー ルのカプセル化や依存関係の明確化を維持することが 重要です。 ※複数スキーマアクセスは全体で採⽤せず局所化させる また、検索専⽤モジュールのDBコネクションも読み取 り専⽤とすることで、予期せぬ事故の防⽌にも繋がり ます。 必要に応じてそれぞれのスキーマに 読み取りのみ可能 個⼈的には、パフォーマンス観点ではリードレプリカの調整でスケールが⽐ 較的容易だと考えているため、API CompositionやRead Modelを採⽤する 場合は対象データの依存度(⾮同期的にビューを構築したいか)や複雑性(カ プセル化したいかどうか)の観点で判断することが多いです。
  31. モジュラーモノリスは、マイクロサービスと同様にサービスの独⽴性を意識しつつも、同⼀プロセスで完結できるがゆえの柔軟な統合⼿段が選べるの が魅⼒です。 開発初期に実装の複雑性を抑えながら、運⽤コストも⼩さく、⾼速にプロダクトを育てていける実践的なアーキテクチャだと感じています。 ただし、モジュールの技術的な分離パターン以上に、DDD(ドメイン駆動設計)における戦略的設計の重要性を強く感じています。 単にレイヤーを分けるのではなく、「どのように業務やユースケースを分割するか」「どこに境界を引くか」という設計思想が、アーキテクチャ全体 の質を⼤きく左右します。 モジュールの粒度や分離の度合いには常にトレードオフがあり、複雑性が増す場⾯もあるでしょう。 ですが、モノリスであるがゆえに必要に応じて調整可能であるという柔軟さもあります。 ⼀度で完璧な設計を⽬指すのではなく、モノリスならではの開発スピードで実践と改善のサイクルを⾼速に回して成⻑させるアプローチの⽅が向いて いるように感じます。

    この記事では主に、コード上でどのようにモジュールを表現するかという観点で紹介してきましたが、 本質的には「今⾃分たちが向き合っている⼤きくて曖昧な要求や要望を、どうモジュールという構造に落とし込むか」とい う設計⼒が圧倒的に重要です。 「きれいなモノリス」を⽬指す⽚⽅の⼀助となれば幸いです。 最後に
  32. - DDD - 実践ドメイン駆動設計 - ドメイン駆動設計をはじめよう(O'Reilly ) - アーキテクチャ -

    ソフトウェアアーキテクチャ‧ハードパーツ(O'Reilly ) - マイクロサービスパターン - 実践的システムデザインのためのコード解説 - Microsoft Azure アーキテクチャセンター (web) - モジュラーモノリス - Modular Monoliths • Simon Brown • GOTO 2018 (Youtube) - Modular Monolith: A Primer (web) 参考⽂献