2020.05.13に開催された「オンライン開催!【シューマイ】Tech Lead Engineerから最新技術を学べ!Rails編」で話した「生鮮ECプラットフォームの バックエンドを支えるRails」についてのスライドです。
生鮮ECプラットフォームのバックエンドを支えるRailsクックパッド株式会社買物事業部部長勝間 亮
View Slide
•勝間 亮 (かつま りょう) @ryo_katsuma•2009~ クックパッド入社‣レシピ領域バックエンドエンジニア‣レシピ領域マネージャー•2018~ 買物事業領域立ち上げメンバー‣副部長 兼テックリード‣2020~ 部長
クックパッドマート•2018年〜買物領域の新規事業•生鮮食材のEC‣ 半分正しい、半分正しくない
クックパッドマート•インターネットで生鮮食材を扱う上での課題1. 生鮮食材の流通の課題2. 生鮮ECの課題
生鮮食材の流通の課題
これまでの流通網生産・出荷組合¥120市場・直売所¥130仲卸・問屋¥150小売店¥150〜198生産者¥50~80• お届けまで数日〜数週間• 中間業者が通るたびに利益が乗る構造• 生産者の手取りは小売価格に比べて少ない
解決アプローチクックパッドマート¥120〜150生産者¥80~100•当日集荷、当日配送• 生産者直売で地域で一番安く買える(= 生産者の手取りを増やせる)• 生産者直売でお届けまでのリードタイムが短い(= 新鮮なままお届け)
生鮮ECの課題
現状の生鮮EC•個配の配送コスト問題‣ 多くのECサービスは最低注文金額3,000~5,000円‣ まとめ買いをせざるをえない•再配達問題‣ 肉・魚などナマモノは宅配Boxや置き配を適用しづらい‣ お届け時間に必ず家にいないといけない制約
解決アプローチ•敢えて個配をしないピックアップ型EC‣ 生活動線上に受取り場所を作って自分で好きな時間で取りに行く‣ 再配達問題を解決•集荷配送コストの圧縮‣ 複数人の注文をまとめて配送することで配送コストを1/N‣ 生産者の集荷場所も一元化することで集荷コストも1/M‣ 最低注文金額を0円に
クックパッドマート•生鮮ECサービス•生鮮ECプラットフォーム
クックパッドマートのバックエンド
バックエンド•Rails 6.0.2•Ruby 2.6
可能なかぎり素朴なRailsに•MVC以上のレイヤーは極力増やさない‣ app/services• 複数Modelをまたがる処理‣ app/resources• RESTful APIレスポンスのClassを定義するModelのラッパー
モノリシックなサービス‣ ユーザー向けモバイルアプリAPI‣ 販売者向けWebアプリ‣ ドライバー向けモバイルアプリAPI‣ スタッフ向け管理Webアプリ‣ ハードウェアキッティング会社向け管理Webアプリ
モノリシックなサービス•新規事業なので変化がかなり大きい‣ サービスをまたいだ実装を単純化‣ アプリ分割によるModel層の管理の煩雑化を避けたい (DBは共通)•必要になればサービス分割を検討•必要な外部サービスは適宜利用‣ 決済: Stripe / Push通知: Firebase
実装も素朴•の、ように見える
実装も素朴?•の、ように見えるかもしれないが。。。
現実世界を反映した設計と開発
物体と概念のmodel設計
受け取り場所(マートステーション)
モデル設計• ステーションは複数の冷蔵庫を• 冷蔵庫は複数のコンテナを• コンテナは複数の注文商品を• 注文商品は商品にひも付き
モデル設計• Location has_many LocationFridges• LocationFridge has_many LocationFridgeContainers• LocationFridgeContainer has_many OrderItems• OrderItem belongs_to Item
コンテナの内容は日毎に変わる「ある日のコンテナ状況」を再現するときにSQLだけで解決したい
モデル設計• Location has_many LocationFridges• LocationFridge has_many LocationFridgeContainers• LocationFridgeContainer has_many DeliveryLocationFridgeContainers• DeliveryLocationFridgeContainer has_many OrderItems• OrderItem belongs_to Item5/10付
モデル設計のポイント•現実世界のものをそのままモデリングしても駄目•現実世界と結びつく「概念」をどう表現するか‣ 例) モノは同一でも時系列で状況が変わるもの‣ 表現をミスるとデータ取得が困難に
モデル設計のポイント•とはいえ何度も設計はミスってる•DB設計ミスに気づいたら積極的にリファクタリング‣ 作り直しは許容‣ 2度目はさすがにうまくいく
物理制約の設計
一般的ECの購入時チェック•在庫が存在するかどうか•ユーザー情報が正しいかどうか‣ 決済情報、住所など
集荷からお届けまで
集荷冷蔵庫集荷からお届けまで
配送車集荷からお届けまで
受け取り用冷蔵庫集荷からお届けまで
注文処理ではこれらのチェックが必要
受け取り冷蔵庫のキャパシティ•注文商品はコンテナに入る必要‣ order_items.sum(&:volume) ɹ<= delivery_location_fridge_container.available_capacityɹ‣ を満たすコンテナがあるかを確認
商品毎の重さや体積測定
配送車のキャパシティ•ステーションのコンテナが全て車に入る必要•配送資材(シッパー)にコンテナを入れて配送‣ delivery_location_fridge_containers.size <= distribution_routing_shipper.available_capacity‣ を満たす車内のシッパーがあるかを確認
集荷用冷蔵庫のキャパシティ•集荷用冷蔵庫にコンテナが入る必要•配送コンテナを設置する空き棚があるか‣ CollectionSpotFridgeAddress .where.not(id: already_assigned_address_ids) .merge(collection_spot_fridge: collection_spot_fridges)‣ を満たす空き棚があるかを確認
採番処理•注文された商品に‣ どの配送先冷蔵庫のどのコンテナに入るか‣ どの配送車のどの資材に入るか‣ どの集荷元冷蔵庫のどの棚に入るか•の情報(= nanika_id)を付加する採番処理を行う
物理制約の設計のポイント•制約の実装は複雑になりがち‣ 現実世界を表現すると仕方なし(と思ってる)•制約を採番処理と見立ててServiceClassにまとめる‣ DeliveryLocationFridgeContainerAssignerService‣ DistributionRoutingShipperContainerAssignerService‣ CollectionSpotFridgeAddressAssignerService•採番ができない場合は物理制約にひっかかったと見なしてraise
物理制約のValidation
一見、採番成功していても注意•タイムセール施策‣ 継続率向上•特定の商品への注文が短期間に集中‣ 同一データに対してread/writeが集中
実際にあった話•注文時の採番処理は完了•早朝にドライバーへの集荷指示データ作成時にraise•
実際にあった話•注文時の採番処理は完了•早朝にドライバーへの集荷指示データ作成時にraise•ドライバー稼働開始までにデバッグのタイムアタック
ここから9:00までにデータ不整合を直してドライバーに指示データを作成しないと集荷が間に合わない
起きたこと•決済サービスへの通信も挟まり、transaction処理を行いづらい•受け取り場所Aの注文データに対して受け取り場所Aには巡らない配送車の資材IDが採番‣ データとしてはIDは採番されている‣ 採番されるべきでないデータでコンテナが奪われる
要するにこうなっていた•DBのデータ的にはValidationはOK•物理世界を考慮したビジネスロジック的にはNG
対応方針•定期データチェックバッチ•DBのデータがビジネスロジック的に正しいか‣ 未来の全注文データを定期チェック‣ 最悪raiseしても落ち着いて対応
まとめ
まとめ•流通を愚直に表現すると‣ 物理と概念を的確に紐付けた設計する必要‣ 物理世界の制約を表現する必要‣ ビジネスロジックを満たすデータかチェックする必要
まとめ•愚直に実現することは(ご覧の通り)複雑•が、それはそれで面白い‣ 普通のRailsアプリではない技術的な楽しさ
https://www.wantedly.com/projects/300736
ご清聴ありがとうございました