JK
Projekt
JK
>LADDAR // LADDAR
INITIERAR VY
Byggd och designad avJK
JK
>LADDAR // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Tillbaka till projekt
Visa på GitHub
Aktiebelöningssystem
Slutförtgopostgresgingolang-migratevalidatorlogrusdocker

Aktiebelöningssystem

Finansiell huvudbok och aktiebelöningsbackend med dubbel bokföring och exakt INR-värdering.

Problem och lösning

Hantering av aktiebelöningar kräver strikt transaktionsnoggrannhet och feltolerans vid externa API-avbrott.

Funktioner

  • Oföränderlig transaktionshuvudbok med dubbel bokföring
  • Realtids- och fallback-värdering i INR
  • Automatiserad intjäning (vesting) och aktiejusteringar
  • Fullständig revisionshistorik för regelefterlevnad
  • PostgreSQL transaktionsintegritet

Teknikstack

TeknikVersionSyfteVarför vald
Go1.25.1ProgrammeringsspråkStark stdlib, concurrency-primitiver, snabb kompilering
Gin1.11.0HTTP-ramverk, routing, middlewareHög prestanda, middleware-stöd, aktiv community
PostgreSQL17RelationsdatabasAvancerade SQL-funktioner (vyer, LATERAL, genererade kolumner, ENUM)
golang-migratev4.19.0DatabasmigreringarEnkel filbaserad migrationskörare
logrus1.9.3Strukturerad loggningStrukturerade fält, nivåbaserad loggning
go-sqlmockv1.5.2Databasmockning för testerSimulerar SQL för enhetstester utan riktig DB
lib/pqv1.10.9PostgreSQL-drivrutinBeprövad, inbyggt PostgreSQL-stöd
Docker—ContaineriseringReproducerbara byggen, isolerade miljöer

Arkitektur

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.

Kärnkomponenter

  • Gin HTTP-server med RequestID-middleware för spårning av förfrågningar
  • Reward-handler med SAVEPOINT-baserat idempotensmönster
  • Adjustment-handler med SELECT FOR UPDATE radlåsning
  • Läshandlers (today, historical, portfolio, stats) som frågar PostgreSQL-vyer
  • Bakgrunds-PriceService med pool på 5 goroutines och 3-slots DB-semafor

Systemets motståndskraft

  • In-memory PriceCache med RWMutex för läslunga arbetslaster
  • CAS-baserad samtidighetskontroll förhindrar kaskaderande uppdateringsfördröjningar
  • Panikåterhämtning i price-updater-arbetare förhindrar processkrascher
  • Graceful HTTP-nedstängning med 10 s dräneringstimeout
  • Transaktionsomslutning med uppskjuten rollback för alla skrivoperationer

Projektstruktur

cmd
stocky-api
main.goIngångspunkt: DB-setup, migrationer, bakgrundsjobb, HTTP-server, graceful nedstängning
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goBakgrunds-PriceService med workerpool, cache, retry och simulerad fetcher
storage
models
utils

API-slutpunkter

Stocky API (port 8080)

MetodRuttSyfte
GET/healthHälsokontroll — returnerar {status: OK}
POST/api/v1/rewardSkapa belöning — idempotent, transaktionell, avgiftsberäkning
POST/api/v1/adjustments/:idTillämpa justering — radlåsning, redovisningsposter per typ
GET/api/v1/today-stocks/:userIdDagens belöningar från vyn today_rewards
GET/api/v1/historical-inr/:userIdHistorisk INR-värdering — paginerad, händelsemultiplicerad
GET/api/v1/stats/:userIdStatistiköversikt — dagens belöningar + totalt portföljvärde
GET/api/v1/portfolio/:userIdPortföljdetaljer — paginerade, filtrerade för avnotering

Databasdesign

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

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPStandard: NOW()

Användarkonton. Seedade med en testanvändare.

rewards

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, negativt = återföring
idempotency_keyVARCHAR(64)UNIQUE, tillåter NULL
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Kärnbelöningsevenemang. Unikt på (user_id, stock_symbol, reward_date) med skiftlägesokänslig variant.

ledger

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
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)Tillåter NULL
quantityNUMERIC(18,6)Tillåter NULL
amountNUMERIC(18,4)Tillåter NULL

Redovisningsbok med dubbel bokföring. Varje belöning genererar 2–5 poster.

stock_prices

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Aktuellt pris
updated_atTIMESTAMPUppdateras av PriceService

Senast kända pris per aktie. Uppdateras av bakgrunds-PriceService.

stock_price_history

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE med stock_symbol

Dagliga prisögonblicksbilder för historisk värdering. Upsertas av PriceService.

