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.
→ heap over each source's head"] A --> C["edit at a moving point
→ two stacks meeting at the cursor"] A --> D["move a body, ends only
→ deque + a parallel set"] B --> E["k-way merge, bounded by followees"] C --> F["every edit is O(1)"] D --> G["collision check O(1) instead of O(length)"]
💡 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):
addText("X") push X onto left → abcX|def
deleteText(2) pop 2 from left → ab|def
cursorLeft(1) pop from left,
push onto right → a|bdef
cursorRight(3) pop from right,
push onto left three times
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)
snake body: (0,0) (0,1) (0,2) head at (0,0), tail at (0,2)
move down then right then up ... eventually the head targets (0,2)
WRONG order: head (0,2) in occupied? yes → game over ✗
RIGHT order: pop tail (0,2), discard from set
head (0,2) in occupied? no → legal move ✓
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
Build a stripped-down social feed. There are users (just integer ids), tweets (also integer ids), a "who follows whom" graph, and one read query: show me the latest few posts from everyone I care about.
You implement a Twitter class with four operations:
postTweet(userId, tweetId)— useruserIdpublishes a new tweettweetId. EverytweetIdpassed in is unique.getNewsFeed(userId)— return the 10 most recent tweet ids visible touserId, newest first. "Visible" means: tweets posted byuserIdthemself or by anyoneuserIdfollows.follow(followerId, followeeId)—followerIdstarts followingfolloweeId.unfollow(followerId, followeeId)—followerIdstops followingfolloweeId.
The whole difficulty lives in getNewsFeed. There is no wall-clock time; "most recent" is defined purely by the order tweets were posted. So the design needs a way to compare any two tweets by recency — which is what a single monotonically increasing counter, stamped onto each tweet at post time, gives you.
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