概要
- 15年前ぐらいに設計、実装したシステムの内容を公開します。
- 誰かに特許を取られてしまっても困るので公開しておきます。自分で特許を取るほどでもないので共有資産として残しておきます。特許というか、どちらかと言うと論文で発表するほうが向いていそうです。
- 航空会社のマイルで使われているポイント管理機能の実現方法です。このようなポイントシステムの要求はビジネスサイドではよくありそうですが、設計内容が公開されている事例を見つけられませんでした。
類似ポイントシステム
- JALやANAのマイルシステム
- まさにこれと同じ機能の実現方法です。
- 構築するのは難しいので、当時はこの機能を実現しているサービスは見つけられませんでした。
- 2022年の今となってはメジャーになりつつあります。様々な大手ポイントサイトで使われています。難易度が高かかったり、ユースケースが限られていたりするので中小規模のところではあまり採用されていないと思います。
- ビックカメラやヨドバシカメラのポイントシステム
- 最終利用日から1年で有効期限が切れる。
- この仕組の実装は簡単なのであちこちで採用されています。
要求
アクター
- ユーザ
- 経理
- システム管理者
機能要求
- 基本機能
- ユーザに対してポイントを加算、減算できるシステム。
- 加算に対して、任意の有効期限をつける。
- 有効期限が近いポイントから消費していく。
- 運用
- 減算を取り消すときには、減算前のポイントの有効期限を復元する。
- ある時点の残高を算出できる。
- ポイントの整合性をチェックできる。
- ユーザ向け機能
- 有効期限ごとに残高を表示できる。
- 現在の残高を取得できる。
- 経理
- できるだけオンラインで処理できるようにする
- 特定時点のBSの算出
- 特定時点のPLの算出
- BSとPLの一致
- システム
- 長期間運用しても遅くならないクエリーで取り出せる。
- 減算、加算の整合性を検算できる。(=BSとPLを作っておいて、整合性を検証)
ユースケースの例
- 2022年6月まで有効の100ポイントを加算
- 2022年7月まで有効の100ポイントを加算
- 150ポイントを消費。(1から100ポイント、2から50ポイントを引く)
アプローチ
いくつか設計のアプローチがあるので整理します。どのような特徴に注目して設計を行うかによっていくつかのアプローチがあります。
- トランザクションモデル
- 入金と出金の処理のトリガーは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つになるのでデータの管理が煩雑です。出金時のクエリーを複数回発行する必要があり、計算量が収束しないので処理が遅くなる可能性があります。
メリット・デメリット
Matrixで整理
- metric
- 有効残高算出
- 減算・加算の時系列一覧
- 計算量
- BSとPLによる検算
採用する手法
3番を採用します。
設計
ここでは物理スキーマの設計を行います。
実装
- 実装の方向性
- 各エンティティにまたがった排他制御が必要なのでRDBMSで実装するのが一般的かと思います。
- 行ロックはデッドロックを回避するために左側のエンティティから順番にかけていくように実装します。
加算
- Depositにレコードを入れるだけ。
残高の表示
- Depositから有効期限が未来のレコードを抽出し、DepoistWithdraw.amountのsum()を除算
減算
- トランザクション
- 残高をチェック。
- Depositから有効期限に近い順にソート
- 影響のあるDepositを排他ロック
- Withdrawへレコードの追加
- DepositWithdrawへレコードの追加
減算取り消し
- トランザクション
- 関連するDepositWithdrawを排他ロック
- Withdrawの削除
- 関連するDepositWithdrawの削除
一覧
加算と減算を別のテーブルで管理しているので加算と減算の一覧を時系列に出す処理が重くなってしまいます。計算量、メモリ使用量がO(n)です。
このシステムの設計における一番大きなデメリットになります。
- DepositとWithdrawをUNION ALLして時間でソートする。
- 上記の処理は重いのでユースケースレベルで加算と減算それぞれのページを用意する事によってパフォーマンスは担保できる。
ユーザの削除
- PLを簡単に出すために、退会したユーザに対しては減算処理をして残高を0にしておく必要がある。そうでないと、BSを出すときに退会ユーザを除外するのが難しくなる。現時点でのBSを出すのは簡単だが、過去のとある時点のBSを出す際にはクエリーが不可能ではないが困難になる。
ポイントの失効
- BSを計算する際に、expiration_dateを元に算出することになる。有効期限が切れたポイントに関連するPLを算出するのはクエリーが複雑になる。
- 考察
- PLを集計しやすいように、ポイントが失効したら減算するレコードを入れるバッチを作っても良いが、実行タイミングは失効と同時に行う必要があるので実行が困難。
検算
BSのスナップショットは持っていないので、PLの整合性を検証するのみになります。PLを複数テーブルで管理しているのでその整合性を確認します。
- sum(DepositWithdraw.amount) == sum(Withdraw.amount)
- Deposit.amount => sum(DepositWithdraw.amount)
考察
- この設計で今までに2つのアプリケーションを構築しましたが、不整合なしに稼働しています。
- 監査、経理、マーケティング用途の集計要求にも答えられています。そのかわり、クエリーが複雑なので正確な数値を出したい場合は少し大変です。

コメント