stock_events

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)Multiplikatorns täljare
ratio_denNUMERIC(18,6)Multiplikatorns nämnare
new_symbolVARCHAR(20)För fusioner
effective_dateTIMESTAMPNOT NULL

Företagshändelser. ratio_num/ratio_den definierar multiplikatorn (t.ex. 2:1-split).

adjustments

KolumnTypAnteckningar
idPKSERIALGenereras automatiskt
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)Tillåter NULL

Ändringar av befintliga belöningar med radlåsning.

En-till-många: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Implicita korsreferenser: rewards.stock_symbol ↔ stock_prices.stock_symbol (via UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (händelsemultiplikatorer). Ingen formell FK mellan rewards och stock_prices/stock_events; upprätthålls på vynivå.

Viktiga designbeslut

SQL-vyer för aggregering

Beslut: Komplexa portfölj-/historiska beräkningar implementeras som PostgreSQL-vyer i stället för applikationskod

Varför: Minskar applikationskomplexiteten; utnyttjar PostgreSQLs frågeoptimerare; en enda sanningskälla för beräkningar

Fördelar: Konsekvent beräkning över alla endpoints; databasen hanterar JOIN och aggregering effektivt

Avvägningar: Vyer kan vara långsamma för stora datamängder; svårare att felsöka än applikationskod; bundet till PostgreSQL

Logaritmisk produkt för multiplikatorer

Beslut: Använd EXP(SUM(LN(ratio))) för att beräkna kumulativa multiplikatorer för företagshändelser

Varför: Undviker flyttalsöverflöd/underflöde när många händelser kombineras

Fördelar: Numeriskt stabilt; hanterar ett godtyckligt antal händelser

Avvägningar: Mindre läsbart än direkt multiplikation; kräver positiva förhållanden

Endast-handlers-arkitektur (ingen servicelager)

Beslut: Affärslogiken ligger direkt i handlers utan ett separat servicelager

Varför: Enklare för ett litet projekt; färre filer och abstraktioner att underhålla

Fördelar: Lite boilerplate; lätt att följa förfrågningsflödet

Avvägningar: Svårare att återanvända logik; hård koppling mellan HTTP och dataåtkomst; svårare att testa affärslogik separat

Simulerad prisfetcher

Beslut: Använd RandomPriceFetcher i stället för ett verkligt marknadsdata-API

Varför: Utvecklings-/testmiljö; ingen API-nyckelhantering behövs

Fördelar: Inga externa beroenden; nästan deterministiskt beteende

Avvägningar: Inte produktionsklar; portföljvärden är meningslösa; måste ersättas för verklig driftsättning

SAVEPOINT-baserad idempotens

Beslut: Använd PostgreSQL SAVEPOINTs för att hantera idempotens utan att avbryta den yttre transaktionen

Varför: Möjliggör retry-säkerhet utan extern idempotens-tjänst

Fördelar: Avancerad idempotens inom transaktionen som fungerar inom PostgreSQLs begränsningar

Avvägningar: Komplex felhantering; SAVEPOINT-overhead

Ingen autentisering/auktorisering

Beslut: Alla endpoints är offentligt åtkomliga utan auth

Varför: Utvecklings-/demoprojekt; auth förväntas läggas till senare (t.ex. via API-gateway)

Fördelar: Enklare implementering; enklare testning

Avvägningar: Inte lämpligt för produktion; vilken klient som helst kan skapa belöningar eller tillämpa justeringar

Tester

Go standardtesting + go-sqlmock v1.5.2•19 enhetstester

Vad som testas

  • Belöningshandler: NULL-idempotens, ogiltig JSON, noll/negativ kvantitet, normalisering, idempotent retry (8 tester)
  • Justeringshandler: ogiltigt belönings-ID, ogiltig JSON, ogiltig typ, redovisningsbeteende vid återföring (4 tester)
  • Historikhandler: ogiltig userId, ogiltig limit (2 tester)
  • Portföljhandler: ogiltig userId, ogiltig offset, skiftlägesokänsligt avnoteringsfilter (3 tester)
  • Dagens handler: ogiltig userId (1 test)
  • Statistikhandler: ogiltig userId (1 test)

Saknad täckning

  • Integrationstester mot riktig PostgreSQL
  • Tester för läshandlers med verifiering av verklig data
  • Tester för bakgrundsprisuppdateraren (felsökvägar, paniker, retries)
  • Tester för samtidig justeringshantering
  • Tester för scenarier med transaktionsrollback
  • Tester för pagineringsgränsfall

Viktiga utmaningar

  • 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

Viktiga lärdomar

  • 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