JK
Projekty
JK
>ŁADOWANIE // ŁADOWANIE
INICJALIZACJA WIDOKU
Zbudowane i zaprojektowane przezJK
JK
>ŁADOWANIE // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Powrót do projektów
Zobacz na GitHubie
System Nagród w Akcjach
Ukończonogopostgresgingolang-migratevalidatorlogrusdocker

System Nagród w Akcjach

Księga finansowa i backend nagród akcyjnych wdrażający księgowość podwójnego zapisu z dokładną wyceną w INR.

Problem i rozwiązanie

Zarządzanie nagrodami kapitałowymi wymaga bezwzględnej spójności transakcyjnej oraz odporności na awarie zewnętrznych źródeł cen.

Funkcje

  • Niezmienna księga transakcji podwójnego zapisu
  • Wycena w INR w czasie rzeczywistym z mechanizmami awaryjnymi
  • Automatyczne nabywanie uprawnień (vesting) i korekty akcji
  • Pełna ścieżka audytu spełniająca wymogi finansowe
  • Gwarancja spójności transakcyjnej z PostgreSQL

Stos technologiczny

TechnologiaWersjaCelDlaczego wybrano
Go1.25.1Język programowaniaSolidna biblioteka standardowa, prymitywy współbieżności, szybka kompilacja
Gin1.11.0Framework HTTP, routing, middlewareWysoka wydajność, wsparcie middleware, aktywna społeczność
PostgreSQL17Baza danych relacyjnaZaawansowane funkcje SQL (widoki, LATERAL, kolumny generowane, ENUM)
golang-migratev4.19.0Migracje bazy danychProsty runner migracji oparty na plikach
logrus1.9.3Logowanie strukturalneUstrukturyzowane pola i logowanie z poziomami
go-sqlmockv1.5.2Mockowanie bazy na potrzeby testówSymuluje SQL w testach jednostkowych bez prawdziwej bazy
lib/pqv1.10.9Sterownik PostgreSQLSprawdzony w boju, natywne wsparcie PostgreSQL
Docker—KonteneryzacjaPowtarzalne buildy, izolowane środowiska

Architektura

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.

Główne komponenty

  • Serwer HTTP Gin z middleware RequestID do śledzenia żądań
  • Handler nagród z wzorcem idempotencji opartym na SAVEPOINT
  • Handler korekt z blokowaniem na poziomie wiersza SELECT FOR UPDATE
  • Handlery odczytu (today, historical, portfolio, stats) odpytujące widoki PostgreSQL
  • Usługa PriceService w tle z pulą 5 goroutine i semaforem 3 miejsc dla bazy

Odporność systemu

  • Cache PriceCache w pamięci z RWMutex dla obciążeń z przewagą odczytów
  • Kontrola współbieżności oparta na CAS zapobiega kaskadowym opóźnieniom aktualizacji
  • Odzyskiwanie po panice w workerach aktualizacji cen zapobiega awariom procesu
  • Łagodne zamykanie HTTP z 10-sekundowym limitem opróżniania
  • Opakowanie transakcyjne z odroczonym rollbackiem dla wszystkich operacji zapisu

Struktura projektu

cmd
stocky-api
main.goPunkt wejścia: konfiguracja bazy, migracje, zadanie w tle, serwer HTTP, łagodne zamykanie
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goPriceService w tle z pulą workerów, cache, ponawianiem i symulowanym fetcherem
storage
models
utils

Punkty końcowe API

Stocky API (port 8080)

MetodaTrasaCel
GET/healthKontrola stanu — zwraca {status: OK}
POST/api/v1/rewardUtwórz nagrodę — idempotentnie, transakcyjnie, z obliczaniem opłat
POST/api/v1/adjustments/:idZastosuj korektę — blokowanie wiersza, wpisy księgowe według typu
GET/api/v1/today-stocks/:userIdDzisiejsze nagrody z widoku today_rewards
GET/api/v1/historical-inr/:userIdHistoryczna wycena w INR — paginowana, z mnożnikami zdarzeń
GET/api/v1/stats/:userIdPodsumowanie statystyk — dzisiejsze nagrody + łączna wartość portfela
GET/api/v1/portfolio/:userIdSzczegóły portfela — paginowane, filtrowane pod kątem wycofania z giełdy

Projekt bazy danych

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

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPDomyślnie: NOW()

Konta użytkowników. Zasilone jednym użytkownikiem testowym.

rewards

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, wartość ujemna = odwrócenie
idempotency_keyVARCHAR(64)UNIQUE, dopuszcza NULL
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Główne zdarzenia nagród. Unikalność na (user_id, stock_symbol, reward_date) z wariantem ignorującym wielkość liter.

ledger

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
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)Dopuszcza NULL
quantityNUMERIC(18,6)Dopuszcza NULL
amountNUMERIC(18,4)Dopuszcza NULL

Księga podwójnego zapisu. Każda nagroda generuje od 2 do 5 wpisów.

stock_prices

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Bieżąca cena
updated_atTIMESTAMPAktualizowane przez PriceService

Ostatnia znana cena akcji. Aktualizowana przez PriceService w tle.

stock_price_history

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE z stock_symbol

Dzienne migawki cen do wyceny historycznej. Wstawiane/aktualizowane przez PriceService.

