シード期からモジュラモノリスに手を出したスタートアップの末路
はじめに
先日、開発していたマーケティングSaaSをようやくリリースすることができました。以下、サービスページ:
サービスの詳細な説明や顧客ターゲットなどはここでは省きますが、一言で表現すると、ワンクリックで様々なマーケティングデータを一元管理できるプロダクトになっています。
このプロダクトでは、初期からモジュラモノリス設計を採用し、その選択は結果的には良かったと感じています。この記事では、シード期からモジュラモノリスを選択した経緯と、その所感を書きたいと思います。
モジュラモノリスを採用した経緯
モジュラモノリスとは
モジュラモノリスとは、アプリケーション全体が一つのコードベースやプロジェクトとして管理され、単一のアプリケーションとしてデプロイされるが、その内部はモジュール化されているアーキテクチャのことです。各モジュールは独立して開発・保守され、特定の機能を担当しますが、デプロイメントや運用はモノリシック(単一)の形で行われます。
有名なのが、Shopifyの事例ですね
モジュラモノリスの検討開始
プロダクトの性質上、Google AnalyticsやFacebookのAPIなど、さまざまな外部APIにアクセスする必要がありました。マーケ系のAPIという特性上、アップデートが激しく、頻繁に仕様が変更されるという特性があります。(開発中もGoogle AnalyticsがUA→GA4への変更、動画コンテンツの充実により動画に関するデータが増えたなど、大きな仕様変更がありました)
これらの外部APIの依存関係を最小限に抑える方法として、外部APIごとのモジュール分割を検討し始めました。検討するにあたって、他社のモジュラモノリス/マイクロサービスでの採用事例を調べました。
- シード期からモジュラモノリス/マイクロサービスを採用した事例は皆無。必要機能や開発人数が増え、シリーズが進んだタイミングで非機能要件上検討し出す。最近だと大型調達からの「コンパウンド」「マルチプロダクト」の文脈で採用する例が多い。
- モジュールを「適切に」分割することが難しい。開発が進むにつれて、モジュール間の過度な疎通や、循環が発生する(=設計ミス)。高度なドメイン知識と高度なソフトウェア設計が必要な領域。
今回の開発の場合、モジュール分割が機能要件上必要だったこと(APIのバージョンアップに追随するため)、モジュール分割の単位がわかりやすかったこと(外部APIごと)から、初期からモジュラモノリスを採用しました。
モジュラモノリスを考慮したアーキテクチャ
上記を考慮した結果、最終的な全体像として次のようなアーキテクチャになりました。(ここまでで他にも紆余曲折ありましたが。。。)
フロントエンドとデータベースはひとつずつしかありませんが、それらを繋ぐバックエンドには、モダンCQRS設計パターンを採用しています。
クエリモデル(画像でいくと上部の疎通部分)に関してはPostgreSQL + Postgraphileを採用しており、DBスキーマからGraphQLエンドポイントを自動生成しています。
toB SaaSという性質上データベースにPostgreSQL(RLS)を採用し、マーケ系のプロダクトという性質上スキーマ設計が肥大化しやすくGraphQLとの相性が良かったです。詳しくは以下の記事を参照ください。
コマンドモデル(画像でいくと下部の疎通部分)に関してはモジュラモノリスを生かしてGoで外部APIへの接続を実装し、それらをBFFで集約しています。BFFに関しては初めは他社での事例が多いNestJSを使っていましたが、Slim BFFの設計の方が適していると思いGraphQL Meshに移行しています。
結果的にフロントエンドからの他サービスへのアクセス方法はGraphQLに統一されることになり(GraphQL Clientはエンドポイントが2つあっても対応可能)、フロントエンドの開発体験がかなり上がりました。
追加での検討事項
またアーキテクチャ以外にもスキーマの設計も外部APIごとに統一する必要がありますし、他にもCI/CDやIaCなども先に導入しておく必要があります。CI/CDに関してはGithub Actions、IaCに関してはCDK for Terraformを採用しています。
そして何より大変なのが、
- とにかくエンジニアの数が必要
ということです。フルタイムの他にも、副業やインターンも含めてたくさんのエンジニアに関わっていただき、開発を進めることができました。プロダクトの初期からの構想上、大規模になることが予想され先進的な取り組みをしていましたが、Zennでの発信を見て多くの方に興味を持っていただききました。またシード期から大規模な開発の必要性を理解していただいた投資家の方々には、この場を借りて感謝申し上げます。
振り返り
このプロダクトの関しては、プロダクト要求と技術選定がマッチしていて、シード期からモジュラモノリスを採用して正解だったなと感じています。もし別のプロダクトで初手からモジュラモノリス/マイクロサービスを検討していますと相談されたら、99%「辞めておけ」と答えます。
- 機能要件上モジュール分割が必要な場合
- モジュール分割がプロダクトの競争力と直結する場合
は納得するかもしれません。今回はその1%に該当していました。
逆に、上記のメリットを享受する一方、次のデメリットは受け入れる必要があります。
- 開発工数の大型化、長期化(とにかくお金も時間もかかる!)
- シード期なのに機能の実装以外にも、インフラへの投資、採用を見据えたDevRel活動など、レイターステージに必要な業務も求められる
- 一定の開発ルールに従ってもらう必要があるため、エンジニアにある程度のレベルが求められる(幸いにも扱っている技術は先進的だったため、面白いと思ってレベルの高い方が集まってくれました)
まとめ
Q. シード期からモジュラモノリス/マイクロサービスを導入するために必要な要素ってなんですか?
A. 資本政策と開発体制です。(技術力は関係ない!)
フィシルコムのテックブログです。MMMマーケティングSaaSを開発しています。 マイクロサービス・AWS・NextJS・Golang・GraphQLに関する発信が多めです。 カジュアル面談はこちら(corp.ficilcom.jp/recruit/develop-apply)から
Discussion
楽しく読ませていただきました!
質問なのですが、モジュラモノリスでの開発をする上でコンテキストを跨いだモジュールの呼び出しをどのように実装されたか気になります。
モジュラモノリスでは、モジュール間の呼び出しとサービス間通信をスムーズに差し替えられる...etcといったようなサービスの分割を見据えた実装が望ましいとされていますが、こちらを実現するために工夫した点などありましたら知りたいです!
マニアックな記事なのに読んでいただきありがとうございます。
コンテキストを跨いだモジュールの呼び出し処理は行っていません。幸い、モジュール分割は綺麗にできたので、そういった問題は発生しなかったです。
もしそういう場面に当たったら、コンテキストを跨いだモジュールが発生しないように、過度な分割は避けると思います。
なるほど、遭遇しなかった感じなのですね。
承知です、返信ありがとうございました!