---
title: "39/60 Days System Design Questions"
url: https://daily.dev/posts/39-60-days-system-design-questions-zqzig9cat
source_url: https://daily.dev/posts/39-60-days-system-design-questions-zqzig9cat
type: freeform
source: "Joud Awad"
author: "Joud Awad"
published: 2026-06-14T16:01:08.904Z
updated: 2026-06-14T16:01:30.756Z
reading_time: 2
upvotes: 56
comments: 12
language: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 39/60 Days System Design Questions

**[Joud Awad](https://daily.dev/sources/iac4jsbu0lv8wbsc85fsh)** · [@joudawad](https://daily.dev/joudawad) · 2 min read · 56 upvotes · 12 comments

## Summary

A payment system concurrency problem is presented where two users simultaneously read the same wallet balance and both attempt to spend more than the remaining funds, resulting in a negative balance. Four strategies are outlined: pessimistic locking (SELECT FOR UPDATE), optimistic locking (version numbers with retry), MVCC (snapshot isolation with conflict detection), and serializable isolation. The post challenges readers to identify which strategy silently allows double-spend, which kills throughput, and which they would actually deploy at 10K transactions/second.

## Content

You have a payment system. Two users try to spend from the same wallet balance at the same time.

Both read $200. Both want to spend $150. Both see enough balance. Both write the deduction.

The wallet is now at -$100. How do you stop this?

A) Pessimistic locking — SELECT FOR UPDATE on the wallet row. One transaction blocks until the other commits.

B) Optimistic locking — read the row with a version number, only write if version hasn't changed since you read it. Retry on conflict.

C) MVCC — let both reads see a consistent snapshot, rely on the database to detect write conflicts at commit time.

D) Serializable isolation — set the transaction isolation level to SERIALIZABLE and let the database handle it.

All four are real production strategies. One of them will silently allow double-spend under concurrent load. One will destroy your throughput on a high-write table. One is almost always the wrong default choice despite being the "safe" option.

Pick one — and tell me which one you'd actually ship in a payment system processing 10K transactions/second.

Full breakdown in the comments.

If you're building anything with concurrent writes — share this. It's the class of bug that costs money before you catch it.

Drop your answer 👇

#30DaysOfSystemDesign #SystemDesign #Databases #BackendEngineering

## Community discussion

Top comments from developers on daily.dev.

**@joudawad** · 7 upvotes

> **Why B wins (optimistic locking):**
>
>
> You read the wallet row, grab the version number. When you write, you include a WHERE version = :read_version condition. If another transaction already updated the row, your WHERE matches zero rows — conflict detected, retry.
>
> UPDATE wallets
>
> SET balance = balance - 150, version = version + 1
>
> WHERE id = :wallet_id AND version = :read_version;
>
> If rows_affected = 0 → conflict → retry. No locks held during the read. At 10K TPS with low conflict rates, this is significantly faster than pessimistic locking — you're only paying for retry cost on actual...

**@joudawad** · 4 upvotes

> **Why A is the trap answer (pessimistic locking):**
>
>
> SELECT FOR UPDATE works. It's correct. But it holds a row-level lock for the entire transaction — every concurrent write queues up.
>
>
> At 10K TPS on a popular wallet, you've serialized all writes to a single queue. Throughput collapses. Lock wait timeouts cascade — failed transactions retry, adding more contention. You've built a self-reinforcing bottleneck.
>
>
> Use pessimistic locking for low-concurrency paths (admin ops, batch jobs) where conflict probability is near 100%. Not for high-throughput payment writes.

**@joudawad** · 3 upvotes

> **Why C is the silent failure (MVCC + default isolation):**
>
>
> This is the dangerous one. PostgreSQL defaults to READ COMMITTED. MVCC gives each transaction a consistent snapshot — but at READ COMMITTED, that snapshot refreshes per statement, not per transaction.
>
>
> Two concurrent transactions both read $200, both pass the "sufficient funds" check, both commit. No conflict detected. **Double spend in production.**
>
>
> MVCC prevents dirty reads. It does NOT prevent lost updates at READ COMMITTED. Most engineers assume MVCC = safe from concurrent writes. It doesn't.

**@nathanpledger** · 1 upvotes

> I spent time with this IRL, so definitely helpful reminder. I'm going A, but as you suggest, behind queuing.
>
>
> There's also a bunch of due diligence to perform on every transaction against every other transaction by the customer and others, so speed and accuracy can be paramount. I've definitely found queuing to be very useful to control this.
>
>
> Fraud can happen fast but definitely don't get in the way of them client!

**@epicuser** · 1 upvotes

> my Answer is B

---

[View this post on daily.dev](https://daily.dev/posts/39-60-days-system-design-questions-zqzig9cat)
