Stateful Systems
What is this?
Give someone the wrong structure and a simple operation becomes a scan. Give them the right one and it becomes a pop. These three problems are chosen because in each, the obvious structure makes one operation quadratic and a different structure makes every operation constant — and the gap between them is entirely about matching shape to access pattern.
A snake game is the clearest case. The body is a list, so checking whether the head hit the body is a scan of that list — until you keep a set alongside it and the check becomes one lookup.
💡 Fun fact: Design Twitter's feed is a k-way merge, not a sort. Pulling the ten most recent tweets from everyone a user follows does not require collecting and sorting all their tweets — put each followee's most recent tweet in a heap, pop the newest, and push that person's next one. The heap never exceeds the number of followees, and you stop after ten pops. This is the same machinery as merging sorted lists, which is worth recognising because it means the design scales to users who follow thousands of accounts.
🔓 The 3 problems in this chapter are free. Sign in with Google or Microsoft to start solving.
The one-line idea: pick the structure whose natural cheap operation is the one the system performs most — a heap when you repeatedly need the best, paired stacks when edits happen at a moving point, a deque when only the ends change — and add a parallel index when one structure cannot answer everything.
1. Two stacks around a cursor
Represent abc|def as a left stack holding a, b, c and a right stack holding f, e, d (top is d):
Every operation touches only the tops of the two stacks, so all are O(1) per character moved. The position that an array makes expensive — the middle — is exactly where this structure is cheapest, because the cursor is the boundary between the two stacks.
2. A parallel index
The snake's body is a deque: it grows at the head and shrinks at the tail, which is exactly what a deque does cheaply. But "did the head hit the body?" is a membership question, and a deque answers that in O(n). Keep a set with the same contents:
body = deque([(0, 0)])
occupied = {(0, 0)}
def move(direction):
head = next_cell(body[0], direction)
if not eating:
tail = body.pop(); occupied.discard(tail) # remove BEFORE the check
if head in occupied or out_of_bounds(head):
return -1 # game over
body.appendleft(head); occupied.add(head)
The ordering is the subtlety: the tail must be removed before testing the head, because moving into the cell the tail is vacating is legal. Testing first reports a false collision on exactly the input designed to catch it.
3. Bounded merging
For the feed, each user's tweets are stored newest-first. Push the head of each followee's list into a heap keyed by tweet id (which encodes recency), pop ten times, and refill from the popped tweet's own list after each pop.
4. A 30-second worked example (the tail-vacate case)
The snake is allowed to follow its own tail, and this ordering is the only thing that permits it.
5. Where you'll actually meet this
- Social feeds. Timeline generation by merging ranked sources is exactly the Twitter design, at scale.
- Text editors and terminals. The gap buffer used by real editors is the two-stacks idea with one array.
- Undo/redo. A pair of stacks, with the same "move an item across" mechanic as the cursor.
- Game state. Any moving body with ends-only mutation plus fast collision checks.
- Rate limiters and sliding windows. A deque of timestamps with a parallel aggregate.
6. Problems in this chapter
▶ Design Twitter
postTweet, getNewsFeed, follow, unfollow. Per-user tweet lists plus a k-way merge over followees for the ten most recent.
Pattern: heap-based bounded merge. Target: O(k log k) per feed for k followees.
▶ Design a Text Editor
Cursor movement, insertion and deletion at the caret. Two stacks meeting at the cursor.
Pattern: paired stacks. Target: O(1) per character moved or edited.
▶ Design Snake Game
Move the snake, grow on food, detect collisions. A deque for the body plus a set for O(1) membership.
Pattern: deque with a parallel index. Target: O(1) per move.
7. Common pitfalls 🚫
- Checking the head before removing the tail. The snake may legally move into the cell its tail is leaving.
- Scanning the body for collisions.
O(n)per move; the parallel set exists precisely to avoid it. - Letting the set and the deque drift apart. Every mutation must touch both, or the index lies.
- Sorting all tweets for the feed. A heap bounded by the number of followees is the point.
- Forgetting that a user sees their own tweets, and that following yourself must not duplicate them.
- Using a string or array for the editor. Insertion in the middle is
O(n)and the whole exercise is avoiding that. - Returning more characters than exist —
cursorLeftand the "last ten characters" query must clamp to what is available.
8. Key takeaways
- Match the structure to the operation performed most. That is the entire design step.
- A parallel index fixes what one structure cannot answer — a set beside a deque for membership.
- Keep the index in sync on every mutation, without exception.
- Two stacks make a cursor free, which is why real editors use a gap buffer.
- Merging beats sorting when you only need the top few from many sources.
- Ordering within an operation matters. Remove the tail before testing the head.
- Why interviewers like it: these are miniature system designs. The follow-ups — what if a user follows a million people, what if the document is enormous — probe whether your choice was deliberate.
Order: Design Twitter → Design a Text Editor → Design Snake Game.
Design Twitter
Core idea: Give every tweet a global, ever-increasing timestamp as it's posted, and store each user's tweets as an append-only list of
(timestamp, tweetId). A news feed is then just the 10 most recent items merged across a handful of sorted lists — the user's own tweets plus each followee's. Don't dump everything into one big pile and sort; k-way merge the lists with a max-heap keyed on timestamp, seeded by the latest tweet from each source, and pop 10 times.
Problem, rephrased
You're building the in-memory core of a tiny social network: users post short messages, follow each other, and open the app to a feed of the latest posts from the people they care about. You're given operations that identify everything by integer ids — a user is a userId, a tweet is a tweetId (every tweetId passed in is unique) — and there is no wall clock: "most recent" is defined purely by the order tweets were posted.
Implement the Twitter class. postTweet(userId, tweetId) publishes a new tweet by userId. follow(followerId, followeeId) makes followerId start following followeeId; unfollow(followerId, followeeId) undoes it. getNewsFeed(userId) returns the 10 most recent tweet ids visible to userId, newest first — where "visible" means posted by userId themself or by anyone userId follows.
Strip the story away and this is Design Twitter (LeetCode 355): support post, follow, and unfollow, plus a feed query that merges the caller's own and their followees' tweets by recency and returns the top 10.
Here is the canonical sequence (LeetCode 355's Example 1), as a table of operations:
| Call | Returns | Why |
|---|---|---|
postTweet(1, 5) |
— | user 1 posts tweet 5 |
getNewsFeed(1) |
[5] |
user 1 sees only their own tweet |
follow(1, 2) |
— | user 1 follows user 2 |
postTweet(2, 6) |
— | user 2 posts tweet 6 |
getNewsFeed(1) |
[6, 5] |
tweet 6 is newer than 5, so it comes first |
unfollow(1, 2) |
— | user 1 unfollows user 2 |
getNewsFeed(1) |
[5] |
user 1 no longer sees user 2's tweet 6 |
Notice the feed is always self-inclusive (user 1 sees tweet 5 even before following anyone) and ordered strictly by recency (6 before 5).
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson