gold-app · engineering showcase

A real-time trading backend for gold coins, bullion and currency.

Backend of a two-person fintech project, built in Go for a phone-OTP trading platform. I handled the backend, while the other team member worked on the frontend. The system includes live SSE price feeds, Redis-backed quotes, WebSocket order updates and a double-entry ledger backed by PostgreSQL transactions. This page focuses on the backend architecture and implementation.

spec sheet

runtime
Go 1.26 · Gin
storage
PostgreSQL 18 · Redis 9
realtime
WebSocket · Server-Sent Events
deploy
Docker · docker-compose · one VPS
auth
Phone OTP · Redis session store

Recorded walkthrough

A short screen recording of the platform in use — login, live prices, placing an order and the admin side of approving and settling it.

demo

Architecture

single Go binary, no microservices

Everything runs as one Gin application plus three background goroutines. Postgres is the source of truth for money and orders; Redis handles everything that's ephemeral or needs to fan out fast — sessions, quotes, rate limits and the price Pub/Sub feed. Nothing here needs a message queue.

Sources

  • External price hub API
  • SMS gateway (OTP delivery)
  • Admin console actions
  • User web app
→

Gin API + workers

  • Auth / OTP service
  • Market & quote service
  • Order & settlement service
  • Price feeder (poll → publish)
  • Order-expiry worker
  • Market daily-close worker
→

State & realtime

  • PostgreSQL — durable state
  • Redis — sessions, quotes, rate limits
  • Redis Pub/Sub → SSE price stream
  • WebSocket hub → per-user push

What it does, and how

12 entries

Kept as a ledger, since that's the theme of the system itself — every row is a piece of the backend, how it's implemented, and the problem it solves.

component implementation purpose
OTP authentication Redis rate limiter + 6-digit code, atomic verify via a Lua script Passwordless login for phone-based users, brute-force resistant
Session store Redis hash per session, sliding TTL refreshed on every request Stateless API — any instance can serve any request
Live price feed Poll external price hub, publish to Redis Pub/Sub, stream over SSE Sub-second price updates without a persistent socket per client
Order notifications WebSocket hub keyed by user ID, separate admin broadcast channel Push order status changes the instant an admin acts
Quote locking Signed quote written to Redis with a short TTL Freezes a price for N seconds before an order can be placed
Order expiry Redis keyspace-notification listener + background worker Unconfirmed orders expire on their own — no polling loop
Market auto-close Ticker-based worker checking a configured close time Trading stops on schedule without manual operator action
Settlement engine Partial money/asset settlement inside one DB transaction An admin can settle an order in installments, safely
Double-entry ledger Append-only ledger + running balance, row-locked with FOR UPDATE Full audit trail, no negative-balance race conditions
Rate limiting & cooldowns Redis counters with TTL, scoped per phone number and per user Stops OTP abuse and rapid-fire order placement
Role-based access Session-derived claims checked by route middleware One session model powers both the user and admin panels
Safe migrations golang-migrate, with an automatic pg_dump before every deploy Rollback-ready schema changes in production

Life of an order

8 steps

From tapping "buy" to money and asset landing in the ledger — this is the exact path a trade takes through the system.

  1. 01

    Request an OTP

    The phone number is checked against the users and admins tables, Redis rate-limits repeat attempts, and an SMS provider sends the code.

  2. 02

    Verify and open a session

    The code is checked with an atomic get-and-delete Lua script. A session is written to Redis and a cookie is set on the response.

  3. 03

    Get a quote

    The client asks for a price. Buy and sell prices are derived from the live base price and the product's coefficients, then cached in Redis with a short TTL.

  4. 04

    Place an order

    The quote is atomically read and deleted from Redis, the price is sanity-checked against the current market rate, and a pending order is created with a matching expiry key.

  5. 05

    Admin review or auto-approval

    An admin approves or rejects the order — or the market's auto-approve setting does it instantly. Approval updates stock, balance and asset holdings in one transaction.

  6. 06

    Expiry, if nobody acts

    Redis fires a key-expired event; a background worker locks the order and marks it expired (or auto-approves it, depending on market settings).

  7. 07

    Settlement

    An admin settles the money and/or asset owed, in full or in parts. Every movement is written to an append-only ledger with a running balance.

  8. 08

    Reporting

    Exposure, profit, and top user/product reports are computed straight from the ledger and order tables for the admin dashboard.

Stack

Language & framework

  • Go 1.26
  • Gin
  • GORM

Data & realtime

  • PostgreSQL 18
  • Redis 9 — sessions, Pub/Sub, rate limiting
  • Server-Sent Events
  • WebSocket (gorilla/websocket)

Infra & ops

  • Docker multi-stage builds
  • docker-compose
  • golang-migrate
  • Automated pre-migration backups

This showcase site

  • Plain HTML, CSS & JS
  • No build step
  • Nginx
  • Docker

Data model

postgres · gorm · golang-migrate

Fourteen tables, grouped by what they're responsible for. Every money-moving table (orders, ledgers, settlements) is written inside a transaction with row-level locks — nothing here trusts the application layer to be the only writer.

Identity & access

  • admins
  • users
  • plans
  • plan_limits

Trading

  • products
  • markets
  • market_notes
  • orders
  • order_notes

Money & audit

  • assets
  • ledgers
  • asset_ledgers
  • settlements
  • audit_logs