JK
Projects
JK
>LOADING // LOADING
INITIALIZING VIEW
Built and Designed byJK
JK
>LOADING // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Back to Projects
View on GitHub
Stock Reward System
Completedgopostgresgingolang-migratevalidatorlogrusdocker

Stock Reward System

Monolithic REST API for managing stock-based employee rewards with double-entry ledger tracking, idempotent reward creation, financial fee calculation, corporate action handling via SQL views, and a background price updater with worker pool concurrency.

Problem & Solution

Tracks stock rewards given to users, maintains a double-entry-style ledger for every financial event, and computes current and historical portfolio values in INR that account for corporate actions (splits, bonus issues, mergers, delisting). Intended for internal/corporate use where a company grants stock units to employees.

Features

  • Idempotent reward creation with per-user-per-day-per-stock uniqueness
  • Financial fee calculation (brokerage 0.5%, STT 0.1%, GST 18% on brokerage+STT)
  • Adjustment system (reversals, fee refunds, manual corrections) with row-level locking
  • Double-entry ledger for stock units, INR outflows, and fee entries
  • PostgreSQL views for portfolio aggregation using logarithmic multiplier accumulation
  • Background price updater with worker pool, retry, and in-memory cache
  • Corporate action handling: splits, bonus issues, mergers, delisting
  • Docker Compose deployment with multi-stage build

Tech Stack

TechnologyVersionPurposeWhy Chosen
Go1.25.1Programming languageStrong stdlib, concurrency primitives, fast compilation
Gin1.11.0HTTP framework, routing, middlewareHigh performance, middleware support, active community
PostgreSQL17Relational databaseAdvanced SQL features (views, LATERAL, generated columns, ENUMs)
golang-migratev4.19.0Database migrationsSimple file-based migration runner
logrus1.9.3Structured loggingStructured fields, leveled logging
go-sqlmockv1.5.2Test database mockingSimulates SQL for unit tests without real DB
lib/pqv1.10.9PostgreSQL driverBattle-tested, native PostgreSQL support
Docker—ContainerizationReproducible builds, isolated environments

Architecture

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.

Core Components

  • Gin HTTP server with RequestID middleware for request tracing
  • Reward handler with SAVEPOINT-based idempotency pattern
  • Adjustment handler with SELECT FOR UPDATE row-level locking
  • Read handlers (today, historical, portfolio, stats) querying PostgreSQL views
  • Background PriceService with 5-goroutine worker pool and 3-slot DB semaphore

System Resilience

  • In-memory PriceCache with RWMutex for read-heavy workloads
  • CAS-based concurrency control prevents cascading update slowdowns
  • Panic recovery in price updater workers prevents process crashes
  • Graceful HTTP shutdown with 10s drain timeout
  • Transaction wrapping with deferred rollback for all write operations

Project Structure

cmd
stocky-api
main.goEntry point: DB setup, migrations, background job, HTTP server, graceful shutdown
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goBackground PriceService with worker pool, cache, retry, simulated fetcher
storage
models
utils

API Endpoints

Stocky API (port 8080)

MethodRoutePurpose
GET/healthHealth check — returns {status: OK}
POST/api/v1/rewardCreate reward — idempotent, transactional, fee calculation
POST/api/v1/adjustments/:idApply adjustment — row locking, type-based ledger entries
GET/api/v1/today-stocks/:userIdToday's rewards from today_rewards view
GET/api/v1/historical-inr/:userIdHistorical INR valuation — paginated, event-multiplied
GET/api/v1/stats/:userIdStats summary — today rewards + total portfolio value
GET/api/v1/portfolio/:userIdPortfolio details — paginated, delist-filtered

Database Design

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

ColumnTypeNotes
idPKSERIALAuto-generated
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPDefault: NOW()

User accounts. Seeded with one test user.

rewards

ColumnTypeNotes
idPKSERIALAuto-generated
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, negative = reversal
idempotency_keyVARCHAR(64)UNIQUE, allows NULLs
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Core reward events. Unique on (user_id, stock_symbol, reward_date) with case-insensitive variant.

ledger

ColumnTypeNotes
idPKSERIALAuto-generated
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)Nullable
quantityNUMERIC(18,6)Nullable
amountNUMERIC(18,4)Nullable

Double-entry-style ledger. Each reward generates 2-5 entries.

stock_prices

ColumnTypeNotes
idPKSERIALAuto-generated
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Current price
updated_atTIMESTAMPUpdated by PriceService

Latest known price per stock. Updated by background PriceService.

stock_price_history

