JK
プロジェクト
JK
>読み込み中 // 読み込み中
ビューを初期化中
制作・デザイン:JK
JK
>読み込み中 // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
プロジェクト一覧に戻る
GitHubで見る
株式インセンティブ・リワードシステム
完了gopostgresgingolang-migratevalidatorlogrusdocker

株式インセンティブ・リワードシステム

複式簿記方式を採用し、INR換算と厳格な監査証跡を提供する株式インセンティブ管理バックエンド。

課題と解決策

株式報酬は厳密なトランザクション整合性と完全な監査追跡が不可欠であり、市場価格APIの障害時にも安定稼働する耐障害性が求められます。

機能

  • 改ざん不能な複式簿記式トランザクション元帳
  • リアルタイム株価取得とフォールバックキャッシュ機能
  • 権利確定(ベスティング)と株式調整の自動処理
  • 監査対応のトランザクション履歴トラッキング
  • PostgreSQLのACIDトランザクション保証

技術スタック

技術バージョン目的採用理由
Go1.25.1プログラミング言語強力な標準ライブラリ、並行処理プリミティブ、高速なコンパイル
Gin1.11.0HTTP フレームワーク、ルーティング、ミドルウェア高性能、ミドルウェア対応、活発なコミュニティ
PostgreSQL17リレーショナルデータベース高度な SQL 機能(ビュー、LATERAL、生成列、ENUM)
golang-migratev4.19.0データベースマイグレーションシンプルなファイルベースのマイグレーションランナー
logrus1.9.3構造化ログ構造化フィールドとレベル付きログ
go-sqlmockv1.5.2テスト用データベースモック実際の DB なしで SQL をシミュレート
lib/pqv1.10.9PostgreSQL ドライバー実績があり、ネイティブな PostgreSQL サポート
Docker—コンテナ化再現可能なビルドと分離された環境

アーキテクチャ

Docker Compose

Gin HTTP Server

:8080

Handlers

Reward, Adjustment, Today, Historical, Stats, Portfolio

PriceService Worker Pool

PriceCache

in-memory RWMutex

RandomPriceFetcher

PostgreSQL 17

:5432

7 Tables

3 Views

today_rewards, historical_rewards, user_portfolio

2 ENUMs

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

コアコンポーネント

  • リクエストトレース用の RequestID ミドルウェアを備えた Gin HTTP サーバー
  • SAVEPOINT ベースの冪等性パターンを用いたリワードハンドラー
  • SELECT FOR UPDATE による行レベルロックを用いた調整ハンドラー
  • PostgreSQL ビューを照会する読み取りハンドラー(today、historical、portfolio、stats)
  • 5 ゴルーチンのワーカープールと DB 用 3 スロットセマフォを備えたバックグラウンド PriceService

システムの耐障害性

  • 読み取り負荷の高い処理向けの RWMutex 付きインメモリ PriceCache
  • CAS ベースの並行性制御により連鎖的な更新遅延を防止
  • 株価更新ワーカーのパニックリカバリによりプロセスクラッシュを防止
  • 10 秒のドレインタイムアウトを伴う HTTP のグレースフルシャットダウン
  • すべての書き込み操作に対する遅延ロールバック付きトランザクションラッピング

プロジェクト構成

cmd
stocky-api
main.goエントリーポイント: DB セットアップ、マイグレーション、バックグラウンドジョブ、HTTP サーバー、グレースフルシャットダウン
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goワーカープール、キャッシュ、リトライ、シミュレートフェッチャーを備えたバックグラウンド PriceService
storage
models
utils

APIエンドポイント

Stocky API (ポート 8080)

