Keep a hash map from event ID to the time its accepted copy arrived, plus a queue of IDs in arrival order. On each event, pop everything older than the window from the queue's front and delete it from the map; the event is a duplicate if its ID is still in the map. O(1) amortized per event.
mediumPattern: Sliding windowFrom Devana's Netflix question set
Problem
Playback clients retry uploads when they do not hear back, so the same event can arrive several times. Downstream counts must not double count, but the stream runs at a million events a second, so you cannot remember every ID forever.
Write a class EventDeduplicator(window_seconds) with is_duplicate(event_id, now). It returns True when the same ID was accepted within the last window_seconds, and otherwise accepts the event and returns False. State the guarantee the window gives.
class EventDeduplicator:
def __init__(self, window_seconds: int = 900): ...
def is_duplicate(self, event_id: str, now: float) -> bool: ...This is one of Devana's own questions, written in the style of Netflix's interviews. It is not a question leaked from Netflix, and Devana is not affiliated with Netflix.
Constraints
nownever decreases from one call to the next.- A copy arriving at most
window_secondsafter the accepted one is a duplicate; the boundary is inclusive. - A duplicate does not extend the window, which runs from the accepted copy.
- After the window, the next copy is accepted again and starts a new window.
- Memory is bounded by the events inside the window, and dropping a genuine event is worse than letting an occasional duplicate through.
Examples
Example 1
Input: window_seconds = 900
is_duplicate("e1", 0), is_duplicate("e1", 10)
Output: False, TrueThe retry 10 seconds later is inside the 15-minute window.
Example 2
Input: window_seconds = 900
is_duplicate("e1", 0), is_duplicate("e1", 7200)
Output: False, FalseTwo hours later the first copy has left the window, so the second is accepted. That is the guarantee to state: duplicates are caught within 15 minutes, not forever.
Hints
Open one at a time, and try the problem again after each.
Hint 1
Membership questions want a hash set or map. What goes wrong if that is all you keep?
Hint 2
Entries expire in time order, and events arrive in time order. Which structure gives you the oldest entry cheaply?
Hint 3
Pair the map with a queue of (time, id). Before answering, pop entries older than now - window_seconds from the front and delete them from the map.
Approach: from brute force to optimal
Brute force: remember everything, or scan
A set of every ID ever seen answers instantly but grows without bound; at a million events a second it fills memory within hours. Scanning a list of recent events instead bounds memory but costs time proportional to the window on every event.
Complexity: O(1) per event with unbounded memory, or O(w) per event for w events in the window
A map plus a queue: a time-based window
The map answers "seen recently?" in O(1). The queue holds accepted events oldest first, so expiry only ever touches its front. Every accepted event enters the queue once and leaves once, so the eviction work averages out to O(1) per event.
Complexity: O(1) amortized time per event; O(w) space for the w events accepted within the window
Fitting it in memory
Do the arithmetic: a million events a second for 900 seconds is 900 million IDs, and an 8 GB budget leaves about 9 bytes per entry, less than a typical hash map spends on overhead alone. So say how you would cut it: shard by event ID across machines, store fixed-size hashes instead of ID strings, or keep one set per minute and drop a whole set when it ages out.
A Bloom filter is the tempting answer, but its errors are false positives, and a false positive here drops a genuine event, the error the requirements rank as worse.
How to say it in the interview
Lead with the guarantee, because the question asks for it: "copies within 15 minutes of the first are dropped; later ones are let through." Then the structure, a map for membership and a queue for expiry, both O(1) amortized. Then do the memory arithmetic out loud, since it is what pushes the design toward sharding.
Solution
Python
from collections import deque
class EventDeduplicator:
def __init__(self, window_seconds=900):
self.window = window_seconds
self.accepted = {} # event id -> time its accepted copy arrived
self.order = deque() # (time, event id), oldest first
def is_duplicate(self, event_id, now):
# Forget accepted events that have left the window.
while self.order and self.order[0][0] < now - self.window:
_, old_id = self.order.popleft()
del self.accepted[old_id]
if event_id in self.accepted:
return True
self.accepted[event_id] = now
self.order.append((now, event_id))
return FalseJavaScript
class EventDeduplicator {
constructor(windowSeconds = 900) {
this.window = windowSeconds;
this.accepted = new Map(); // event id -> time its accepted copy arrived
this.order = []; // [time, event id], oldest first, from this.head
this.head = 0;
}
isDuplicate(eventId, now) {
// Forget accepted events that have left the window.
while (this.head < this.order.length && this.order[this.head][0] < now - this.window) {
this.accepted.delete(this.order[this.head][1]);
this.head++;
}
// Drop the consumed prefix now and then, so the array stays bounded.
if (this.head > 1024 && this.head * 2 > this.order.length) {
this.order = this.order.slice(this.head);
this.head = 0;
}
if (this.accepted.has(eventId)) return true;
this.accepted.set(eventId, now);
this.order.push([now, eventId]);
return false;
}
}Both versions are tested against the same cases, including every example on this page.
- The eviction test uses
<, so an entry exactlywindow_secondsold is kept and the boundary is inclusive, as specified. - JavaScript has no built-in deque, and
shift()on an array is O(n), so the code keeps a head index and compacts occasionally. - A duplicate never enters the queue, so it cannot extend the window.
Follow-up questions
Would you use a Bloom filter here? What is the risk given the cost asymmetry?
A Bloom filter never misses a real duplicate but sometimes reports a new event as seen, which drops a genuine event. At this volume even a 0.01% false positive rate loses about 8.6 million events a day, so if memory forces it, size the filter for a much lower rate and say what it costs.
How do you expire entries as the window slides?
From the front of the queue, in arrival order. A coarser alternative keeps one set per minute and drops whole sets as they age out, which is cheaper still.
How does this behave across many ingestion servers?
Retries can land on a different server, so route by a hash of the event ID: every copy of an event reaches the same deduplicator. Rebalancing then loses at most one window of history for the IDs that moved.
What if events can arrive with timestamps out of order?
Run the window on the server's arrival time, which never goes backwards, rather than on the client's timestamp, and keep the client's timestamp in the event itself.
Common mistakes
- A set of every ID ever seen, which grows until the process runs out of memory.
- Using
list.pop(0)orArray.shift()for the queue, which makes every eviction O(n). - Refreshing the timestamp on every duplicate, so a client that keeps retrying holds the window open indefinitely.
- Never stating the window. "Deduplicates the stream" without a time bound is a promise the design cannot keep.
Practice it in a mock interview
Reading a solution and explaining one under questioning are different skills. The button below starts this problem as a voice mock interview with Josh, Devana's AI interviewer, with an editor that runs your code.
Practice this problem in a mock interview
Related problems
- Time-Weighted Moving Average over Irregular Ticks: A time-based window with a running area, evicting from the front as ticks age out.
- Deduplicate a Member's Skills: Normalize each entry to a lookup key, map it through a taxonomy, keep first appearances.
- Sliding window: the pattern guide, with more practice questions.