Q5 - The Household (Sharing One Subscription)
We believe a variation of this interview question has recently been reported by Netflix candidates. It is a small design that lives right where the product does: one subscription is tied to one household, and a household holds only so many people. The interviewer is watching how you pin down the rules, what counts as a member, what happens at the limit, how you keep the design honest as it grows, more than any clever algorithm.
Scenario: A Netflix subscription is shared by a single household of up to capacity members (for a real plan, think five). You are designing the small service that manages who is in the household and who is allowed to stream. Members are identified by an id, a profile or a device.
Part 1. Implement Household(capacity) with add(member), remove(member), canStream(member), and size(). Adding is rejected once the household is full, and a member is never counted twice.
Example 1 (Part 1: a capped membership set)
Household(3) (a household of up to 3 members)
add("amy") -> added
add("ben") -> added
add("cara") -> added
add("dan") -> household full (already at 3 members)
add("amy") -> already a member (no duplicates)
canStream("ben") -> true
canStream("dan") -> false (never joined)
remove("cara") -> removed
add("dan") -> added (a slot opened up)
💡 It is a size-capped set, and the rules are the point. The data structure is just a set of members with a maximum size, so the value is in nailing the semantics cleanly: adding a duplicate is a no-op you report, adding past the cap is refused, and only members may stream. Getting those three cases right, and saying them out loud before you code, is most of what this question is checking.
Part 1: A Capped Membership Set
Pick the state, get the two add rules right, and everything else is one line.
Step 1: Model the household as a capped set
One field carries the whole thing: a set of the current members, alongside the fixed capacity. The count, the membership check, and "who may stream" all read straight off that set.
Household(3): members = { } capacity = 3
after add amy, ben: members = { amy, ben }
Step 2: add has two different failure modes
Adding can fail two ways, and they are not the same. The member might already be in (a harmless no-op you report), or the household might be at capacity (a real rejection). Check the duplicate first, then the cap; otherwise insert.
members = { amy, ben, cara }, capacity = 3
add("amy") -> already in the set -> "already a member" (nothing changes)
add("dan") -> not in, but size 3 == 3 -> "household full" (nothing changes)
(after remove one) add("dan") -> room now -> "added"
def add(self, member):
if member in self.members:
# a duplicate is a no-op, not an error
return "already a member"
if len(self.members) >= self.capacity:
# at the cap: refuse
return "household full"
self.members.add(member)
return "added"
Step 3: Everything else reads off the set
remove reports whether the member was there, canStream is a membership test, and size is the count. Each is a single line, because the state was chosen well.
canStream("ben") = ("ben" in members) = true
remove("cara") = ("cara" was in members) ? "removed" : "not a member"
size() = number of members
# every method O(1) time | O(capacity) space
class Household:
def __init__(self, capacity):
self.capacity = capacity
# the members currently sharing the subscription
self.members = set()
def add(self, member):
if member in self.members:
# a duplicate is a no-op
return "already a member"
if len(self.members) >= self.capacity:
# at the cap: refuse
return "household full"
self.members.add(member)
return "added"
def remove(self, member):
if member not in self.members:
return "not a member"
self.members.remove(member)
return "removed"
def can_stream(self, member):
# only members of the household may stream
return member in self.members
def size(self):
return len(self.members)
Part 2: Keep the Most-Active Members
Real households are not static: people drift off, old devices linger, and a new member should not be turned away just because a long-idle one is holding a slot. So make the slots activity-aware. Track when each member last watched, and when a new member joins a full household, keep the capacity most-recently-active and evict the stalest one.
Example 2 (Part 2: keep the active members)
Household(3), now activity-aware
add("amy", 1) -> None joined; room to spare, nobody evicted
add("ben", 2) -> None
add("cara", 3) -> None full at 3
watch("amy", 4) -> ok amy's last-watched time is now 4
watch("ben", 5) -> ok
add("dan", 6) -> "cara" full, so the stalest member (cara, last active @3) is evicted
members() -> ["amy", "ben", "dan"]
The state changes from a plain set to a map from each member to their last-active time (set when they join, refreshed when they watch). Then add on a full household drops the member with the smallest last-active time, the one who has been away longest, and lets the newcomer in.
Step 1: Remember when each member last watched
Swap the set for a map last_active from member to a timestamp. Joining records the current time; that is also the tie-breaker later when deciding who is stalest.
after add amy@1, ben@2, cara@3:
last_active = { amy: 1, ben: 2, cara: 3 }
Step 2: watch refreshes activity
When a member streams, bump their last-active time to now. A member who keeps watching keeps their slot; one who stops slowly becomes the stalest.
watch("amy", 4) -> last_active = { amy: 4, ben: 2, cara: 3 }
watch("ben", 5) -> last_active = { amy: 4, ben: 5, cara: 3 }
now cara (@3) is the least-recently-active
Step 3: A full add evicts the stalest
If the household is full, find the member with the smallest last-active time (ties broken by member id, so the result is deterministic), remove them, and admit the newcomer. Return whoever was evicted so the caller can sign them out.
add("dan", 6), full:
stalest = argmin last_active = cara (@3) -> evict cara
last_active = { amy: 4, ben: 5, dan: 6 } -> return "cara"
if len(self.last_active) >= self.capacity:
# keep the most-recently-active members; drop the stalest (ties by member id)
evicted = min(self.last_active, key=lambda m: (self.last_active[m], m))
del self.last_active[evicted]
Watch a household of three fill up, refresh as members watch, then make room for a newcomer by dropping whoever has been away longest:
# add / watch / remove: O(capacity) time | O(capacity) space
class Household:
def __init__(self, capacity):
self.capacity = capacity
# member -> the time they last watched (or joined)
self.last_active = {}
def watch(self, member, time):
if member not in self.last_active:
return "not a member"
self.last_active[member] = time
return "ok"
def add(self, member, time):
if member in self.last_active:
# already in: just refresh their activity
self.last_active[member] = time
return None
evicted = None
if len(self.last_active) >= self.capacity:
# keep the most-recently-active members; drop the stalest (ties by member id)
evicted = min(self.last_active, key=lambda m: (self.last_active[m], m))
del self.last_active[evicted]
self.last_active[member] = time
# who was evicted, or None if there was room
return evicted
def remove(self, member):
if member not in self.last_active:
return "not a member"
del self.last_active[member]
return "removed"
def members(self):
return sorted(self.last_active)
Complexity Analysis
- Part 1 is
O(1)per operation.add,remove,canStream, andsizeare single hash-set operations. Space isO(capacity), the members themselves. - Part 2's
addisO(capacity), the restO(1). Finding the stalest member scans the map once, which is a handful of entries for a real household (five).watch,remove, and a membership check are single hash-map operations. Space staysO(capacity). - Why the linear scan is fine, and when it would not be: because a household is small, a scan for the minimum last-active time is a few comparisons, simpler and faster in practice than any heap. If the "household" were instead thousands of members, you would keep them in a min-heap or an ordered structure by last-active time to make eviction
O(log n), calling that out is the forward-looking note the interviewer wants. - Determinism. Breaking ties in the eviction by member id means the same inputs always produce the same result, which is what makes the behavior testable.
Final Notes
🏠 We believe "the household" problem, and small membership or access-control designs like it, have appeared frequently in recent interviews for software engineers at Netflix. There is no hard algorithm here; the interviewer is watching whether you turn a fuzzy product rule into a crisp set of behaviors, and whether your edge cases (duplicate, at-capacity, not-a-member) are deliberate rather than accidental.
- Clarify before you code. What is a member, a profile or a device? What happens at the limit, reject or evict? Can someone be in two households? Asking these, and stating the answers you are assuming, is the actual content of this round; the code is the easy part.
- Pick the state, then the methods write themselves. "A set of members plus a cap" makes Part 1 fall out in one line each. Naming the state first is the habit that turns an open design prompt into a short, clean class.
- Name the two failure modes of
add. Already-a-member and household-full are different outcomes for different reasons; collapsing them into one "no" hides a decision. Reporting each precisely is the kind of care that reads as considered design. - Let activity choose who stays. Keeping the most-recently-active members and dropping the stalest is the honest version of "one household," it reclaims an abandoned device instead of punishing a new one. That "last-active timestamp, evict the minimum" move is the whole of Part 2.
- Say what changes at scale. A linear scan is right for five members and wrong for fifty thousand. Calling out the heap you would reach for if the cap grew shows you know the trade-off you are making, not just the one line you wrote.