JK
Projets
JK
>CHARGEMENT // CHARGEMENT
INITIALISATION DE LA VUE
Conçu et développé parJK
JK
>CHARGEMENT // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Retour aux projets
Voir sur GitHub
Système de Récompenses en Actions
Terminégopostgresgingolang-migratevalidatorlogrusdocker

Système de Récompenses en Actions

Grand livre comptable et backend de gestion d'actions appliquant la comptabilité en partie double et la valorisation précise en INR.

Problème et solution

La gestion d'actions requiert une rigueur transactionnelle absolue et une résilience sans faille face aux pannes des sources de prix externes.

Fonctionnalités

  • Grand livre immuable en partie double
  • Valorisation en direct en INR avec mécanismes de secours
  • Attribution automatisée (vesting) et ajustements d'actions
  • Piste d'audit complète pour conformité financière
  • Garantie d'intégrité transactionnelle avec PostgreSQL

Stack technique

TechnologieVersionObjectifPourquoi ce choix
Go1.25.1Langage de programmationBibliothèque standard solide, primitives de concurrence, compilation rapide
Gin1.11.0Framework HTTP, routage, middlewareHaute performance, support middleware, communauté active
PostgreSQL17Base de données relationnelleFonctionnalités SQL avancées (vues, LATERAL, colonnes générées, ENUM)
golang-migratev4.19.0Migrations de base de donnéesExécuteur de migrations simple basé sur des fichiers
logrus1.9.3Journalisation structuréeChamps structurés et journalisation par niveaux
go-sqlmockv1.5.2Simulation de base de données pour testsSimule SQL pour les tests unitaires sans BD réelle
lib/pqv1.10.9Driver PostgreSQLÉprouvé, support natif de PostgreSQL
Docker—ConteneurisationBuilds reproductibles, environnements isolés

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.

Composants principaux

  • Serveur HTTP Gin avec middleware RequestID pour le traçage des requêtes
  • Gestionnaire de récompenses avec modèle d'idempotence basé sur SAVEPOINT
  • Gestionnaire d'ajustements avec verrouillage au niveau ligne SELECT FOR UPDATE
  • Gestionnaires de lecture (today, historical, portfolio, stats) interrogeant les vues PostgreSQL
  • PriceService en arrière-plan avec pool de 5 goroutines et sémaphore de 3 emplacements pour la BD

Résilience du système

  • PriceCache en mémoire avec RWMutex pour les charges à forte lecture
  • Contrôle de concurrence basé sur CAS empêchant les ralentissements en cascade
  • Récupération des paniques des workers de mise à jour des prix pour éviter les crashs du processus
  • Arrêt HTTP gracieux avec délai de drainage de 10 s
  • Enveloppement transactionnel avec rollback différé pour toutes les opérations d'écriture

Structure du projet

cmd
stocky-api
main.goPoint d'entrée : configuration BD, migrations, tâche d'arrière-plan, serveur HTTP, arrêt gracieux
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goPriceService en arrière-plan avec pool de workers, cache, retry et fetcher simulé
storage
models
utils

Points d'API

Stocky API (port 8080)

MéthodeRouteObjectif
GET/healthVérification de l'état — renvoie {status: OK}
POST/api/v1/rewardCréer une récompense — idempotent, transactionnel, calcul des frais
POST/api/v1/adjustments/:idAppliquer un ajustement — verrouillage de ligne, écritures comptables par type
GET/api/v1/today-stocks/:userIdRécompenses du jour depuis la vue today_rewards
GET/api/v1/historical-inr/:userIdValorisation historique en INR — paginée, multipliée par les événements
GET/api/v1/stats/:userIdRésumé des statistiques — récompenses du jour + valeur totale du portefeuille
GET/api/v1/portfolio/:userIdDétails du portefeuille — paginés, filtrés par retrait de cote

Conception de la base de données

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

ColonneTypeNotes
idPKSERIALGénéré automatiquement
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPPar défaut : NOW()

Comptes utilisateurs. Initialisés avec un utilisateur de test.

rewards

ColonneTypeNotes
idPKSERIALGénéré automatiquement
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, négatif = annulation
idempotency_keyVARCHAR(64)UNIQUE, autorise NULL
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Événements de récompense principaux. Unique sur (user_id, stock_symbol, reward_date) avec variante insensible à la casse.

ledger

ColonneTypeNotes
idPKSERIALGénéré automatiquement
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)Autorise NULL
quantityNUMERIC(18,6)Autorise NULL
amountNUMERIC(18,4)Autorise NULL

Grand livre en partie double. Chaque récompense génère 2 à 5 écritures.

stock_prices

ColonneTypeNotes
idPKSERIALGénéré automatiquement
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Prix actuel
updated_atTIMESTAMPMis à jour par PriceService

Dernier prix connu par action. Mis à jour par le PriceService en arrière-plan.

stock_price_history

ColonneTypeNotes
idPKSERIALGénéré automatiquement
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE avec stock_symbol

Instantanés quotidiens des prix pour la valorisation historique. Upsertés par PriceService.

stock_events