メソッドルート目的
GET/healthヘルスチェック — {status: OK} を返す
POST/api/v1/rewardリワード作成 — 冪等、トランザクション対応、手数料計算
POST/api/v1/adjustments/:id調整の適用 — 行ロック、タイプ別の台帳エントリ
GET/api/v1/today-stocks/:userIdtoday_rewards ビューからの本日のリワード
GET/api/v1/historical-inr/:userId過去の INR 評価額 — ページネーション、イベント乗算
GET/api/v1/stats/:userId統計サマリー — 本日のリワードとポートフォリオ総額
GET/api/v1/portfolio/:userIdポートフォリオ詳細 — ページネーション、上場廃止フィルタ

データベース設計

users

4 cols
idPK
serial
username
varchar
email
varchar
created_at
timestamp

rewards

6 cols
idPK
serial
user_id
int
stock_symbol
varchar
quantity
numeric(18,6)
idempotency_key
varchar
reward_date
date

ledger

6 cols
idPK
serial
reward_id
int
entry_type
ledger_entry_type
stock_symbol
varchar
quantity
numeric(18,6)
amount
numeric(18,4)

adjustments

6 cols
idPK
serial
reward_id
int
adjustment_type
varchar
delta_quantity
numeric(18,6)
delta_amount
numeric(18,4)
reason
varchar

stock_prices

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
updated_at
timestamp

stock_price_history

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
date
date

stock_events

6 cols
idPK
serial
stock_symbol
varchar
event_type
stock_event_type
ratio_num
numeric(18,6)
ratio_den
numeric(18,6)
new_symbol
varchar
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

users

カラム型備考
idPKSERIAL自動生成
usernameVARCHAR(50)NOT NULL、UNIQUE
emailVARCHAR(100)NOT NULL、UNIQUE
created_atTIMESTAMPデフォルト: NOW()

ユーザーアカウント。テストユーザー 1 件がシードされます。

rewards

カラム型備考
idPKSERIAL自動生成
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL、負数は取消
idempotency_keyVARCHAR(64)UNIQUE、NULL 可
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

(user_id, stock_symbol, reward_date) の一意制約と大文字小文字を区別しないバリアントを持つ中核のリワードイベント。

ledger

カラム型備考
idPKSERIAL自動生成
reward_idINTFK → rewards(id) ON DELETE CASCADE
entry_typeledger_entry_typeENUM: stock_units、inr_outflow、fees、brokerage_fee、stt_fee、gst_fee
stock_symbolVARCHAR(20)NULL 可
quantityNUMERIC(18,6)NULL 可
amountNUMERIC(18,4)NULL 可

複式簿記形式の台帳。各リワードで 2〜5 件のエントリが生成されます。

stock_prices

カラム型備考
idPKSERIAL自動生成
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)現在価格
updated_atTIMESTAMPPriceService が更新

銘柄ごとの最新価格。バックグラウンドの PriceService が更新します。

stock_price_history

カラム型備考
idPKSERIAL自動生成
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL、stock_symbol との複合 UNIQUE

過去評価額用の日次価格スナップショット。PriceService がアップサートします。

stock_events

カラム型備考
idPKSERIAL自動生成
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split、bonus、merge、delist
ratio_numNUMERIC(18,6)乗数の分子
ratio_denNUMERIC(18,6)乗数の分母
new_symbolVARCHAR(20)合併用
effective_dateTIMESTAMPNOT NULL

コーポレートアクション。ratio_num/ratio_den が乗数を定義します(例: 2:1 分割)。

adjustments

カラム型備考
idPKSERIAL自動生成
reward_idINTFK → rewards(id) ON DELETE CASCADE
adjustment_typeVARCHAR(64)reward_reversal、fee_refund、manual_correction
delta_quantityNUMERIC(18,6)NOT NULL
delta_amountNUMERIC(18,4)NOT NULL
reasonVARCHAR(255)NULL 可

行レベルロックを用いた既存リワードの変更。

1 対多: users→rewards(CASCADE)、rewards→ledger(CASCADE)、rewards→adjustments(CASCADE)。暗黙の相互参照: rewards.stock_symbol ↔ stock_prices.stock_symbol(UPPER() 経由)、rewards.stock_symbol ↔ stock_events.stock_symbol(イベント乗数)。rewards と stock_prices/stock_events の間に正式な FK はなく、ビューレベルで強制されます。

