Q5 - The Household (Sharing One Subscription)

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)

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:

An empty household with three slots; each slot will remember a member and when they last watched
1 / 7
# 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, and size are single hash-set operations. Space is O(capacity), the members themselves.
  • Part 2's add is O(capacity), the rest O(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 stays O(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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Was this page helpful?