毎月の定期便で様々な香りを楽しめる「カラリア」
まずは弊社のプロダクト「カラリア」ですが、理想の香りの香りと出会う場所というコンセプトの香り関連のプロダクトです。中でも主軸なのが、「カラリア 香りの定期便」という香水を中心とした商品のサブスクリプションサービスです。
約1000種類の商品から毎月ユーザーが好きなものを選べます。目に見えないからこそ難しい香りとの出会いをテクノロジーの活用によって身近に楽しんでもらうのが目的です。 2019年にサービスを開始して、現在はユーザー数が50万人以上いるサービスになっています。
ユーザー規模が大きくなってきているのでデータ分析の重要性が高いのが特徴で、今回紹介する負債も関連したものになっています。
本サービスのユーザー体験は、まずユーザーが定期便カレンダー(商品カート)にアイテムを追加します。 毎月ユーザーごとに決められた注文日に、自動でカレンダー内の商品が注文されてお届けするという形です。
毎月の自動注文が体験の基本の流れですが、定期注文のスキップや停止ができます。 つまり、注文の継続性に関する状態を持ちます。
この状態を定期便ステータスと呼んでいますが、これが今回紹介する負債の発生箇所になります。
定期便ステータスのデータの不備と分析の複雑さが顕在化
サービス開始から4年後の2023年に、定期便ステータスに関するデータ分析で問題が顕在化していきました。
問題は2つあって、1つ目はデータが不完全だったことです。 事業計画を立てる上で定期便ステータスは売り上げに関連するので重要度が高いのですが、注文した人が次の月にどういうステータスだったかを正確に出せませんでした。
もう1つの問題が、分析がかなり複雑化して難しいというところでした。データ基盤である程度吸収できたものの、データ基盤のメンテナンスコストが高く、高度な分析をする時には複雑なクエリを書く必要が生じてデータチームのリソースを圧迫していました。
この問題はデータの更新によって過去の情報が失われる実装になっていたことが原因だったんです。
スキップ、決済エラーといったサービスの開発初期からあるイベントを既存カラムの更新によって実装していました。
実際には定期便契約というテーブルに次回決済日を表すカラムがあります。スキップ機能を実現するにあたって次回の注文日を飛ばすために、注文日を1ヶ月後倒しにする実装になっていました。
これは当時の機能要件を満たすために工数を少なく実現できる実装方法でしたが、ずっとそのままにしていました。
こうすると全ての履歴情報が残らないので、過去情報の喪失を補うために途中からイベントログを出力していましたが、過去の状態をSQLで100パーセント復元するのが難しいログになっていました。
定期便ステータスの定義を見直して負債を解消
負債解消にあたって、まずは目的を2つ設定しました。
1つ目は、分析者やデータ利用者のニーズに答えられる状態にすることです。今後発生するデータの分析や、分析しやすいデータの持ち方を100%の精度でできるようにします。
もう1つが、上記の状態を維持しやすいデータにすることです。事業計画を作成するのにも重要なデータなので、一時的ではなく長期的に正しいデータが維持されやすいデータの持ち方にする必要があります。
2つの目的を達成するために、2つの方針を重視して進めました。
1つが、定期便ステータスや定期便に関するイベント、ステータス間の遷移についての定義と紐づくデータをドキュメント化することです。スキップやエラー、停止のようなステータス・イベントは時間をかけて少しずつ追加した関係で、仕様や遷移の発生については明文化されていませんでした。なので、改めて定義して社内で共通認識を持つことで、分析チームや開発チームで齟齬が生まれないようにしました。
2つ目が、ログ出力をただ改善するだけではなく、機能要件と分析要件を共に満たすデータの持ち方に完全に直すことです。データの完全性を一時的に満たすためだけならログの修正だけでも良いです。しかし、ログ出力にバグが入り込んでデータが欠損する可能性があるので、データの持ち方を直して機能要件と分析要件を共に満たせるデータ設計にしました。
そして、ステータスとイベントの定義を明確にした上で状態遷移図を作成していきました。
このデータ仕様を基にバックエンド側の自動テストとデータ基盤側のテストを組めば、仕様に合致しているかどうかを継続的に確かめられるのでデータを維持しやすくなります。
実際のテーブルとは厳密には異なりますが、先ほどの定期便契約テーブルに紐づく形でイベントテーブルが存在していて、スキップやエラーといったイベントが書き込まれます。
これによって状態遷移が発生して、ステータステーブルに新規レコードがインサートされ、既存のレコードが更新されるという形です。
データの完全性は状態とイベントの両方に履歴情報が保持されるようになって解決しました。データの完全性の維持に関しても、RDBのUNIQUE制約によって各定期便契約が単一の状態を持つことを保証しているので、実装にバグが生じてもDBレイヤーで弾けます。
分析の複雑性も分析状態の履歴が保持されているので、過去の状態を復元するのにログを参照する必要がありません。 副次的に状態が単一のテーブルに集約されてデータ仕様が理解しやすくなり、開発や分析のメンバーも容易に分析できるようになりました。
どうすれば負債の発生を防げたのか?
今回はデータの持ち方を完全に変えて負債を解消するまで3か月以上期間を要したので、今後は負債の発生を防いでいきたいです。
どうやって防げたかというと非常にシンプルで、データの更新を避ければ良かったと思います。
今回の負債発生の根本原因は、レコードの更新によって履歴情報が失われることにありました。
イベントを明示的に扱って、レコードの更新ではなく新しいレコードの作成で表現すると履歴が残るので、こういった問題は起きなかったと思います。
本サービスの開発初期の段階で全ての変更やアップデートに関する履歴情報を保持するのは、実装スピードの兼ね合いで難しかったのですが、特に売り上げや売り上げの予測に関連する情報のみはレコードの更新を避けるポリシーがあれば、実装スピードも落とさずに負債を回避できたと思います。
もう1つ別の切り口で見ると、機能追加の際に最適なデータの持ち方を考え直すのが大事だったと思います。
カラリアの場合、スキップ機能は後から追加されたので、最初の頃はスキップを前提とした作りではありませんでした。このように機能要件が変化すると、どうしても既存のモデルやデータの持ち方に引きずられて考えてしまいます。
しかし、既存のデータの持ち方でどう要件を満たせるかを考えるのではなく、要件を実現するのに最適なデータの持ち方は何かを考えるのが理想ですね。継続的にデータモデリングをやり続ける心持ちが大事だと思います。
どうすれば負債が大きくなることを防げたのか?
もう1つ重要なのが、事業に大きな影響が出始める前にどうすれば負債が大きくなるのを防げたかだと思います。
サービス開発初期はどうしても負債が生まれやすいです。初期は開発リソースも時間もないですし、パフォーマンス要件はサービスが成長してから発生する傾向にあります。そのため、ある程度は負債が生まれる前提で、生まれた負債とうまく向き合い続けることが重要です。
負債が大きくなるのを防ぐには、対処療法ではなく根本治療ですね。今回は過去情報が欠損している箇所で分析要件が挙がってきた時に、対処療法的にログを追加しました。しかし、元のデータの移行を検討すべきだったと思います。
あとはデータ移行を恐れずにやると、データ移行がチームとして習熟していきます。データの移行はサービスの規模が小さいほどコストが低いので、早い段階でガンガンデータを移行していくべきですね。
サービスを停止せずに行うデータ移行は手間になりますが、何度かやっているうちにチームが慣れていくので、根本治療もやりやすくなるでしょう。
負債を小さいうちに倒す→データ移行に習熟する→小さいうちに負債を倒す...という好循環を生み、大きなデータ負債も生みづらい環境を作るのが大事ですね。
設計レビューやデータ品質の管理で負債を予防する
最後にチームや組織として、データ負債を生みにくくする、あるいは大きくしないための取り組みを紹介します。
1つ目はDesign Docによる設計レビューです。我々の場合は2週間以上かかる比較的粒度の大きい開発プロジェクトについて、設計ドキュメントを記述して実装前の相互レビューをしています。
特に、継続的なデータモデリングを重点的にレビューして致命的なデータ負債の発生を防いでいます。
2つ目がデータチームと連携した継続的なデータ品質向上です。早い段階でのデータ課題の発見と解消を継続的に行うために、開発エンジニアとアナリティクスエンジニアでDevData定例と呼ばれるミーティングを実施しています。これによってデータに関する問題や要望をいち早くキャッチして改善するサイクルを繰り返しています。
最後は、組織全体でエンジニアリソースの20%程度をデータ含む技術的負債の解消やライブラリアップデート、新しい技術の検証などに当てられる仕組みを取っています。
これによってビジネスサイドとの合意形成なしで、エンジニアが優先度を付けて負債解消を積極的に進められます。
スタートアップにおいては特に負債を生まないのも大事ですが、負債を大きくしないのも大事です。
視聴者からの質問に答える質問タイムへ
――ここからは視聴者の質問に答えていきます。まず一つ目は「データモデリングの観点で、どのようにDesign Docsによるレビューを行っているのでしょうか」とのことですが、いかがでしょうか。
データモデリングの件では、要件を満たすためにエンティティの抽出をしっかりやることです。要件を切り出した時、ユーザーは定期便の注文をスキップできますが、スキップという概念をどう表現するかをエンティティで明示的に表現します。テーブル設計ではどうしても既存テーブルから入りがちですが、モデリングの概念から行います。
また、データ移行をどうやるかもDesign Docsに含めるようにしています。こういったレビューの事例として、定期便の注文に対して普通の商品と比べてオプションをつけられる機能をリリースしようとしているのですが、それと定期便ステータスの紐付けが名前に引っ張られるというのは最近ありました。
定期ステータスと定期便のオプションエラーステータスみたいな感じで紐付ける形がぱっと思いつきますが、よく考えてみると全然性質が違います。そのため、違う形で定義した方が良いというのはモデリングの段階で気付けるはずです。このように既存のテーブル名に引っ張られがちなケースが結構あります。
――続いて、「テーブル変更前のデータの扱いについて、ログを活用してデータ加工を行って新テーブルに移行したのか、あるいは過去データは今まで通りの対応で分析しているのでしょうか」とのことですが、いかがでしょうか。
ログを活用してデータ加工をして、新テーブルにぶち込んで移行しました。 データが欠損していた関係上、100%の精度では移行できてないのですが、「こういうログはこういう形で新テーブルに移行できる」という感じで事前に対応ルールを仕様として書いて、その詩を元にワンショットのバッチで移行する形で行いました。
――最後の質問です。「20%ルールやDevData定例は素敵だなと思いました。これらはいつどのようなきっかけで始めたのでしょうか」とのことですが、いかがでしょうか。
20%ルールは数年前からあるルールです。技術的負債に限らず新しい技術の検証の必要性は、解像度が一番高く見えているのはエンジニアなので、エンジニアが優先度を決めて進めた方が良いという背景から導入しています。
DevData定例は今回の負債が顕在化してきて、早期の課題抽出や改善サイクルを繰り返した方が良いという背景から、去年辺りから始まりました。