有効期限付きポイントシステムの要求定義と設計
概要
- 有効期限付きポイント管理システムの要件定義と設計を公開します。
- 有効期限付きポイント管理システムとは、例えば航空会社のマイルで使われているポイント管理機能です。
- このようなポイントシステムの要求はビジネスサイドではよくありそうですが、設計内容が公開されている事例を見つけられませんでした。
どんな人向けの記事?
- 有効期限付きポイントシステムを開発するシステムアーキテクト、実装するエンジニアの方。
- ポイントシステムの要求定義を考える方。
ポイントシステム事例
既存のポイントシステムが使われている事例を確認してみます。
- 航空会社のマイルシステム
- ポイントの獲得日に対して有効期限が決まるシステムの設計と実装が比較的難しいので、当時はこの機能を実現しているサービスは見つけられませんでした。
- 2023年の今となってはこのポイントの仕組みはメジャーになりつつあります。様々な大手ポイントサイトで使われています。設計と実装の難易度が高かかったり、ユースケースが限られていたりするので中小規模のところでは同様なシステムは採用されていないです。
- まさにこれと同じ機能がほしいです。
- ビックカメラやヨドバシカメラのポイントシステム
- 最終利用日から1年で有効期限が切れる。
- この仕組の実装は簡単なのであちこちで採用されています。
要求定義
ポイントシステムと一言で言っても実は、運用する上では様々なユースケースを満たさなければなりません。よく論点になるユースケースを書き出しておきます。
アクター
- ユーザ
- 経理
- システム管理者
機能要求
- 基本機能
- ユーザに対してポイントを加算、減算できるシステム。
- 加算に対して、任意の有効期限をつける。
- 有効期限が近いポイントから消費していく。
- 運用
- 減算を取り消すときには、減算前のポイントの有効期限を復元する。
- ある時点の残高を算出できる。
- ポイントの整合性をチェックできる。
- ユーザ向け機能
- 有効期限ごとに残高を表示できる。
- 現在の残高を取得できる。
- 経理
- できるだけオンラインで処理できるようにする
- ある時点のBS(バランスシート)の算出
- ある時点のPL(Profit&Loss)の算出
- BSとPLの一致
- システム
- 長期間運用しても遅くならないクエリーで取り出せる。
- 減算、加算の整合性を検算できる。(=BSとPLを作っておいて、整合性を検証)
ユースケースの例
例えば、以下のようなユースケースを満たすようなシステムが理想ではあります。
- 2022年6月まで有効の100ポイントを加算
- 2022年7月まで有効の100ポイントを加算
- 150ポイントを消費。(1から100ポイント、2から50ポイントを引く)
- 150ポイントの消費を取り消す。元の状態に戻す。
アプローチ
いくつか設計のアプローチがあるので整理します。
どのユースケースを満たすためによって、取るべきアプローチが変わってきます。
それぞれの設計とメリット、デメリットをまとめておきます。
- トランザクションモデル
- 入金と出金の処理のトリガーはPLなので、必要最低限のPLを保存する。
- 会計的アプローチ
- PLとBS (有効期限ごと)を分離して保存する。
- オブジェクト指向
- 入金、出金をオブジェクトとして考える。
- 入金、出金、1つ1つがオブジェクトとなる。
1. トランザクションモデル
方向性は、入金と出金の履歴は最低限取らなければいけないのでそれぞれの履歴をエンティティとして表現します。
Userエンティティは、ユーザ情報のマスタです。
DepositHistoryは入金の履歴を表現するエンティティです。amountにはポイントの額を入れます。used_amountはその入金に対して利用済みの金額を保存するためのカラムです。デフォルトは0になります。 expiration_dateはその入金の有効期限の日付を入力します。
WithdrawHistoryは出金の履歴を表現するエンティティです。
入金と出金を1つのエンティティで表現しても良かったのですが、used_amountとexpiration_dateの2つのアトリビュートが出金には非従属なので正規化してエンティティで分けました。
このアプローチの良い点は、テーブルが2つで済むのでシンプルです。悪い点は、出金と入金の紐付けが行えないので出金の取り消しができません。
2. 会計的アプローチ
PLとBS (有効期限ごと)を分離して保存する。ユーザや管理者が見たいのは主にPLかBLのどちらかです。なので、ユーザ側の要求に近い状態でデータ自体も保存してしまえば集計も楽というアプローチです。
良い点は集計が容易です。悪い点は、1つ目と同じように入金と出金の紐付けがないので出金の取り消しが行なえません。
3. オブジェクト指向
入金、出金を別の事象と扱い、必然的に別のテーブルで扱います。
入金に対しての出金を厳密に関連付けることで取り消しを行えるようにします。
データとしてはPLを持ちます。BSはこのPLのデータを使って集計して出すか、参照頻度が高いのであれば別途BS用のテーブルを用意しても良いと思います。
BS用のテーブルを用意すると、集計は行いやすいですがその反面、PLの更新があるたびにBSのテーブルの更新も行う必要が出てきます。システムのユースケース頻度やトラフィックの多さによって使い分けるのが良いと思います。
実装
アプローチ3のシステムを実装する方法を記載します。
まず、実装するストレージに関しては、各エンティティにまたがった排他制御が必要なのでRDBMSで実装するのが一般的かと思います。
排他制御をするためにはロックを使います。行ロックはデッドロックを回避するために左側のエンティティから順番にかけていくように実装します。
以下に、各ユースケースごとにどのような実装するのかを文章で記載しておきます。
加算
- Depositにレコードを入れるだけ。
残高の表示
- Depositから有効期限が未来のレコードを抽出し、DepoistWithdraw.amountのsum()を除算
減算
- トランザクション
- 残高をチェック。
- Depositから有効期限に近い順でソート
- 影響のあるDepositを排他ロック
- Withdrawへレコードの追加
- DepositWithdrawへレコードの追加
減算取り消し
- トランザクション
- 関連するDepositWithdrawを排他ロック
- Withdrawの削除
- 関連するDepositWithdrawの削除
一覧
このシステムの設計における一番のデメリットになります。
加算と減算を別のテーブルで管理しているので加算と減算の一覧を時系列に出す処理が重くなってしまいます。計算量とメモリ使用量が O(n)
です。
加算と減算を一覧で見たい場合は以下の2つのアプローチがあります。
- DepositとWithdrawをUNION ALLして時間でソートする。
- 上記の処理は重いのでユースケースレベルで加算と減算それぞれのページを用意する事によってパフォーマンスは担保できる。
- 加算と減算の1つ上の概念の
入出金
というエンティティを追加して加算と減算への外部キーを持つような設計をする。
ユーザの削除
- PLを簡単に出すために、退会済みユーザに対しては減算処理をして残高を0にしておく必要がある。
- BSを出すときに退会ユーザを除外するのが難しくなりる。
- 現時点でのBSを出すのは簡単だが、過去のとある時点のBSを出す際にはクエリーがかなり複雑になる。
ポイントの失効
- BSを計算する際にexpiration_dateを元に算出する。有効期限が切れたポイントに関連するPLを算出するのはクエリーが複雑になる。
- PLを集計しやすいようにポイントが失効したら減算するレコードを入れるバッチを作っても良いが、実行タイミングは失効と同時に行う必要があるので実行が困難。
- ERのみでPLとBSを表現するためにもバッチなどで消し込むことは行わない。
検算
BSのスナップショットは持っていないので、PLの整合性を検証するのみになります。PLを複数テーブルで管理しているのでその整合性を確認します。
”OK” if sum(DepositWithdraw.amount) == sum(Withdraw.amount)
- “
OK” if Deposit.amount => sum(DepositWithdraw.amount)
実装が正しく動いているか確認するための保険としてdailyで上記のバッチを実行しておくのが良いです。
考察
- 15年前ぐらいに設計、実装しました。この設計で今までに2つのサービスを構築しましたが不整合無く稼働しています。
- 運用を開始してから、ビジネスサイドやファイナンスサイドからの要求に対しても基本的には答えられています。
- 監査、経理、マーケティング用途の集計要求にも答えられています。(設計が少し複雑なのでエンジニアかデータに詳しい人でないと複雑な抽出をするクエリーを書けないという問題はあります)
- 公開する目的は、他人に特許を取られてしまっても困るのと、自分で特許を取るほどでもないので共有資産として残しておきたかったためです。
Discussion
初めまして!ポイントに関する記事ってあまり見かけないので、資料として凄く助かります。
記事にしていただきありがとうございます!
質問なのですが、ポイントに関しては何によって付与されたポイントだとか
何に利用されたかなど。ポイント自体に関する情報とかはどのように管理されていたのか
お聞きできれば幸いです!
お役に立ててよかったです。
まず、前提としてスコープ外なので書いていなかっったのですが私がに運用するときにはDepositとWithdrawにtype属性を付けていて種別は区別するように運用しています。
そのうえで、他のビジネスロジックとの結合度が高い処理に対しては個別に考えます。
ビジネスロジックとの結合度が高い処理とは、例えば: アンケートを回答したらポイントを付与する。重複処理はしてはならないといった処理です。
実行するトリガーが1つしか無いようなら外部キーを貼る程度にしておきます。
実行するトリガーが複数ある場合は普通に中間テーブルを作る感じになると思います。(これが理想だと思います。)
ありがとうございます!!質問に回答までしていただき、本当に感謝しております、、
追加要件があったときなどを踏まえて、中間テーブルを用意していきたいと思います!
また更新が頻繁になりそうなので、インサート管理がメインなテーブル設計にしていければと思ってます。
BSとPLに関しても認識すらなかったので、記載ありがたったです!会計の勉強もしようと思います!