DEV Community

zzzu2221
zzzu2221

Posted on • Originally published at Medium

A Game Wallet Is More Than a Number: Handling Retries and Concurrency

A game wallet often starts as a single balance field. That is fine for a prototype, but payment retries and unreliable networks quickly make the number hard to trust.

A player can tap “buy,” lose the connection, and try again. A store callback can arrive more than once. Two devices can spend the same account at nearly the same time.

The fix is to treat the balance as a cached view of a ledger.

Record every change

Instead of silently changing a balance, record events such as:

  • a verified payment granting virtual currency;
  • an item purchase spending currency;
  • a refund creating a compensating entry;
  • an administrative adjustment with an explicit reason.

The current balance remains useful for fast reads, but the ledger explains where it came from.

Make payment delivery idempotent

A payment callback is a message that may be retried. I use an idempotency key derived from the provider and transaction ID, then enforce uniqueness in the database.

The delivery flow is straightforward:

  1. Verify the external transaction.
  2. Record the payment.
  3. Grant currency with the unique key.
  4. Mark delivery complete.
  5. Return the original result for later retries.

This prevents a temporary network problem from becoming a double grant.

Protect spending too

Client-side balance checks are useful for interface feedback, but they cannot protect an account. The server should lock the wallet row, check the available amount, write the ledger entry, and update the cached balance in one transaction.

The same request key should return the original purchase result instead of charging twice.

Keep payment, wallet, and item orders separate

A real-money payment, a virtual-currency movement, and item delivery are connected but different events:

payment order → wallet grant → item order → wallet spend

That separation makes refunds and reconciliation much easier. When something goes wrong, support can identify which step is missing instead of guessing from one mutable number.

Tests that expose the real failures

Before building a large shop UI, test repeated callbacks, crashes between payment and delivery, concurrent spends, timeout retries, failed item delivery, and refunds after currency has been spent.

The important shift is simple: a wallet is not just a number. It is a history of decisions. Making those decisions explicit and idempotent keeps payment failures explainable.

Top comments (0)