脱マイクロサービス:なぜ2025年は「モジュラーモノリス」の年になるのか
ここ10年、システム設計の議論において「スケーラビリティ」という言葉が出れば、その答えは自動的に**「マイクロサービス(Microservices)」**でした。
エンジニアが数人しかいない初期フェーズのスタートアップでさえ、プロダクトの価値検証(PMF)より先にKubernetesクラスタを構築し、わずか500行のビジネスロジックを6つのサービスに分割していました。これは典型的な**「履歴書駆動開発(Resume Driven Development)」**であり、過剰エンジニアリングの時代でした。
しかし2025年現在、潮目は完全に変わりました。
Amazon Prime Videoがサーバーレス/マイクロサービス構成を捨ててモノリスへ回帰し、コストを90%削減した事例は記憶に新しいでしょう。DHH(Ruby on Rails作成者)が提唱する「モノリスの復権(Majesty of the Monolith)」も、多くの開発者の共感を呼んでいます。
私たちは今、不都合な真実を突きつけられています。
「マイクロサービスは『複雑性』という高金利の負債であり、多くのチームにはその利子を払う余力がない」
本記事では、マイクロサービスへの安易な飛びつきがなぜ危険なのか、そしてその現実的な解としての**「モジュラーモノリス(Modular Monolith)」**について、具体的な実装イメージと共に解説します。
「疎結合」という幻想
マイクロサービスの最大のメリットとされる「疎結合(Decoupling)」。
「システムを分割すればチームは独立して動ける」と語られますが、現実はどうでしょうか?
多くの現場で生まれたのは、**「分散モノリス(Distributed Monolith)」**と呼ばれる怪物でした。
1. 独立性はなく、ネットワーク呼び出しだけが残った
サービスAがBを呼び、BがCを呼ぶ。サービスCのレスポンススキーマが変われば、AとBを担当するチームも巻き込んで修正とデプロイ順序の調整が必要です。
これは疎結合ではありません。かつてIDEのリファクタリング機能で一瞬で終わっていた修正作業を、最も遅く不安定な「ネットワーク」というレイヤーに持ち出してしまっただけです。
2. レイテンシーという税金
モノリシックなアプリでは、関数呼び出しはメモリ内で完結し、ナノ秒単位で終わります。
一方、マイクロサービスでは同じ処理に以下のコストがかかります。
- JSONシリアライズ(CPU消費)
- ネットワーク転送(レイテンシー発生)
- ロードバランサー通過
- JSONデシリアライズ(CPU消費)
- ロジック実行
- ...レスポンスで繰り返し
ユーザープロフィールを表示するだけの処理で、裏側で50回ものHTTPリクエストが飛び交うシステムを見たことがあります。これは「N+1問題」をデータベースではなくネットワークレベルで引き起こしているようなものです。
運用の悪夢
「NetflixやUberの真似をするな」とはよく言われますが、これは真実です。
専任のSREチームを持たない組織にとって、マイクロサービスの運用は地獄です。
- ログの分散:
grepコマンド1つでログを追えた時代は終わりました。DatadogやJaegerなどの分散トレーシングツールを導入・維持しない限り、エラーの原因特定すらままなりません。 - トランザクション管理:
BEGIN...COMMITで保証されていた整合性は失われました。Sagaパターンや補償トランザクション(Compensating Transaction)の実装が必要です。ビジネスロジックよりも、整合性を保つためのコードを書く時間の方が長くなります。 - 開発体験の悪化: ローカル環境でアプリを動かすために、
docker-composeで15個のコンテナを立ち上げる必要があります。PCのファンは回り続け、開発者の生産性は著しく低下します。
現実解:モジュラーモノリス
では、巨大なスパゲッティコード、何でもありの utils.js に戻るべきなのでしょうか?
いいえ、目指すべきは**「モジュラーモノリス」**です。
物理的には単一のデプロイメントユニット(モノリス)でありながら、論理的には厳格にモジュール分割された構造を指します。
モジュラーモノリスの鉄則
- デプロイは一つ: CI/CDパイプラインは1本化され、運用コストは劇的に下がります。
- 通信は関数呼び出し: HTTPは使いません。型安全なメソッド呼び出しで連携します。
- 境界の強制: これが最も重要です。「注文モジュール」が「ユーザーモジュール」のDBテーブルを勝手に参照することを、コードレベルで禁止します。
実装イメージ(TypeScript/Node.js)
NxやTurborepo、あるいはESLintの no-restricted-imports ルールだけで十分に実現可能です。
src/ modules/ users/ index.ts # 【Public】外部に公開するインターフェースのみexport core/ # 【Private】ドメインロジック db/ # 【Private】リポジトリ・DBアクセス orders/ ... shared/ # 本当に共通のものだけ(ログ基盤など) app.ts # 全モジュールの結合
もし orders モジュール内で import UserRepo from '../users/db/UserRepo' と書いたら、Linterがエラーを吐くように設定します。アーキテクチャはドキュメントではなく、コード(CI)で強制するのです。
それでもマイクロサービスが必要な時
マイクロサービス自体が悪なのではありません。「デフォルトの選択肢」にすべきではないというだけです。
移行を検討すべき正当な理由は以下の3つに限られます。
- 組織のスケーリング: バックエンドエンジニアが100人を超え、単一リポジトリでのマージコンフリクトが開発速度のボトルネックになっている場合。
- 技術スタックの異質性: 画像処理モジュールにはPythonとGPUが必要で、メインのAPIサーバー(Go/Node.js)とは要件が根本的に異なる場合。
- 障害隔離(Bulkheading): 特定の機能(例:リアルタイム通知)にアクセスが集中し、それが原因でシステム全体がダウンするリスクがある場合。
結論:退屈なアーキテクチャを選ぼう
システム設計はトレードオフです。
99%のプロジェクトにとって、マイクロサービスへの早期移行は「関数呼び出しを分散システムの問題に変換する」だけの、割に合わない取引です。
2025年、シニアエンジニアとして示すべき技術力は、複雑なアーキテクチャ図を描くことではなく、**「あえて分割しない勇気」**を持つことです。
まずはモジュラーモノリスで始めましょう。境界(コンテキスト)を綺麗に保ちましょう。
本当に分割が必要になった時、綺麗なモジュラーモノリスなら、いつでもマイクロサービスへ切り出せます。
「退屈(Boring)な技術」こそが、最も利益を生み出すのです。