JK
Projecten
JK
>LADEN // LADEN
WEERGAVE INITIALISEREN
Gebouwd en ontworpen doorJK
JK
>LADEN // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Terug naar projecten
Bekijk op GitHub
Aandelenbeloningssysteem
Voltooidgopostgresgingolang-migratevalidatorlogrusdocker

Aandelenbeloningssysteem

Financieel grootboek en aandelenbeloningssysteem met dubbel boekhouden en nauwkeurige INR-waardebepaling.

Probleem en oplossing

Aandelenbeloningen vereisen strikte transactionele nauwkeurigheid en fouttolerantie bij uitval van externe beurs-API's.

Functies

  • Onveranderlijk dubbel boekhoudgrootboek
  • Real-time en fallback-waardering in INR
  • Geautomatiseerde vesting en aandelenaanpassingen
  • Volledige audit trail voor financiële controle
  • PostgreSQL-transactie-integriteit

Technologiestack

TechnologieVersieDoelWaarom gekozen
Go1.25.1ProgrammeertaalSterke stdlib, concurrency-primitieven, snelle compilatie
Gin1.11.0HTTP-framework, routing, middlewareHoge prestaties, middleware-ondersteuning, actieve community
PostgreSQL17Relationele databaseGeavanceerde SQL-functies (views, LATERAL, generated columns, ENUMs)
golang-migratev4.19.0DatabasemigratiesEenvoudige bestandsgebaseerde migratierunner
logrus1.9.3Gestructureerde loggingGestructureerde velden, logging met niveaus
go-sqlmockv1.5.2Database-mocking voor testsSimuleert SQL voor unit tests zonder echte DB
lib/pqv1.10.9PostgreSQL-driverBeproefd, native PostgreSQL-ondersteuning
Docker—ContainerisatieReproduceerbare builds, geïsoleerde omgevingen

Architectuur

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.

Kerncomponenten

  • Gin HTTP-server met RequestID-middleware voor requesttracing
  • Reward-handler met op SAVEPOINT gebaseerd idempotentiepatroon
  • Adjustment-handler met SELECT FOR UPDATE rijvergrendeling
  • Read-handlers (today, historical, portfolio, stats) die PostgreSQL-views bevragen
  • Achtergrond-PriceService met pool van 5 goroutines en 3-slot DB-semafore

Systeemveerkracht

  • In-memory PriceCache met RWMutex voor leesintensieve workloads
  • CAS-gebaseerde concurrencycontrole voorkomt cascadevertragingen bij updates
  • Panic-recovery in price-updater-workers voorkomt procescrashes
  • Gracieuze HTTP-shutdown met drain-timeout van 10 s
  • Transactieverpakking met uitgestelde rollback voor alle schrijfbewerkingen

Projectstructuur

cmd
stocky-api
main.goIngangspunt: DB-setup, migraties, achtergrondtaak, HTTP-server, gracieuze shutdown
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goAchtergrond-PriceService met workerpool, cache, retry en gesimuleerde fetcher
storage
models
utils

API-endpoints

Stocky API (poort 8080)

MethodeRouteDoel
GET/healthHealthcheck — retourneert {status: OK}
POST/api/v1/rewardReward aanmaken — idempotent, transactioneel, kostenberekening
POST/api/v1/adjustments/:idAdjustment toepassen — rijvergrendeling, ledgerposten per type
GET/api/v1/today-stocks/:userIdRewards van vandaag uit de view today_rewards
GET/api/v1/historical-inr/:userIdHistorische INR-waardering — gepagineerd, vermenigvuldigd met events
GET/api/v1/stats/:userIdStatistiekenoverzicht — rewards van vandaag + totale portefeuillewaarde
GET/api/v1/portfolio/:userIdPortefeuilledetails — gepagineerd, gefilterd op delisting

Databaseontwerp

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

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPStandaard: NOW()

Gebruikersaccounts. Gevuld met één testgebruiker.

rewards

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, negatief = terugdraaien
idempotency_keyVARCHAR(64)UNIQUE, staat NULL toe
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Kern-rewardevents. Uniek op (user_id, stock_symbol, reward_date) met hoofdletterongevoelige variant.

ledger

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
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 toegestaan
quantityNUMERIC(18,6)NULL toegestaan
amountNUMERIC(18,4)NULL toegestaan

Ledger volgens dubbel boekhouden. Elke reward genereert 2-5 posten.

stock_prices

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Huidige prijs
updated_atTIMESTAMPBijgewerkt door PriceService

Laatst bekende prijs per aandeel. Bijgewerkt door achtergrond-PriceService.

stock_price_history

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE met stock_symbol

Dagelijkse prijssnapshots voor historische waardering. Upsert door PriceService.

