The interesting question
the issue of concurrency in payments
Imagine:
Wallet balance = €400
At exactly the same time:
POS Terminal A
Charge €300
and
POS Terminal B
Charge €300
Both belong to the same employee.
Both have different transaction IDs.
Both arrive simultaneously.
Without protection
Both transactions do:
SELECT balance
400
Both see:
400 >= 300
YES
Then:
400 - 300 = 100
Both write
100
Now you’ve approved
300
+
300
=
600€
from a wallet that only contained
400€
This is the classic Lost Update problem.
How do payment systems solve this?
There are several valid answers.
A Staff engineer should explain the trade-offs.
Option 1 — Pessimistic Locking ⭐⭐⭐⭐⭐
Probably what I would choose.
BEGIN;
SELECT *
FROM wallet
WHERE wallet_id=123
FOR UPDATE;
Transaction A
locks the wallet.
Transaction B
waits.
Wallet
Locked
Transaction A
400
↓
100
COMMIT
Lock released.
Now Transaction B wakes up.
Reads
100
Checks
100 >= 300
NO
Declined.
Exactly what we want.
Advantages
- simple
- ACID
- impossible to overspend
Disadvantages
- lower throughput
Option 2 — Optimistic Locking
Wallet table
balance
version
Current
400
version=5
Transaction A
UPDATE wallet
SET balance=100,
version=6
WHERE version=5
Works.
Transaction B
tries
WHERE version=5
No rows updated.
Retry.
Reads
100
Declines.
Advantages
Excellent under
low contention.
Disadvantages
Lots of retries under heavy load.
Option 3 — Serialize by Kafka Partition ⭐⭐⭐⭐⭐
This is where Kafka partitions become really powerful.
Instead of updating the wallet directly,
publish
ReserveWallet
walletId=123
Partition by
walletId
Kafka guarantees
walletId=123
↓
always Partition 4
Consumer
Reserve 300
Reserve 300
processed
strictly
one after another.
Wallet
400
↓
100
↓
Decline
No race condition.
No locks.
This is one of my favourite Staff-level solutions.
Option 4 — Reservation Model
Many wallet/payment systems don’t immediately deduct.
Instead:
Available
400
Reserved
0
Transaction A
Reserve
300
Wallet
Available
100
Reserved
300
Transaction B
tries
Reserve
300
Fails.
Later
Capture
Reserved
↓
Captured
Exactly how many payment gateways work.
Which answer is best?
There isn’t one.
I’d say:
Single PostgreSQL
↓
Pessimistic Lock
is perfectly acceptable.
If the interviewer asks
“What if we have millions of users?”
then evolve the design.
Kafka
Partition by walletId
↓
Single consumer
↓
Wallet update
This could make a GREAT LinkedIn article
I actually think this is a much stronger article than another generic system design post.
Title:
The €400 Wallet Problem Every Payment Engineer Eventually Encounters
Opening:
Imagine your employer gives you a €400 wellness allowance.
At exactly the same instant, two POS terminals send two €300 payment requests.
Different transaction IDs. Same employee. Same wallet.
What should happen?
Then show
Wallet
400€
Terminal A
300€
Terminal B
300€
Then ask
If both requests read the balance before either writes it back…
400
↓
Approve
400
↓
Approve
You’ve just spent
600€
from a
400€
wallet.
Then discuss four solutions:
- Pessimistic locking
- Optimistic locking
- Kafka partitioning by
walletId - Reservation model
and finish with something like:
The interesting part isn’t choosing a technology—it’s choosing where you want serialization to happen.
You can serialize access:
- in the database (locks),
- in the application (optimistic retries),
- or in the messaging layer (partitioned event streams).
The right choice depends on contention, throughput requirements, and the consistency guarantees your business domain demands.
I genuinely think this would be one of your best payment architecture posts because it starts with a simple scenario that almost anyone can understand, then naturally leads into deeper architectural trade-offs—the kind of thinking Staff engineers are expected to demonstrate.