STORES でエンジニアリングマネージャーをしている morihirok です。 ストアーズはECの会社、ではない でも話したとおり、今でも STORES は EC の会社として認知されていることが多いです。
その誤解を解くべく、2024年1月にリリースされた 「予約システムと、ひとつになったPOSレジ」 の開発について紹介しながら、これから STORES がどんなものを開発していきたいのかお伝えできればと思います。
STORES は複数のスタートアップが合併してできた会社である
具体的な開発の話をする前に、実は STORES は複数のスタートアップが合併してできた会社だということを知っていただけるとこの後の話が理解しやすいかと思います。
それぞれのスタートアップは Web サービスを提供していた会社で、First commit から数えると10年以上開発が続けられてきたサービスたちでした。それらがひとつの会社となり、STORES のサービスとして提供されています。
こうやってサービスラインナップを見ていただけると、ECだけではなく店舗運営に必要な様々なサービスがあることがわかるかと思います。
しかも各サービスは10年以上スタートアップの荒波を生き抜いてきたもので、ユーザー数もレコード数もコードの規模もなかなかのものとなっています。
こちらは 2024 年の RubyKaigi で STORES のブースに置いていたパネルです。 ご覧のようになかなかの規模の Rails が3つ並んでいます。Rails 以外にも Java や Go のサービスもありますし、フロントエンドには React も Vue もあり、AWS も GCP も使っています。Web サービスを提供しているスタートアップが集合した結果、様々な技術スタックでサービスが構成されています。
STORES はこのような歴史の会社であるため、しばらくはサービス単位での事業部制組織となっており、開発チームもそれぞれのサービス単位で存在していました。 ですが、複数のスタートアップがひとつの会社になったのはそれぞれのサービスを連携させ、より大きい市場でより大きい価値を提供したかったからです。サービス間の連携はわたしたちにとって解くべき重要な課題でした。
なぜ「予約システムと、ひとつになったPOSレジ」を作ろうと思ったか
2022年の暮れ頃、「STORES 予約と STORES レジを連携する」という計画が持ち上がりました。
STORES 予約 とは予約受付・管理業務を行える Web サービスです。2013年から開発が続けられており、「予約」というドメインに関する様々な機能があります。
当時 STORES 予約は、受け付けた予約に対してクレジットカードによる事前決済ができる機能があったのですが、現地での決済を管理する機能はありませんでした。 現地決済の管理はユーザーからの要望が多く、いくつかの方式が検討されましたが、理想の体験としては以下の機能も一緒に提供する必要があるだろうと考えました。
- パソコンを持ち出さなくてもタブレット端末などで当日の予約状況が簡単に確認できる
- 簡単な操作でレジ業務ができ、そのデータを元に会計処理をしたり分析をしたりできる
- レシートプリンタやキャッシャーなどと連動できる
- レジ業務にてさまざまな決済手段を持ったキャッシュレス端末を扱える
当然、これらを1から作ると開発に多くの時間がかかります。
ですが、STORES にはすでに iPad で使える POS レジサービス「STORES レジ」とキャッシュレス端末提供サービスの「STORES 決済」があります。STORES レジはレシートプリンタやキャッシャーとの接続ができ、会計処理・分析の機能がすでにあります。STORES 決済はクレジットカードをはじめ複数の決済手段を利用できる決済端末を提供しており、STORES レジでのレジ業務に組み込むこともできます。
これらを STORES 予約と連携できれば理想の体験を提供できると考え、私たちは本格的に STORES 予約 と STORES レジ・STORES 決済の連携を検討しはじめました。 STORES レジと STORES 決済は iPad 上での連携がすでに完了しているため、STORES 予約と STORES レジを連携する必要がありました。
当時のシステム構成
検討段階におけるシステム構成は以下のようになっていました。
順を追って説明していきます。
STORES レジ
STORES レジとは STORES ネットショップと商品・在庫情報を連携できる iPad POS レジです。
STORES レジ と STORES ネットショップはバックエンドは同じ Ruby on Rails で作られたモノリシックなアプリケーションとなっており、DB も同じなので商品情報も在庫情報も連携されるというシステムになっています。
ちなみになぜこうなっているかというと、STORES レジ自体は 2021 年にリリースされた比較的最近のサービスで、すでに存在している STORES ネットショップ の資産を有効活用しようという判断があったからでした。
なお、STORES ネットショップのフロントエンドは Nuxt で作られており、同一のバックエンドから iOS アプリ向けのAPIと Nuxt 向けのAPIを提供している形になっています。
STORES において最大級の Rails リポジトリで、1500を超える Model クラスが存在しています。
STORES 予約
STORES 予約はモノリシックな Rails アプリケーションで、フロントエンドは Next.js で作られています。 こちらも負けず劣らず巨大な Rails リポジトリで、1000を超えるModelクラスが存在しています。
ID 基盤
STORES のサービスたちはもともとバラバラのスタートアップで開発されていたため、当然ログインアカウントもそれぞれで用意する必要がありました。 将来的にサービス間連携を行うことを見据えてこの状況を解消するため、Open ID Connect に準拠したIDプロバイダーを自前で作り各サービスに導入していました。 これによって STORES の利用者は単一のアカウントで STORES レジ・STORES ネットショップ・STORES 予約を利用可能となっています。
Amazon Cognito や Firebase Authentication といった IDaaS は利用せず、Go と Next.js で開発・運用されているアプリケーションとなっています。
実現までのいくつもの壁
このようなシステム構成から「予約システムと、ひとつになったPOSレジ」を実現するためには多くの課題がありました。 その中でも特に大変だったものをご紹介します。
アカウントの粒度が揃っていない問題
STORES レジから STORES 予約の情報を取得するには、とあるアカウントの STORES レジにおけるログインセッションから STORES 予約の取得すべき情報を特定できる必要があります。しかし、ログインセッションに含まれるアカウント情報だけではこれは不可能でした。
STORES レジは当時マルチアカウントに対応していないサービスで、権限制御もなかったため1アカウントでどのような情報にもアクセスできるシステムでした。 ざっくり説明すると利用店舗という概念があり、1アカウントが複数の利用店舗を設定でき、ログインする際は1アカウントが自由に利用店舗をひとつ選択できるという仕組みになっていました。
一方 STORES 予約はマルチアカウントに対応したサービスでした。STORES レジと同じく利用店舗のような概念もあったのですが、基本的に1アカウントは特定の利用店舗に紐づくような形となっており、他の利用店舗の情報を確認するためには特別な権限が必要となっていました。
たしかに ID 基盤によってアカウント自体は統一されていたのですが、1アカウントが取得できる情報の粒度が二つのサービスでバラバラだったため、このままでは連携ができませんでした。
アドホックに ID を割り振るようなこともできたのですが、これから STORES が実現したい未来を考えたとき、事業者の情報やお店の情報、そこで働く人たちの情報は STORES のサービス群で共通のものにすべきだろうと考え、今回のタイミングでその体験を目指すことにしました。 そこで、サービスごとにバラバラだった登場人物や概念を整理し統一させ、それを管理するシステムを作ることにしました。
社内では別のコードネームがあるのですが、便宜上ここではそのシステムを「事業者基盤」と呼ぶことにします。
様々なユースケースを想定すればどこまでも複雑な設計になり得たのですが、CTOである藤村さんの「我々の時間は有限であるからこそ、適切な制約を設けることで開発スピードというリターンを得たい」という思想のもと、STORES 全システムが備えるべき概念を以下のように整理しました。(本当はもう少し厳密な定義があるのですが一部の抜粋とさせてください)
* 組織 * 一般的に、会社は事業部や店舗などの要素が集まってツリーをなすものと考えられる * この要素のことを組織と呼ぶ * 組織種別 * 組織の種別のこと * 具体的には 1) 事業者 2) 店舗 3) ネットショップ がある * 事業者 * 組織の種別の一つ * 「STORES株式会社」とか * 店舗 * 組織の種別の一つ * 「STORESラーメン 上北沢店」とか * ネットショップ * 組織の種別の一つ * 「STORESラーメン オンラインストア」とか * (組織種別の中で店舗とネットショップを別概念としたのも多くの議論があったのですがここでは割愛させてください) * STORES アカウント * 人がSTORESというシステム全体で本人であることを認証するためのもの * (ここまでID基盤と呼んでいたもので作られたアカウントと理解してもらえればここでは大丈夫です) * 従業員 * 事業者に所属する人のこと * 最大一つのSTORES アカウントをもつ * 持っていない場合もある
本当はまだまだたくさん定義されている概念があるのですが、なんとなくこんな整理をしたんだなと思ってもらえればこのブログにおいては十分です。
STORES 予約と STORES レジをこれらの概念に従って作り直し、これらの概念を統一して管理する事業者基盤を開発し、STORES 予約と STORES レジが共通して事業者基盤の情報を参照することで各々のサービスは適切に必要な情報を取得できるというアーキテクチャへと作り変えました。 これによって1アカウントのログインセッションに対応する組織が一意に決まり、アカウントごとに適切な情報をSTORES 予約とSTORES レジそれぞれから取得できるようになりました。
簡単に作り替えましたと言ってしまいましたが、1から新たなシステムを作っていますし、歴史のある複数のサービスがお互いのことを理解しながら未来に向かって概念を整理し、お互いのシステムを作り変えていくのは本当に大変なことでした。本当に、本当に大変なことでした...ここにもたくさんのドラマとノウハウがあるのですが、話しているとキリがないのでまた別の機会にお話しできればと思います。
分散システムをどう開発していくか問題
事業者基盤を導入し各サービスを作り替えはじめた段階で、STORES も頑張って分散システムを運用する会社となりました。
まずはバックエンドシステム間の通信をどのように制御していけばよいのかを考える必要がありました。
なんですが実は STORES には「こんなこともあろうかと」ということで 卜部さん がほぼひとりで作った API Gateway がありました。ざっくり説明するとID 基盤から取得したアクセストークンをヘッダに付与して API Gateway に投げると、適切に認証・認可をしてくれて適切なバックエンドシステムにリクエストを流してくれるものです。
バックエンドシステム間の同期的な通信はこのシステムに乗っかることを一定のポリシーとしよう、という整理がされました。
他にも同期的なAPIリクエストだけでなく、非同期でメッセージのやり取りができる仕組みについても考える必要がありました。
なんですが実は STORES には「こんなこともあろうかと」ということで 卜部さん がほぼひとりで作った Pub/Sub があったのでした。ざっくり説明するとこのシステムにリクエストを飛ばすと、社内でこのシステムを Subscribe しているシステムに Webhook を飛ばしてくれるというものです。
バックエンドシステムの非同期的なやり取りはこのシステムに乗っかることを一定のポリシーとしよう、という整理がされました。
また、ローカルの開発環境をどうしていくかということも課題となってきました。手元にデカい Rails リポジトリをふたつ clone してきて、事業者基盤のリポジトリを clone してきて、それぞれコンテナを立ち上げて、と、やっているとあっという間にコンテナだらけになってしまい、さらにそれらをローカルで疎通させるのにも一苦労していました。
このあたりの工夫については先日開催した STORES Tech Conf 2024 で シムさん が話しているので、ご覧ください。
複数のバックエンドを持つクライアントをどのように開発するか問題
概念が整理され複数のバックエンド間で情報のやり取りができるようになりましたが、クライアントからそれらの情報をどう取得するかも課題でした。
「予約システムと、ひとつになったPOSレジ」を実現するためにはクライアントである iOS アプリケーションからこれまで通り STORES レジの Rails で提供されているAPIを利用するのに加えて、STORES 予約が提供しているAPIを利用する必要があります。
それぞれのシステムは当然別のログインセッションを持っており、それぞれのセッションを iOS アプリケーションに持たせてそれぞれリクエストしていく方式ではいくらなんでもクライアントの実装として複雑になりすぎるということで、いくつかの方式を考えることになりました。
一番リッチな設計案としてはそれぞれのシステムの前段に BFF がいてリクエストに応じて BFF が適切な情報を取得してくる、という設計も考えたのですが、まだふたつのサービスが連携する段階で本当にその設計が必要なのか確信が持てませんでした。早めにリリースして顧客の反応を見たかったこともあり、これまでどおり STORES レジの Rails がリクエストを受け、必要に応じて STORES 予約の API を利用するという構成にしました。
STORES レジはもともと GraphQL を利用しており、Schema Stitching という機能を活用して STORES 予約との通信を行うことにしました。(これも卜部さんがベースの実装を2日くらいでバーンと作ってきました)
より詳しい実装についてはこちらでも紹介しておりますのでご覧ください。
iOS アプリケーションとしても複数のバックエンドから適切にリソースを取得しながら、それらを使いやすい UI にまとめるという難しい挑戦となりました。 難しかった分動いたときの感動は大きく、TestFlight で配布された STORES レジで STORES 予約の情報が表示された時は本当に興奮しました。
これらの開発に着手したのが 2023年2月。ここで紹介した課題や紹介していない課題、多くの課題がありましたが、2024年1月には「予約システムと、ひとつになったPOSレジ」をリリースすることができました。 ひとつの機能がリリースされたというだけでなく、これから STORES が行っていくべきエンジニアリングの方向性が定まったという意味でも、重要な機能でした。
これからの STORES
まだまだシステムの理想の姿からは大きなギャップがあると考えています。
例えば分散システムを開発していく上でのベストプラクティスはまだまだ浸透しきれてないと感じますし、システム全体としての可観測性もまだ高められていないと感じています。
また、クライアントからの API 利用についても、ふたつのサービスを対象としているからまだ成り立っているアーキテクチャと感じており、これからバックエンドが増えるとやはり BFF のような層を考えなければならないかもしれません。
事業としてはこれから新プロダクトを10、20と増やしていくことを計画しており、その際は当然ID基盤・事業者基盤と接続することになります。Webフロントエンドもモバイルアプリもさまざまなものを開発し、運用していくことになります。 そうなったときに問題なく運用できるシステムにしていくにはまだまだ解かなければならない課題が多くありますし、それはシステムの話だけではなく組織の話にもなってくると思っています。
複数のスタートアップが合併し、それらのシステムを統合する形で再設計し作り直し、新たな価値を生み出していくという誰もやったことのない挑戦に、STORES のエンジニアは取り組んでいます。
このブログを通じて STORES が EC の会社ではないことが伝われば何よりです。