stock_events

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)Licznik mnożnika
ratio_denNUMERIC(18,6)Mianownik mnożnika
new_symbolVARCHAR(20)Dla fuzji
effective_dateTIMESTAMPNOT NULL

Operacje korporacyjne. ratio_num/ratio_den definiuje mnożnik (np. split 2:1).

adjustments

KolumnaTypUwagi
idPKSERIALGenerowane automatycznie
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)Dopuszcza NULL

Modyfikacje istniejących nagród z blokowaniem na poziomie wiersza.

Jeden-do-wielu: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Niejawne odwołania krzyżowe: rewards.stock_symbol ↔ stock_prices.stock_symbol (przez UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (mnożniki zdarzeń). Brak formalnego FK między rewards a stock_prices/stock_events; egzekwowane na poziomie widoków.

Kluczowe decyzje projektowe

Widoki SQL do agregacji

Decyzja: Złożone obliczenia portfela/historyczne są zaimplementowane jako widoki PostgreSQL zamiast w kodzie aplikacji

Dlaczego: Zmniejsza złożoność aplikacji; wykorzystuje optymalizator zapytań PostgreSQL; jedno źródło prawdy dla obliczeń

Korzyści: Spójne obliczenia we wszystkich endpointach; baza efektywnie obsługuje JOIN-y i agregacje

Kompromisy: Widoki mogą być wolne przy dużych zbiorach danych; trudniejsze w debugowaniu niż kod aplikacji; zależność od PostgreSQL

Iloczyn logarytmiczny dla mnożników

Decyzja: Użycie EXP(SUM(LN(ratio))) do obliczania skumulowanych mnożników operacji korporacyjnych

Dlaczego: Unika przepełnienia/niedomiaru zmiennoprzecinkowego przy kumulacji wielu zdarzeń

Korzyści: Stabilne numerycznie; obsługuje dowolną liczbę zdarzeń

Kompromisy: Mniej czytelne niż bezpośrednie mnożenie; wymaga dodatnich współczynników

Architektura tylko z handlerami (bez warstwy usług)

Decyzja: Logika biznesowa znajduje się bezpośrednio w handlerach, bez osobnej warstwy usług

Dlaczego: Prostsze dla małego projektu; mniej plików i abstrakcji do utrzymania

Korzyści: Mało boilerplate'u; łatwe śledzenie przepływu żądania

Kompromisy: Trudniejsze ponowne użycie logiki; silne sprzężenie HTTP i dostępu do danych; trudniejsze niezależne testowanie logiki biznesowej

Symulowany fetcher cen

Decyzja: Użycie RandomPriceFetcher zamiast rzeczywistego API danych rynkowych

Dlaczego: Środowisko deweloperskie/testowe; brak potrzeby zarządzania kluczami API

Korzyści: Zero zewnętrznych zależności; niemal deterministyczne zachowanie

Kompromisy: Nieprzygotowane do produkcji; wartości portfela są bez znaczenia; trzeba zastąpić przy realnym wdrożeniu

Idempotencja oparta na SAVEPOINT

Decyzja: Użycie SAVEPOINT PostgreSQL do obsługi idempotencji bez przerywania zewnętrznej transakcji

Dlaczego: Zapewnia bezpieczeństwo ponowień bez zewnętrznej usługi idempotencji

Korzyści: Zaawansowana idempotencja w transakcji działająca w ramach ograniczeń PostgreSQL

Kompromisy: Złożona obsługa błędów; narzut SAVEPOINT

Brak uwierzytelniania/autoryzacji

Decyzja: Wszystkie endpointy są publicznie dostępne bez auth

Dlaczego: Projekt deweloperski/demo; uwierzytelnianie ma zostać dodane później (np. przez bramę API)

Korzyści: Prostsza implementacja; łatwiejsze testowanie

Kompromisy: Nieodpowiednie do produkcji; dowolny klient może tworzyć nagrody lub stosować korekty

Testy

Standardowe testy Go + go-sqlmock v1.5.2•19 testów jednostkowych

Co jest testowane

  • Handler nagród: idempotencja NULL, nieprawidłowy JSON, zerowa/ujemna ilość, normalizacja, idempotentne ponowienie (8 testów)
  • Handler korekt: nieprawidłowy ID nagrody, nieprawidłowy JSON, nieprawidłowy typ, zachowanie księgi przy odwróceniu (4 testy)
  • Handler historyczny: nieprawidłowy userId, nieprawidłowy limit (2 testy)
  • Handler portfela: nieprawidłowy userId, nieprawidłowy offset, filtr wycofania z giełdy ignorujący wielkość liter (3 testy)
  • Handler dzisiejszy: nieprawidłowy userId (1 test)
  • Handler statystyk: nieprawidłowy userId (1 test)

Brakujące pokrycie

  • Testy integracyjne z prawdziwym PostgreSQL
  • Testy handlerów odczytu z weryfikacją rzeczywistych danych
  • Testy aktualizatora cen w tle (ścieżki błędów, paniki, ponowienia)
  • Testy równoczesnego przetwarzania korekt
  • Testy scenariuszy rollbacku transakcji
  • Testy przypadków brzegowych paginacji

Kluczowe wyzwania

  • 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

Kluczowe wnioski

  • 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