Low-level design
Part 3 of 3 · Comments and RepliesComments and Replies - Concurrent IDs, Snapshots & Post Isolation
Concurrent ID allocation, defensive snapshots, post isolation, lock granularity decision chart, follow-ups.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does the concurrent test actually assert?
Answer
500 comments, no errors, len(ids) == len(set(ids)), and ids == sorted(ids) in list order.
L2
Why must the increment and the append share the lock?
Answer
An id that is bumped and not yet appended can be observed out of order, or two threads can publish the same id.
L3
Why RLock rather than Lock?
Answer
If a method calls another method that also locks, Lock deadlocks. RLock allows the same thread to re-enter.
L4
What is a defensive snapshot?
Answer
list returns new frozen views. get_thread deepcopy's the nested reply lists. parent.replies is never returned.
L5
How is a post isolated?
Answer
Comments live on that post's list. list_comments(p2) cannot see p1. A reply stores the parent's post_id.
L6
When would you switch to per-post locks?
Answer
When one global lock shows up in a profile. Keep a striped map keyed by post_id, and still protect a global sequence.
L7
What does an atomic counter not solve?
Answer
The append to two structures. Atomics cover a single word. This update is a counter plus two collections. See the atomics page before dropping the lock.
Failure modes
Id allocated outside the lock
Two threads read the same counter and both append it. The uniqueness assertion fails.
Live reply list returned
The caller appends a dict and the next snapshot shows it. deepcopy is the cut.
Global sequence outside per-post locks
Per-post locks do not order a process-wide counter. Ids collide or go backwards.
Misconceptions
Sorting by timestamp fixes races.
It hides them and then reorders history. The list order under the lock is the history.
Per-post locks make ids globally unique for free.
Only if each post has its own id space. A global comment_id still needs a shared sequence.
RWLock is always faster for list_comments.
Short critical sections often prefer a mutex. The RWLock page is the measurement, not a default.
Interviewer traps
Replace the lock with atomics because you mentioned threads.
The critical section updates more than one word. Say why the lock stays, and link atomics for the single-variable case.
Design edit and delete before isolation is true.
Soft-delete can be a follow-up. It must not reorder the list or break snapshots.
Design scenario
Same prompt for every reader.
Requirements
Unique monotonic ids, copies on read, no cross-post leakage, and an explicit lock-granularity choice.
Traffic / scale
10 threads adding comments on one post in the test. Production might split by post.
Latency
The wait is the RLock. Reads copy under that same lock.
Consistency
The list order is the creation order. Snapshots do not alias the store.
Availability
A missing post releases the lock and raises. It does not leave the counter bumped.
Failure assumptions
- Threads share one service.
- A later design splits locks per post and forgets the global counter.
Constraints
- Do not sort by timestamp in list_comments.
- Do not return parent.replies.
Prompt
Prove concurrent ids, snapshots, and post isolation for the two-level comment service, and say how you would split the lock later.
API
What does the caller observe if they mutate a thread dict?
Data
Which fields move together under the lock?
Architecture
When is one RLock enough, and what still needs care if you lock per post?
Lock granularity
Prefer
One RLock for the service
The global counters and both indexes move together. The concurrent test is easy to trust.
- Same-thread helpers can re-enter.
- Id allocation cannot race the append.
- Correct before it is clever.
Alternative
A lock per post
Less contention across posts, and a new bug if comment_seq stays global and unlocked.
- Worth it after a profile, not before.
- A sharded sequence means ids are not globally unique.
- Cross-post indexes still need a rule.
Overview
Deep dive on the three properties interviewers probe after the happy path: unique monotonic IDs under threads, defensive snapshots, and per-post isolation. Includes failure paths, a decision chart for locking granularity, and follow-up questions.
Concurrent ID allocation
Under an RLock, comment_seq += 1 and append happen together. The concurrent test starts 10 threads x 50 comments and asserts len(ids) == len(set(ids)) and ids == sorted(ids) in list order.
Allocate an id and publish it together
Diagram 1. The failure path releases the lock on a missing post and does not bump the counter.
- 1
Enter add_comment
The thread is about to touch the shared counters and the post list. - 2
Acquire the RLock
Re-entrant so a helper can lock again on the same thread. - 3
Post exists?
The map lookup stays inside the lock. - 4
POST_NOT_FOUND
Raise and release. comment_seq does not move. - 5
Bump, append, snapshot
comment_seq, the post list, and the id index update together. The return value is a new CommentView. - 6
Release and return
The next thread can enter. The caller cannot see a half-published comment.
Decisions
- 1
1. Thread enters add_comment
- next2. Acquire RLock
- 2
2. Acquire RLock
- next3. Validate post exists
- 3
3. Validate post exists
- next4. OK?
- ?
4. OK?
- No5. Failure: POST_NOT_FOUND then release
- Yes6. comment_seq plus 1
- 5
5. Failure: POST_NOT_FOUND then release
- 6
6. comment_seq plus 1
- next7. Append to post list and by_id
- 7
7. Append to post list and by_id
- next8. Build CommentView snapshot
- 8
8. Build CommentView snapshot
- next9. Release RLock and return
- 9
9. Release RLock and return
Lesson map
Comments and Replies - Concurrent IDs, Snapshots & Post Isolation
Concurrent ID allocation, defensive snapshots, post isolation, lock granularity decision chart, follow-ups.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["1. Thread enters add_comment"] b["2. Acquire RLock"] c["3. Validate post exists"] d["4. OK?"] e["5. Failure: POST_NOT_FOUND then release"] f["6. comment_seq plus 1"] g["7. Append to post list and by_id"] h["8. Build CommentView snapshot"] i["9. Release RLock and return"] a -->|continues| b b -->|continues| c c -->|continues| d d -->|No| e d -->|Yes| f f -->|continues| g g -->|continues| h h -->|continues| i
Decisions
- 1
Lock granularity?
- Simple and correctOne RLock for service
- Less contentionLock per post_id
- 2
One RLock for service
- 3
Lock per post_id
- nextCross-post indexes?
- ?
Cross-post indexes?
- comment_id globalStill need care on global seq
- Shard seq per postIDs not globally unique
- 5
Still need care on global seq
- 6
IDs not globally unique
Snapshots
list_*builds new frozen dataclasses.get_threadusescopy.deepcopyso nested reply lists are detached.- Never return
parent.repliesdirectly.
Post isolation
Comments for p1 never appear in list_comments("p2"). Reply inherits post_id from parent so relationship checks stay consistent.
Concepts used, learn more
Read the underlying idea on its own study page. This lesson applies it. It does not replace those pages.
- Low-Level Design Under Time — Interfaces, State & Tradeoffs
- Mutexes, Condition Variables, Deadlocks & Happens-Before
- Mutex vs RWLock
- Atomics vs Locks
- Atomics, Memory Ordering & Lock-Free — Races, Barriers & When Locks Win
Interview follow-ups
- Switch to per-post locks; keep a striped lock map keyed by post_id.
- Add edit/delete with soft-delete and tombstones while preserving order gaps.
- Persist to SQLite with UNIQUE and transactions (bridge to the URL shortener store patterns).
Interview Q&A
What is the lock chart in one sentence?
Answer
One RLock is the simple correct service. Per-post locks cut contention only if you still protect a global id, or you accept ids that are unique per post and not globally.
How would edit and delete land later?
Answer
Soft-delete with a tombstone so creation order and id gaps stay stable. Do not compact the list on the read path in a way that renumbers history.
Where is the SQLite version of this?
Answer
Not in this series. The URL shortener store is the pattern for UNIQUE and transactions if you persist comments later. The in-memory invariants come first.
What does the concurrent test start?
Answer
10 threads, 50 comments each, on one post. It asserts an empty error list, 500 comments, unique ids, and ids equal to sorted(ids) in list order.
Why copy post_id onto the reply?
Answer
list and relationship checks should not depend on a later lookup that might race. The parent post is fixed when the reply is created.
Which study should you open before replacing the RLock?
Answer
Mutexes for the critical section, mutex versus RWLock if reads dominate and you have measured, and atomics only for a single variable. This update touches a counter and two collections.
Pitfalls
- Using
threading.Lockincorrectly across re-entrant helpers (prefer RLock if methods call each other). - Sorting by timestamp in
list_commentsafter append-ordered inserts.
Go Deeper
Related
The series pager also walks these pages.