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.