stock_events

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)Teller van multiplier
ratio_denNUMERIC(18,6)Noemer van multiplier
new_symbolVARCHAR(20)Voor fusies
effective_dateTIMESTAMPNOT NULL

Corporate actions. ratio_num/ratio_den definieert de multiplier (bijv. 2:1 split).

adjustments

KolomTypeNotities
idPKSERIALAutomatisch gegenereerd
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 toegestaan

Wijzigingen aan bestaande rewards met rijvergrendeling.

Een-op-veel: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Impliciete kruisverwijzingen: rewards.stock_symbol ↔ stock_prices.stock_symbol (via UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (eventmultiplicatoren). Geen formele FK tussen rewards en stock_prices/stock_events; afgedwongen op viewniveau.

Belangrijkste ontwerpbeslissingen

SQL-views voor aggregatie

Beslissing: Complexe portefeuille-/historische berekeningen zijn geïmplementeerd als PostgreSQL-views in plaats van applicatiecode

Waarom: Vermindert applicatiecomplexiteit; benut de PostgreSQL-queryoptimizer; één bron van waarheid voor berekeningen

Voordelen: Consistente berekening op alle endpoints; de database verwerkt JOINs en aggregaties efficiënt

Afwegingen: Views kunnen traag zijn bij grote datasets; moeilijker te debuggen dan applicatiecode; gebonden aan PostgreSQL

Logaritmisch product voor multiplicatoren

Beslissing: EXP(SUM(LN(ratio))) gebruiken om cumulatieve corporate-actionmultiplicatoren te berekenen

Waarom: Voorkomt floating-point overflow/underflow wanneer veel events samenkomen

Voordelen: Numeriek stabiel; verwerkt een willekeurig aantal events

Afwegingen: Minder leesbaar dan direct vermenigvuldigen; vereist positieve ratio's

Alleen-handlers-architectuur (geen servicelaag)

Beslissing: Bedrijfslogica staat direct in handlers zonder aparte servicelaag

Waarom: Eenvoudiger voor een klein project; minder bestanden en abstracties te onderhouden

Voordelen: Weinig boilerplate; requestflow eenvoudig te volgen

Afwegingen: Logica moeilijk te hergebruiken; sterke koppeling tussen HTTP en data-access; bedrijfslogica lastig afzonderlijk te testen

Gesimuleerde prijsfetcher

Beslissing: RandomPriceFetcher gebruiken in plaats van een echte marktdata-API

Waarom: Ontwikkel-/testomgeving; geen API-sleutelbeheer nodig

Voordelen: Geen externe afhankelijkheden; vrijwel deterministisch gedrag

Afwegingen: Niet productieklaar; portefeuillewaarden zijn betekenisloos; moet worden vervangen voor echte deployment

Op SAVEPOINT gebaseerde idempotentie

Beslissing: PostgreSQL SAVEPOINTs gebruiken om idempotentie af te handelen zonder de buitenste transactie af te breken

Waarom: Maakt retry-veiligheid mogelijk zonder externe idempotentieservice

Voordelen: Geavanceerde idempotentie binnen de transactie die binnen PostgreSQL-beperkingen werkt

Afwegingen: Complexe foutafhandeling; SAVEPOINT-overhead

Geen authenticatie/autorisatie

Beslissing: Alle endpoints zijn openbaar toegankelijk zonder auth

Waarom: Ontwikkel-/demoproject; auth wordt later toegevoegd (bijv. via API-gateway)

Voordelen: Eenvoudigere implementatie; eenvoudiger testen

Afwegingen: Niet geschikt voor productie; elke client kan rewards aanmaken of adjustments toepassen

Tests

Go standaard testing + go-sqlmock v1.5.2•19 unittests

Wat wordt getest

  • Reward-handler: NULL-idempotentie, ongeldige JSON, nul/negatieve hoeveelheid, normalisatie, idempotente retry (8 tests)
  • Adjustment-handler: ongeldige reward-ID, ongeldige JSON, ongeldig type, ledgergedrag bij terugdraaien (4 tests)
  • Historical-handler: ongeldige userId, ongeldige limit (2 tests)
  • Portfolio-handler: ongeldige userId, ongeldige offset, hoofdletterongevoelig delisting-filter (3 tests)
  • Today-handler: ongeldige userId (1 test)
  • Stats-handler: ongeldige userId (1 test)

Ontbrekende dekking

  • Integratietests tegen echte PostgreSQL
  • Tests voor read-handlers met verificatie van echte data
  • Tests voor achtergrond-prijsupdater (foutpaden, panics, retries)
  • Tests voor gelijktijdige adjustment-verwerking
  • Tests voor transactie-rollbackscenario's
  • Tests voor paginering-randgevallen

Belangrijkste uitdagingen

  • 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

Belangrijkste lessen

  • 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