PaperTrader uses virtual funds. Its backend still needs to keep account balances, positions, and trades consistent when several requests arrive together.

A balance check isn’t enough

An account has $100. Two buy orders for $80 arrive at the same time. If both read the balance before either updates it, both can pass the funds check.

PaperTrader locks the account row during a trade, so another request affecting that account has to wait. It checks available funds and updates cash, positions, orders, and trade records within one transaction.

// row lock so two orders on the same account cannot both spend the same cash
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Account a where a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);
Java from AccountRepository.java

Handling retries

A client may retry an order after a slow response, even when the first request succeeded. The backend needs to recognise that retry.

Each order uses an idempotency key scoped to its account. Repeating the same key and payload returns the existing order. Reusing the key with a different payload is rejected.

Cancelling an order during execution

A cancellation can arrive while the matching engine is filling an open order. The backend must resolve those operations consistently. The tests cover this case alongside competing buys and repeated requests.

Open orders are stored in Postgres and loaded after a restart. Portfolio updates are sent after the transaction commits.

Testing concurrent requests

The integration tests submit competing requests, then check the resulting orders, balances, and positions. They verify that the account stays consistent when requests overlap.

This one starts two buys at the same moment. Each would leave too little cash for the other, so exactly one must succeed and only one trade can exist.

@Test void concurrentOrdersCannotSpendTheSameCash() throws Exception {
    var a = createAccount();
    try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
        var start = new CountDownLatch(1);
        Callable<Boolean> task = () -> {
            start.await();
            try { service.place(a.id(), buy(7)); return true; }
            catch (OrderRejectedException expected) { return false; }
        };
        var first = pool.submit(task); var second = pool.submit(task); start.countDown();
        assertThat(first.get(10, TimeUnit.SECONDS) ^ second.get(10, TimeUnit.SECONDS)).isTrue();
    }
    assertThat(service.trades(a.id())).hasSize(1);
    assertThat(portfolio(a).cash()).isEqualByComparingTo("300");
}
Java from OrderConcurrencyTest.java
Read the concurrency testsExplore PaperTrader