はじめに
初めまして、クラウドワークスの新規事業エンジニアのせきと申します。2019年9月に新規事業クラウドリンクス(CrowdLinks)の開発チームにジョインし、サービスの立ち上げ段階から参画してきました。
現在クラウドリンクスは「Nuxt.js × Spring Boot(Kotlin)」という構成でサービスを運営しています。
この記事では、クラウドリンクスの技術選定の経緯と感想を紹介したいと思います。
- 新規事業の技術選定に興味がある、または今まさに技術選定をしている方
- ドメイン駆動設計などの設計思想に興味を持つ方
- 新規事業の開発に関わっている方、または関わりたいと思っている方
こんな方々のご参考になれば嬉しいです!
目次
背景
クラウドリンクス(CrowdLinks)は副業・フリーランスのためのハイクラスのマッチングプラットフォームです。2019年8月頃からMVP(Minimum Variable Product)でのニーズ検証を経て、2020年1月に正式に公開したサービスとなります。
ニーズ検証段階では、「Nuxt.js × Firebase」の組み合わせで最速にMVPを立ち上げ、事業状況と開発リソースを見てFirebase部分だけを移行する予定でプロダクトの開発を始めました。マッチングプラットフォームという特性上、登場概念も少なくなく、Firebaseを軸にした構成では予想通りに徐々に開発がやりにくくなっていました。
例を挙げると次のようなことがあります。
- Firebaseの制限上、ビジネスロジックはフロントエンドアプリ内で実装する必要があり、結果ビューとビジネスロジックが混ざりあい、デザイン改修のコストが高い
- Firebaseの制限上、データベースアクセス権限が「firestore.rules」という一つのファイルに定義する必要がある。そのファイルが1000行以上を超え、管理しきれない
- Firebaseの制限上、OR、IN、LIKEクエリは実行できない(2020年10月時点)
- Firebaseの制限上、ユースケースの関連処理、外部通知、データ集計などはサーバレスのFirebase Cloud Functionで実装しているが、数多くのCloud Functionを管理する仕組みがなく、運用コストが高い
2019年12月に、MVPテストの結果が分かり、長期的にサービスを展開する方針が決まりました。それと同時に、エンジニアの採用も進み、開発リソースが確保できました。そういった背景でFirebaseの部分を移行することにました。
移行後のアプリケーションの技術選定
クラウドリンクスは長期的・継続的な利用を実現するSaaS(Software as a Service)を目指して事業を立てました。長期的に開発・運用していく中に、迅速に市場やユーザーニーズの変化を検知し、継続にアプリケーションの機能を改善しなければなりません。
また、サービスの将来像として「ユーザー側の決済機能」や「コミュニティを形成する機能」を初めやりたいと思われていたことが色々とあり、サービスを拡張する可能性が十分ありました。
そういった背景からクラウドリンクスでは次の5つの方針を軸にアプリケーションを設計し、開発することにしました。
順番に採用理由を説明していきます。
「1. ドメイン駆動設計(DDD)」の採用理由
拡張性の高いシステムを作れる手法としてドメイン駆動設計を採用した。
また、採用を後押しする理由として以下のようなことがありました。
「2. モジュラモノリス」の採用理由
モジュラモノリスとは一枚岩のようなモノリシックアプリケーションと互いに独立しマイクロサービス化したアプリケーションの間に位置づけます。一つのデプロイパイプラインを維持しつつも、モジュール間は疎結合な関係に止まるという特徴を持ちます。
クラウドリンクスはモジュラモノリスを採用した理由は以下のことが挙げられます。
- 3人チームで社内新規事業をやる上で、インフラを保守するための時間的・金銭的コストを下げる必要があった
- アジャイル方式で週3回以上に頻繁にリリースするのでデプロイコストをスプリント内に収まる範囲にコントロールする必要があった
- 保守性・拡張性を維持するためにモジュールの肥大化を防ぐ必要があった
「3. CQRS」の採用理由
CQRS(Command and Query Responsibility Segregation)とは読み取りユースケース(Query Use Case)と書き込みユースケース(Command Use Case)の実装を分けることです。
CQRSが
- ユースケースの種類別で処理速度とセキュリティの最適化ができるようになる
- 読み取り処理の仕様変更する時に、書き込みの部分を気にする必要がなくなり、逆も同じで、ソースコードの変更容易性を高められる
- フロントエンドの画面表示の必要に応じて、クエリのみをスケールさせることもできるから、拡張性にも貢献する
というメリットを有します。
クラウドリンクスにおいて、書き込み処理はオブジェクトの属性ごとに行う一方で、読み取り処理は複数のオブジェクトを結合する必要があり、CQRSのメリットを最大限活用できると判断したため、その採用に至りました。
クラウドリンクスは新規サービスで、現在の70テーブルほどの規模では、一つのデータベースでもサービスの負荷を余裕に耐えられて、データベースを別で用意する必要がないため、また、予算上インフラ費用を節約したいため、データベースを共通化しました。
「4. イベント駆動(インメモリ)」の採用理由
疎結合なモジュール間の通信を実現するためにクラウドリンクスはドメインイベントパターンを選びました。
データ整合性を保つために、一つのトランザクションに収まるように同期的にインメモリでドメインイベントをPubSubしています。この辺りはまた別の記事で詳しく書こうと思います。
「5. 静的型付け言語(Kotlin × Spring Boot)」の採用理由
一般的に、静的型付け言語はタイプエラーをコンパイル時に発見でき、堅牢なアプリケーションをつくりやすいです。
クラウドリンクスの場合、ドメイン駆動設計を採用することで、技術要素が多くなり、互いの依存関係も複雑なので、コンパイルできると、タイプエラーを早期に発見でき、デバッグコストを下げられると判断しました。
また、型が自明していることで、将来的にメンバー人数が増えても、サービス規模が拡大しても、メンテナンスがしやすいことも採用理由として考慮されています。
モダンな静的言語を探していく中で、
という3つで悩んでいました。最終的にKotlinを選んだ理由は以下のようになります。
- 社内にRubyエンジニアが多くシンタックスの似ているKotlinはいざと言うときに読める
- Scalaは社内にコードだけあるけど今読める人がほぼいない状態になっていた
- Golangはフルスタックのフレームワークがなく、大規模になりうるサービスを作るには懸念があると判断した
最終的にできたアプリケーション
そういった技術選定の軸で実現したアプリケーションの全体像は下図のようになります。
以下の順番で少し詳細的に説明していきます。詳細の説明より、感想の方が聞きたい方はこのセクションを飛ばしていただいても結構です。(次のセクションへ)
「1. パッケージ構成」
クラウドリンクスのバックエンドパッケージは大きく
- 境界づけられたコンテキスト(BoundedContext)
- DDD基盤(DDDFoundation)
- 共有カーネル(SharedKernel)
- 横断的関心事(CrossCuttingConcern)
という4パーツに分けられます。
境界づけられたコンテキストは名前通り、DDDの構成要素である境界づけられたコンテキストが並ぶ場所です。
DDD基盤の中にドメインオブジェクトとユースケースのベースクラスが定義されております。実際に、ドメインオブジェクトを定義し、ユースケースを組み立てる時はDDD基盤から継承か派生するようにしています。
共有カーネルにコンテキスト間で共通するドメインオブジェクトが定義されています。例えば、アプリケーション全体でユーザーを一意に識別できるため、ユニバーサルユーザーID「UniversalUid」という値オブジェクトはここに定義します。
認証・認可、ロギングやエラーハンドリングなどの横断的関心事がドメインロジックに紛れ込み、ドメインの本質的に解決すべきことが分かりにくくなるのを防ぐために、横断的関心事はパッケージレベルで隔離して扱います。
「2. モジュラモノリス」
クラウドリンクスではドメイン駆動設計の境界づけられたコンテキストを単位でモジュールを分けています。コンテキスト間はコード上完全に分かれています。
「3. イベント駆動とPubSub」
疎結合するコンテキスト間の通信はドメインイベントによって実現しています。
一つのコンテキストの中で、あるトピックを持つドメインイベントを発火(Publish)すると、そのトピックに関心を持つ他コンテキストの中であらかじめ登録した対象トピックのサブスクライバー(Subscriber)がトリガーされ、イベントペイロードを受け取り、任意の処理を実行することができます。
クラウドリンクスでは実装便益上、PubSubはSpring BootのDIコンテナーを利用しています。
「4. CQRS」
クラウドリンクスはCQRSパターンを採用して、読み取り処理はクエリハンドラーが、書き込み処理はコマンドハンドラーが担当します。クエリハンドラーとコマンドハンドラーはコード上独立しています。
ビジネスロジックを取り入れたドメインオブジェクトをコマンドハンドラーが使い、データ整合性を担保します。一方で、フロントエンドの画面表示に特化した振る舞いを持たないビューモデルをクエリハンドラーが使います。ビューモデルとドメインオブジェクトはコード上独立しています。
データベースの中で、読み取り用のテーブルと書き込み用のテーブルが分かれています。テーブル間のデータ同期はイベント駆動パターンによって実現しています。
「5. ヘキサゴナルアーキテクチャ」
クラウドリンクスではヘキサゴナルアーキテクチャを採用しています。
クラウドリンクスの1コンテキストはコアから外側の順番で
の3階層に分けられます。
ドメイン層はドメインオブジェクトを、ユースケース層はクエリハンドラーとコマンドハンドラーを、ポート層はコンテキスト内からウェブ、データベースや他のコンテキストなど外部システムにアクセスするための要素を実装します。外側にいる階層は内側にいる階層に依存できますが、その逆は絶対に許されません。
感想
よかったこと
- ドメインオブジェクトの定義クラスが100行以内にコントロールされ、個々のドメインオブジェクトの責務と振る舞いを把握しやすい
- ドメインオブジェクトのメソッドとバリデーションは直接に仕様を反映して、ほとんどの場合、わざわざ仕様書を参考する手間がない
- 集約単位で不変条件が担保しているため、仕様変更する時の影響範囲が特定しやすい
- ドメインオブジェクトだけでなく、ユースケースとリポジトリも単独のクラスに定義されて、ユースケースとリポジトリに対するユニットテストが書きやすい
- DDD基盤構築する時に、DIコンテナーやAOPアノテーションなどの特長を備える「Kotlin × Spring Boot」の組み合わせが非常に便利だ
よくなかったこと
- DDDを実現する技術要素の量が多く、初期段階の学習コスト、(短期的に)機能改修するたびの実装コストが高い
- B2Cのマッチングプラットフォームという性質上、クラウドリンクスに登場するドメインオブジェクトは簡単なバリデーションや属性を変更するセッターしか持たないものが多い
- 仕様変更の対応しやすさについては、DDDパターンとMVCパターンどちらが優れているのかが正直今の規模ではわからない。単純にモデルに属性を追加する場合は、むしろMVCの方が変更箇所が少なくてやりやすい
- 読み取り処理を担当するクエリは肥大化しやすい。特に情報規制が必要なクエリは肥大化すると情報漏洩のリスクが高くなる
- イベント駆動のPubSubパターンは使いやすいが、初期実装が難しく、DIコンテナーを持たない環境での再現性が低い
まとめ
FirebaseやMVCなどに比べて、ドメインモデリングを重要視するドメイン駆動設計は絶対に優れていることはなく、採用するかしないかはサービスの規模やチーム人数などの状況を見て判断するといいでしょう。
クラウドリンクスの今の規模では機能改修の実装スピードという観点で、DDDパターンがMVCパターンより絶対的に優れていることは言えないですが、複雑な仕様、大規模なアプリケーションに対応できる潜在力は確かに感じ取れます。
3年後、エンジニアチームの人数が増えて、「決済機能」「応募管理機能」「コミュニティ機能」などが実装された時にこそ、ドメイン駆動設計が真の威力を発揮できると信じています。
We Are Hiring!!
ドメイン駆動開発(DDD)をゼロから実践しているプロダクト「クラウドリンクス」の1→10のフェーズで活躍していただけるエンジニアを絶賛募集しております。「"働く"を通して人々に笑顔を」というミッションに共感した方は是非エントリーをお願いします。
https://jobs.forkwell.com/crowdworks/jobs/9855
クラウドリンクス以外にも、新たな事業も仕込んでおり、そちらもDDDを実践して開発しているのでよければこちらもご覧ください!
https://jobs.forkwell.com/crowdworks/jobs/9825
最後
このブログ作成にあたり、丁寧に指導とレビューしてくださった高梨さんに感謝の意を表します。