ColumnTypeNotes
idPKSERIALAuto-generated
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE with stock_symbol

Daily price snapshots for historical valuation. Upserted by PriceService.

stock_events

ColumnTypeNotes
idPKSERIALAuto-generated
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)Multiplier numerator
ratio_denNUMERIC(18,6)Multiplier denominator
new_symbolVARCHAR(20)For mergers
effective_dateTIMESTAMPNOT NULL

Corporate actions. ratio_num/ratio_den defines multiplier (e.g., 2:1 split).

adjustments

ColumnTypeNotes
idPKSERIALAuto-generated
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)Nullable

Modifications to existing rewards with row-level locking.

One-to-Many: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Implicit cross-referencing: rewards.stock_symbol ↔ stock_prices.stock_symbol (via UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (event multipliers). No formal FK between rewards and stock_prices/stock_events — enforced at view level.

Key Design Decisions

SQL Views for Aggregation

Decision: Complex portfolio/historical calculations implemented as PostgreSQL views rather than in application code

Why: Reduces application complexity; leverages PostgreSQL query optimizer; single source of truth for calculations

Benefits: Consistent computation across all endpoints; database handles JOINs and aggregations efficiently

Trade-offs: Views can be slow for large datasets; harder to debug than application code; tied to PostgreSQL

Logarithmic Product for Multipliers

Decision: Use EXP(SUM(LN(ratio))) to compute cumulative corporate event multipliers

Why: Avoids floating-point overflow/underflow when many events compound

Benefits: Numerically stable; handles arbitrary number of events

Trade-offs: Less readable than direct multiplication; requires positive ratios

Handler-Only Architecture (No Service Layer)

Decision: Business logic lives directly in handlers without a separate service layer

Why: Simpler for small project; fewer files and abstractions to maintain

Benefits: Low boilerplate; easy to trace request flow

Trade-offs: Harder to reuse logic; tighter coupling between HTTP and data access; harder to test business logic independently

Simulated Price Fetcher

Decision: Use RandomPriceFetcher instead of real market data API

Why: Development/testing environment; no API key management needed

Benefits: Zero external dependencies; deterministic-ish behavior

Trade-offs: Not production-ready; portfolio values are meaningless; must replace for real deployment

SAVEPOINT-based Idempotency

Decision: Use PostgreSQL SAVEPOINTs to handle idempotency without aborting the outer transaction

Why: Allows retry safety without external idempotency service

Benefits: Sophisticated in-transaction idempotency that works within PostgreSQL constraints

Trade-offs: Complex error handling; SAVEPOINT overhead

No Authentication/Authorization

Decision: All endpoints are publicly accessible with no auth

Why: Development/demo project; auth expected to be added later (e.g., via API gateway)

Benefits: Simpler implementation; easier testing

Trade-offs: Not suitable for production; any client can create rewards or apply adjustments

Testing

Go standard testing + go-sqlmock v1.5.2•19 unit tests

What's Tested

  • Reward handler: NULL idempotency, invalid JSON, zero/negative quantity, normalization, idempotent retry (8 tests)
  • Adjustment handler: invalid reward ID, invalid JSON, invalid type, reversal ledger behavior (4 tests)
  • Historical handler: invalid userId, invalid limit (2 tests)
  • Portfolio handler: invalid userId, invalid offset, case-insensitive delist filter (3 tests)
  • Today's handler: invalid userId (1 test)
  • Stats handler: invalid userId (1 test)

Missing Coverage

  • Integration tests against real PostgreSQL
  • Tests for read handlers with actual data verification
  • Background price updater tests (error paths, panics, retries)
  • Concurrent adjustment handling tests
  • Transaction rollback scenario tests
  • Pagination edge case tests

Key Challenges

  • Computing cumulative corporate event multipliers (splits, bonuses) using logarithmic aggregation in SQL views
  • Idempotent reward creation without external state — SAVEPOINT-based pattern with pq.Error inspection
  • Case-insensitive stock symbol matching across application and database layers
  • Race condition prevention in adjustments using FOR UPDATE row locking with dual application+database checks
  • Missing historical prices causing rewards to be invisible in historical views

Key Learnings

  • Transactional consistency using Go & PostgreSQL with proper rollback patterns
  • Advanced SQL techniques: LATERAL subqueries, EXP(SUM(LN())) for numerical stability
  • Worker pool concurrency with buffered channel semaphores for controlled DB writes
  • SAVEPOINT-based idempotency for retry-safe operations within transactions
  • Trade-offs of handler-only architecture vs service layer separation