ColonneTypeNotes
idPKSERIALGénéré automatiquement
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM : split, bonus, merge, delist
ratio_numNUMERIC(18,6)Numérateur du multiplicateur
ratio_denNUMERIC(18,6)Dénominateur du multiplicateur
new_symbolVARCHAR(20)Pour les fusions
effective_dateTIMESTAMPNOT NULL

Opérations sur titres. ratio_num/ratio_den définit le multiplicateur (ex. : split 2:1).

adjustments

ColonneTypeNotes
idPKSERIALGénéré automatiquement
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)Autorise NULL

Modifications des récompenses existantes avec verrouillage au niveau ligne.

Un-à-plusieurs : users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Références croisées implicites : rewards.stock_symbol ↔ stock_prices.stock_symbol (via UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (multiplicateurs d'événements). Aucune FK formelle entre rewards et stock_prices/stock_events ; contrainte appliquée au niveau des vues.

Décisions de conception clés

Vues SQL pour l'agrégation

Décision : Les calculs complexes de portefeuille/historiques sont implémentés sous forme de vues PostgreSQL plutôt que dans le code applicatif

Pourquoi : Réduit la complexité applicative ; exploite l'optimiseur de requêtes PostgreSQL ; source unique de vérité pour les calculs

Avantages : Calcul cohérent sur tous les endpoints ; la base gère efficacement les JOIN et agrégations

Compromis : Les vues peuvent être lentes sur de gros volumes ; plus difficiles à déboguer que le code applicatif ; dépendance à PostgreSQL

Produit logarithmique pour les multiplicateurs

Décision : Utiliser EXP(SUM(LN(ratio))) pour calculer les multiplicateurs cumulés des opérations sur titres

Pourquoi : Évite le dépassement/sous-dépassement en virgule flottante lorsque de nombreux événements se cumulent

Avantages : Numériquement stable ; gère un nombre arbitraire d'événements

Compromis : Moins lisible que la multiplication directe ; nécessite des ratios positifs

Architecture uniquement par gestionnaires (sans couche de service)

Décision : La logique métier vit directement dans les gestionnaires, sans couche de service séparée

Pourquoi : Plus simple pour un petit projet ; moins de fichiers et d'abstractions à maintenir

Avantages : Peu de code répétitif ; flux de requête facile à suivre

Compromis : Logique difficile à réutiliser ; couplage fort entre HTTP et accès aux données ; tests métier indépendants difficiles

Fetcher de prix simulé

Décision : Utiliser RandomPriceFetcher au lieu d'une API de données de marché réelle

Pourquoi : Environnement de développement/test ; aucune gestion de clé API nécessaire

Avantages : Aucune dépendance externe ; comportement quasi déterministe

Compromis : Pas prêt pour la production ; les valeurs du portefeuille n'ont pas de sens ; à remplacer pour un déploiement réel

Idempotence basée sur SAVEPOINT

Décision : Utiliser les SAVEPOINT PostgreSQL pour gérer l'idempotence sans annuler la transaction externe

Pourquoi : Permet une sécurité de reprise sans service d'idempotence externe

Avantages : Idempotence avancée intra-transaction compatible avec les contraintes PostgreSQL

Compromis : Gestion d'erreurs complexe ; surcoût des SAVEPOINT

Aucune authentification/autorisation

Décision : Tous les endpoints sont accessibles publiquement sans authentification

Pourquoi : Projet de développement/démo ; authentification prévue ultérieurement (ex. : via une passerelle API)

Avantages : Implémentation plus simple ; tests plus faciles

Compromis : Pas adapté à la production ; tout client peut créer des récompenses ou appliquer des ajustements

Tests

Tests standard Go + go-sqlmock v1.5.2•19 tests unitaires

Ce qui est testé

  • Gestionnaire de récompenses : idempotence NULL, JSON invalide, quantité nulle/négative, normalisation, retry idempotent (8 tests)
  • Gestionnaire d'ajustements : ID de récompense invalide, JSON invalide, type invalide, comportement du grand livre lors d'une annulation (4 tests)
  • Gestionnaire historique : userId invalide, limite invalide (2 tests)
  • Gestionnaire de portefeuille : userId invalide, offset invalide, filtre d'exclusion insensible à la casse (3 tests)
  • Gestionnaire du jour : userId invalide (1 test)
  • Gestionnaire de statistiques : userId invalide (1 test)

Couverture manquante

  • Tests d'intégration contre PostgreSQL réel
  • Tests des gestionnaires de lecture avec vérification de données réelles
  • Tests du planificateur de prix en arrière-plan (chemins d'erreur, paniques, retries)
  • Tests de traitements d'ajustements concurrents
  • Tests de scénarios de rollback de transaction
  • Tests des cas limites de pagination

Défis clés

  • Éliminer les conditions de concurrence dans les transactions financières
  • Concevoir des stratégies de repli en cas de défaillance des APIs de marché

Apprentissages clés

  • Modélisation comptable en partie double dans une base relationnelle
  • Verrouillage optimiste et pessimiste pour sécuriser les transactions