主要な設計判断

集計に SQL ビューを採用

判断: 複雑なポートフォリオ/過去計算をアプリケーションコードではなく PostgreSQL ビューとして実装

理由: アプリケーションの複雑さを軽減し、PostgreSQL クエリオプティマイザを活用して計算の単一情報源を実現

利点: 全エンドポイントで一貫した計算が行われ、DB が JOIN と集計を効率的に処理

トレードオフ: 大規模データセットではビューが遅くなる可能性があり、アプリコードよりデバッグが難しく、PostgreSQL に依存

乗数に積の対数を使用

判断: 累積コーポレートイベント乗数の計算に EXP(SUM(LN(ratio))) を使用

理由: 多数のイベントが複合した際の浮動小数点オーバーフロー/アンダーフローを回避

利点: 数値的に安定し、任意の数のイベントを処理可能

トレードオフ: 直接乗算より読みにくく、正の比率が必要

ハンドラのみのアーキテクチャ(サービス層なし)

判断: ビジネスロジックをサービス層に分離せず、ハンドラーに直接配置

理由: 小規模プロジェクトではシンプルで、保守するファイルと抽象化が少ない

利点: 定型コードが少なく、リクエストフローを追跡しやすい

トレードオフ: ロジックの再利用が難しく、HTTP とデータアクセスの結合が強く、ビジネスロジックの独立テストが困難

シミュレートされた株価フェッチャー

判断: 実際の市場データ API ではなく RandomPriceFetcher を使用

理由: 開発/テスト環境であり、API キー管理が不要

利点: 外部依存がゼロで、ほぼ決定的な挙動

トレードオフ: 本番利用には不向きで、ポートフォリオ評価額に意味がなく、実運用には置き換えが必要

SAVEPOINT ベースの冪等性

判断: 外側のトランザクションを中断せずに冪等性を処理するため PostgreSQL SAVEPOINT を使用

理由: 外部の冪等性サービスなしでリトライ安全性を確保

利点: PostgreSQL の制約内で動作する高度なトランザクション内冪等性

トレードオフ: 複雑なエラーハンドリングと SAVEPOINT のオーバーヘッド

認証/認可なし

判断: 全エンドポイントが認証なしで公開

理由: 開発/デモ用プロジェクトで、認証は後から追加予定(例: API ゲートウェイ経由)

利点: 実装がシンプルでテストが容易

トレードオフ: 本番には不向きで、任意のクライアントがリワード作成や調整適用を行える

テスト

Go 標準テスト + go-sqlmock v1.5.2•ユニットテスト 19 件

テスト内容

  • リワードハンドラー: NULL 冪等性、不正 JSON、ゼロ/負の数量、正規化、冪等リトライ(8 テスト)
  • 調整ハンドラー: 不正なリワード ID、不正 JSON、不正タイプ、取消時の台帳挙動(4 テスト)
  • 履歴ハンドラー: 不正な userId、不正な limit(2 テスト)
  • ポートフォリオハンドラー: 不正な userId、不正な offset、大文字小文字を区別しない上場廃止フィルタ(3 テスト)
  • 本日ハンドラー: 不正な userId(1 テスト)
  • 統計ハンドラー: 不正な userId(1 テスト)

不足しているカバレッジ

  • 実 PostgreSQL に対する統合テスト
  • 実データ検証を含む読み取りハンドラーのテスト
  • バックグラウンド株価更新のテスト(エラーパス、パニック、リトライ)
  • 同時調整処理のテスト
  • トランザクションロールバックシナリオのテスト
  • ページネーションのエッジケーステスト

主な課題

  • 高頻度トランザクションにおけるレースコンディションの完全排除
  • 外部株価APIダウン時における堅牢なフォールバック設計

学んだこと

  • リレーショナルデータベースにおける複式簿記モデリング
  • 金融取引における悲観的・楽観的ロックの実践