Skip to content
Ben Pritchard
← All work

Live Trading Simulation

A real-time, multiplayer trading game for a trader-training platform, built in Java, Spring Boot and React.

Year
2025
Role
Lead developer — full stack, edtech startup
Status
Archived
Stack
  • Java
  • Spring Boot
  • STOMP
  • SockJS
  • TypeScript
  • React
  • Redux Toolkit
  • Chakra UI
  • Recharts
  • Vite
  • Jest
  • JUnit
  • Docker
  • nginx
  • GitHub Actions
Live Trading Simulation: cover image

The problem

An edtech startup building a training platform for stock traders wanted a way to make learning to trade feel like a game. The idea was a live session that a trainer runs for a room of people: everyone joins on their own device, watches the same market move in real time, reacts to breaking news, and competes on a live leaderboard.

I led the full-stack build of that simulation, from the Java back end and its real-time messaging through to the React interfaces for the host and the players.

Constraints

  • Everyone has to see the same market at the same moment. The game is only fair if a price move or a headline reaches every player together, so prices have to be pushed from the server, not polled.
  • Players join from phones. The player screen has to work on a small display with one hand free, while the host's screen is shown on a large display at the front of the room.
  • Connections drop. People lock their phones and walk between Wi-Fi access points. A dropped connection shouldn't break the session for everyone else.
  • Money maths has to be exact. Profit, loss and portfolio values feed a leaderboard that people are competing on, so rounding errors would be visible and unfair.

Approach & architecture

A host creates a session and gets a six-character join code. Players enter the code and a name, and appear in the host's lobby as they join. When the host starts the game, the server plays a scripted scenario: a five-minute price series with news headlines dropped in at key moments. Each player starts with $10,000 of virtual cash, chooses a trade size with a slider, and buys or sells. They can close any open trade at the current price, and the leaderboard re-ranks everyone by portfolio value as the market moves. When the scenario ends, a final leaderboard appears on every screen.

  • Back end. Java 17 and Spring Boot. REST endpoints handle the one-off actions (create, join, start), and STOMP over WebSocket handles everything live: trades in, and prices, headlines, trade confirmations and leaderboards out. Each session has its own set of topics, such as session.{code}.prices and session.{code}.leaderboard, so sessions never see each other's traffic.
  • Front end. React 19 and TypeScript with Vite, Redux Toolkit for session state, Chakra UI for the responsive layouts, and Recharts for the live price chart, which shows a sliding window of the most recent ticks.
  • Delivery. Two Docker images: the Spring Boot service, and nginx serving the built front end and proxying /api and the /ws WebSocket upgrade to the back end. Docker Compose runs them together. GitHub Actions runs the front-end lint, typecheck and tests, and the back-end JUnit tests, on every pull request.

Key decisions

A server-scheduled price tape

The simplest way to play a price series is a loop that sends a tick and sleeps for five seconds. That drifts, because every send adds a little time, and it ties up a thread per session. Instead, when a game starts, the server schedules every tick of the scenario up front on a shared thread pool, each at an absolute time measured from the start. Ticks stay on time however long the game runs, many sessions share a few threads, and stopping a game is just a matter of cancelling its scheduled tasks.

The server owns the closing price

When a player closes a trade, the client doesn't tell the server what price it closed at. The server uses the last price it broadcast for that session. A slow phone, or someone tampering with the request, can't close a trade at a better price than the market showed everyone else.

Exact money maths with BigDecimal

All cash, profit and loss and portfolio values are calculated with BigDecimal, with explicit precision and rounding, never with floating point. Buys and sells are handled symmetrically: a sell profits when the price falls, by the same percentage logic a buy uses when it rises. The leaderboard is recomputed from the trade records on every tick and every trade, so it can never drift out of step with the trades themselves.

Coalescing bursts of joins

When a trainer reads out the code, a whole room joins within a few seconds. Broadcasting a fresh leaderboard for every join would send a burst of near-identical updates to every screen. Joins instead schedule a leaderboard push half a second later, cancelling any push already waiting, so a burst of joins produces a single update.

Connections that recover on their own

Players identify themselves in the STOMP connection headers with their session code and participant ID. When a socket disconnects, the server removes that player and tells the lobby, so the host's view stays accurate. On the client, a WebSocket provider wraps the STOMP client with automatic reconnection and heartbeats in both directions. Subscriptions requested before the socket is ready are queued and applied once it connects, so components can subscribe whenever they mount without worrying about connection state. Errors go to a private per-user queue rather than the shared session topics.

Outcome

Automated tests (87 front end, 10 back end)
97
Price ticks pushed to every player at once
5s
  • A complete host and player experience: create, join, lobby, live trading with news, a live leaderboard and final results
  • Real-time updates for every player in a session over STOMP, with automatic reconnection
  • Containerised front and back end, with CI running both test suites on every pull request

What I'd do differently

  • Move session state out of memory. Sessions, players and trades live in the service's memory, which kept the build simple but means a restart ends every game and the service can't run as more than one instance. The next step would be Redis for state and a full message broker, such as RabbitMQ behind Spring's STOMP relay, so the service could scale out.
  • Make the server authoritative for opening prices too. Closes already use the server's price. Opens still take the price the client saw, and should be stamped by the server in the same way.
  • Load scenarios as data. The game plays a single scenario bundled with the service. Loading scenarios from storage would let trainers create and choose their own.