Netflix Python Developer Interview Questions & Answers

netflix icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: September 3, 2026)

21. Implement a TTL cache.CodingMediumNetflix

Question Details

Implement an in-memory cache where every key expires after a time-to-live duration, and define how expired entries are detected and removed.

Short Interview Answer (30-60 seconds)

I would use a dictionary for the current record of each key and a min-heap for expiration records. Each heap item stores the expiration time, version, and key. Before every put or get, I pop records whose expiration time is less than or equal to now. A version check prevents an old heap record from deleting a newer value. Dictionary operations are O(1) on average, heap operations are O(log h), and auxiliary space is O(k + h).

Detailed Explanation

See the Code while reading this explanation.

The cache stores values in memory and makes each value unavailable after a fixed TTL. Scanning every key on each operation would be wasteful. Instead, the solution uses a dictionary for direct key lookup and a min-heap for expiration order. A version number makes overwrites safe when an older expiration record is still inside the heap.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Implement a TTL cache. diagram
How to Explain It in an Interview
1. Define the input and output

The constructor receives ttl_seconds.

put(key, value, now) stores the value under the key. Its expiration time is now + ttl_seconds.

get(key, now) returns the stored value when the current record has not expired. It returns None when the key is missing or expired.

A record is expired when expires_at <= now.

2. Choose the data structures

The dictionary maps each key to:

(value, expires_at, version)

It stores the newest record for that key.

The min-heap stores:

(expires_at, version, key)

Python heapq is a min-heap. Therefore, the record with the smallest expiration time is always at the top.

The version number identifies one specific write. It prevents an old heap record from deleting a newer value written under the same key.

3. Initialize and maintain the invariant

The dictionary starts empty. The heap starts empty. next_version starts at zero.

Before every put and get, the cache runs _evict_expired(now).

After this cleanup, every record remaining in the dictionary has expires_at greater than now.

The heap may still contain stale records. A popped record may delete a dictionary entry only when both its expiration time and version match the current dictionary record.

4. Walk through the exact example

The TTL is 5 seconds.

At t=0, put A=100.

The cache creates version 1 and calculates expires_at = 0 + 5 = 5. The dictionary becomes A -> (100, 5, 1). The heap becomes [(5, 1, A)].

At t=1, put B=200.

The cache creates version 2 and calculates expires_at = 1 + 5 = 6. The dictionary contains A -> (100, 5, 1) and B -> (200, 6, 2). The heap contains (5, 1, A) and (6, 2, B).

At t=3, put A=150.

No record has expired. The cache creates version 3 and calculates expires_at = 3 + 5 = 8. The dictionary replaces A with A -> (150, 8, 3). The heap receives (8, 3, A). The older record (5, 1, A) remains in the heap, but it is stale.

At t=5, get A.

Cleanup pops (5, 1, A). The current dictionary record for A is (150, 8, 3). The expiration time and version do not match, so the popped record is stale and cannot delete A. The cache returns 150. The dictionary remains unchanged. The heap contains (6, 2, B) and (8, 3, A).

At t=6, get B.

Cleanup pops (6, 2, B). It matches the current dictionary record for B, so B is deleted. The following lookup does not find B, so get returns None. The dictionary contains only A -> (150, 8, 3). The heap contains (8, 3, A).

At t=9, get A.

Cleanup pops (8, 3, A). It matches the current dictionary record for A, so A is deleted. The following lookup does not find A, so get returns None.

The returned values are [150, None, None]. The final dictionary is empty, and the final heap is empty.

5. Explain why the solution is correct

The heap always exposes the next possible expiration in time order.

Cleanup removes every heap record whose expiration time is less than or equal to now.

A matching expiration time and version proves that the popped record still represents the current dictionary record. It is therefore safe to delete that key.

A mismatch proves that the heap record belongs to an older write. Ignoring it prevents a stale record from deleting a newer value.

After cleanup, every dictionary record is valid at the supplied time. Therefore, get returns a value only when that value has not expired.

6. Explain the Python implementation

The constructor validates the TTL and creates the dictionary, heap, and version counter.

_evict_expired repeatedly examines the heap top. It pops every record with expires_at <= now. It deletes a key only when the popped expiration time and version match the current dictionary record.

put runs cleanup first. It creates a new version, calculates the expiration time, updates the dictionary, and pushes one heap record.

get also runs cleanup first. It then returns the value from the current dictionary record or None when the key is absent.

7. Explain complexity and edge cases

Let h be the number of heap records before cleanup. Let r be the number of expired or stale records popped during one operation.

A put takes O(r log h + log h) time for cleanup and the new heap push, plus O(1) average dictionary work.

A get takes O(r log h) cleanup time plus O(1) average dictionary lookup.

Each heap record is pushed once and popped at most once. Cleanup work is therefore amortized across the sequence of operations.

Auxiliary space is O(k + h), where k is the number of current dictionary records. Repeated overwrites can make h larger than k because stale heap records remain until they reach the top.

A negative TTL is rejected. A TTL of zero makes an entry expire at the same timestamp. The next operation at that time or later removes it.

Key Insight / Why This Solution Works

The key insight is to separate direct lookup from expiration order. The dictionary provides O(1) average access to the newest record for a key. The min-heap exposes the next possible expiration without scanning all dictionary entries. The central invariant is that after _evict_expired(now), every dictionary record has expires_at greater than now. Version numbers make lazy cleanup safe. A heap record can delete a key only when its expires_at and version still match the current dictionary record. Any mismatch means the heap record belongs to an older overwrite and must be ignored.

Code
from __future__ import annotations

import heapq
from typing import Any, Optional


class TTLCache:
    def __init__(self, ttl_seconds: int) -> None:
        # Reject a TTL that would expire entries before they are written.
        if ttl_seconds < 0:
            raise ValueError("ttl_seconds must be non-negative")

        # Every write expires after this fixed number of seconds.
        self.ttl_seconds = ttl_seconds

        # key -> (value, expires_at, version)
        # The dictionary stores the newest record for each key.
        self.store: dict[str, tuple[Any, int, int]] = {}

        # Each heap record is (expires_at, version, key).
        # heapq keeps the smallest expires_at at index 0.
        self.expiry_heap: list[tuple[int, int, str]] = []

        # Each write receives a unique increasing version.
        self.next_version = 0

    def _evict_expired(self, now: int) -> None:
        # Pop every record whose expiration time has arrived.
        while self.expiry_heap and self.expiry_heap[0][0] <= now:
            expires_at, version, key = heapq.heappop(self.expiry_heap)

            # The key may already have been removed.
            current = self.store.get(key)
            if current is None:
                continue

            _, current_expires_at, current_version = current

            # Delete only when the popped heap record still represents
            # the current dictionary record for this key.
            if current_expires_at == expires_at and current_version == version:
                del self.store[key]

    def put(self, key: str, value: Any, now: int) -> None:
        # Remove expired current records and stale heap records
        # that have reached the top.
        self._evict_expired(now)

        # Give this write a new version.
        self.next_version += 1
        version = self.next_version

        # Calculate the exact expiration time.
        expires_at = now + self.ttl_seconds

        # Store the newest record for the key.
        self.store[key] = (value, expires_at, version)

        # Add its expiration record to the min-heap.
        heapq.heappush(
            self.expiry_heap,
            (expires_at, version, key),
        )

    def get(self, key: str, now: int) -> Optional[Any]:
        # Remove records that are expired at this time.
        self._evict_expired(now)

        # Read the newest remaining record for the key.
        current = self.store.get(key)
        if current is None:
            return None

        # The first tuple item is the cached value.
        return current[0]


if __name__ == "__main__":
    cache = TTLCache(ttl_seconds=5)

    # Exact example from the diagram.
    cache.put("A", 100, now=0)
    cache.put("B", 200, now=1)
    cache.put("A", 150, now=3)

    results = [
        cache.get("A", now=5),
        cache.get("B", now=6),
        cache.get("A", now=9),
    ]

    print(results)  # [150, None, None]
    print(cache.store)  # {}
    print(cache.expiry_heap)  # []
Time & Space Complexity

Let h be the number of heap records before cleanup, and let r be the number of expired or stale records popped during the operation. A put takes O(r log h + log h) time for cleanup and the new heap push, plus O(1) average dictionary work. A get takes O(r log h) time for cleanup, plus O(1) average dictionary lookup. Each heap record is pushed once and popped at most once, so cleanup work is amortized across many operations. Auxiliary space is O(k + h), where k is the number of current dictionary records. Repeated overwrites can make h larger than k.

Where it is used

This pattern is useful for in-memory caches, temporary authentication tokens, sessions, rate-limit state, deduplication records, and other data that becomes invalid after a fixed time. The dictionary supports fast access by key. The min-heap supports ordered lazy expiration without scanning every stored key during each operation.

Why Interviewers Ask This

This question tests whether you can combine data structures with different strengths. The interviewer is checking whether you choose fast dictionary lookup, use a min-heap for expiration order, handle overwrites without deleting newer values, define the expiration boundary correctly, maintain a clear invariant, and explain amortized heap cleanup accurately. It also tests whether your Python code and complexity claims match the behavior you describe.

Common interview mistakes

One mistake is deleting a key whenever any old heap record expires. That can remove a newer value written under the same key. Compare both expires_at and version before deleting. Another mistake is running cleanup only during get. put must also clean expired records before writing. A third mistake is using expires_at < now instead of expires_at <= now. In this design, an entry is expired exactly at its expiration timestamp. Candidates may also claim every operation is O(1), even though heap pushes and pops cost O(log h). Finally, repeated overwrites can leave stale records in the heap, so space can grow beyond the number of current keys.

Interview tip

Say the invariant before coding: after cleanup at time now, every dictionary record expires after now, and a popped heap record may delete a key only when both its expiration time and version match the current record.

Interviewer may ask next
How would you reduce stale heap records after many overwrites?

The current solution uses lazy cleanup, so old heap records remain until they reach the top. I could rebuild the heap when its size becomes much larger than the number of current dictionary records. I would create one heap record for each current dictionary entry and call heapq.heapify. Rebuilding takes O(k) time and O(k) temporary or replacement space for k current keys. The tradeoff is an occasional O(k) pause in exchange for lower long-term heap memory.

How would the design change if each put could have a different TTL?

I would pass ttl_seconds to put instead of using one shared TTL from the constructor. Each write would calculate expires_at = now + ttl_seconds and store that expiration time in both the dictionary and heap. The version check and cleanup logic would remain unchanged, so correctness is preserved. The time complexity would still be O(r log h + log h) for put and O(r log h) plus O(1) average lookup for get. Space would remain O(k + h).

22. Compute earliest completion times for all tasks.CodingHardNetflix

Question Details

Given positive task durations and prerequisite relationships, return the earliest completion time for every task while detecting invalid dependency cycles.

Short Interview Answer (30-60 seconds)

I would model the prerequisites as a directed graph and use Kahn’s topological sort. I keep an indegree count for each task and a deque of tasks with no remaining prerequisites. For every task, I also track the largest finish time among its prerequisites. When its indegree becomes zero, I add its duration to that value. If I process all tasks, I return the finish times. Otherwise, there is a cycle. The time is O(n + m), with O(n + m) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem gives task durations and directed prerequisite relationships. We must return the earliest finish time for every task. Tasks may run in parallel, so a task waits only for its slowest prerequisite. Kahn’s topological sort fits because it processes a task only after all of its prerequisites are known. A dynamic programming array stores the largest prerequisite finish time for each task.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Compute earliest completion times for all tasks. diagram
How to Explain It in an Interview
1. Understand the graph and required output

Each task is a node in a directed graph. An edge pre -> task means pre must finish before task can start.

The input contains:

  • n, the number of tasks
  • durations, where durations[i] is the time needed by task i
  • prerequisites, where each pair is (pre, task)

The output is an array named finish. The value finish[i] is the earliest time when task i can finish.

For the example:

  • n = 5
  • durations = [3, 2, 4, 2, 1]
  • prerequisites = [(0, 2), (1, 2), (1, 3), (2, 4), (3, 4)]

The result is [3, 2, 7, 4, 8].

2. Choose topological sorting and the required state

I use Kahn’s topological sort. It uses an indegree count. The indegree of a task is the number of prerequisites that have not yet been removed.

I store the graph as an adjacency list. graph[pre] contains the tasks that depend directly on pre.

I also use max_prereq_finish. The value max_prereq_finish[t] is the largest finish time seen among the processed prerequisites of task t.

The central invariant is this: when a task is removed from the queue, its earliest finish time is final. Also, when a task’s indegree becomes zero, all of its prerequisite finish times have been considered.

3. Initialize tasks with no prerequisites

The initial indegree array is [0, 0, 2, 1, 2].

Tasks 0 and 1 have indegree zero, so they can start at time zero. Their finish times are their own durations:

  • finish[0] = 3
  • finish[1] = 2

The initial queue is deque([0, 1]).

The initial finish array is [3, 2, 0, 0, 0].

The initial max_prereq_finish array is [0, 0, 0, 0, 0].

4. Walk through the example

First, remove task 0 from the queue. Task 0 finishes at time 3 and points to task 2.

Update:

  • max_prereq_finish[2] = 3
  • indegree[2] changes from 2 to 1

Task 2 still has one remaining prerequisite, so it is not added to the queue.

The queue becomes [1]. The finish array remains [3, 2, 0, 0, 0].

Next, remove task 1. It points to tasks 2 and 3.

For task 2:

  • max_prereq_finish[2] = max(3, 2) = 3
  • indegree[2] changes from 1 to 0

All prerequisites of task 2 are now complete. Its earliest start time is 3. Its duration is 4, so:

  • finish[2] = 3 + 4 = 7

Task 2 is added to the queue.

For task 3:

  • max_prereq_finish[3] = 2
  • indegree[3] changes from 1 to 0

Task 3 has duration 2, so:

  • finish[3] = 2 + 2 = 4

Task 3 is added to the queue.

The queue becomes [2, 3]. The finish array becomes [3, 2, 7, 4, 0].

Next, remove task 2. It points to task 4.

Update:

  • max_prereq_finish[4] = 7
  • indegree[4] changes from 2 to 1

Task 4 still waits for task 3. The queue becomes [3]. The finish array remains [3, 2, 7, 4, 0].

Next, remove task 3. It also points to task 4.

Update:

  • max_prereq_finish[4] = max(7, 4) = 7
  • indegree[4] changes from 1 to 0

Task 4 can now start. Its earliest start time is 7, and its duration is 1:

  • finish[4] = 7 + 1 = 8

Task 4 is added to the queue. The queue becomes [4]. The finish array becomes [3, 2, 7, 4, 8].

Finally, remove task 4. It has no outgoing edges. The queue becomes empty.

The final finish array is [3, 2, 7, 4, 8].

5. Explain cycle detection and correctness

A task enters the queue only after every incoming edge has been removed. This means every prerequisite has already been processed.

Because prerequisites may run in parallel, the task does not wait for the sum of their finish times. It waits for the largest one. That largest value is the earliest legal start time for the task.

After the queue becomes empty, I compare the number of processed tasks with n. In this example, all 5 tasks were processed, so the graph is acyclic. If fewer than n tasks were processed, a cycle would be blocking the remaining tasks.

6. Explain the Python implementation and complexity

The code first builds the adjacency list and indegree array. It then places every indegree-zero task in a deque and sets its finish time to its duration.

The main loop removes one ready task at a time. For each dependent task, it updates the largest prerequisite finish time and decreases the indegree. When the indegree reaches zero, it calculates the dependent task’s finish time and adds it to the queue.

Each task is added to and removed from the queue once. Each prerequisite edge is processed once. Therefore, the time complexity is O(n + m), where n is the number of tasks and m is the number of prerequisite edges. The adjacency list, arrays, and queue use O(n + m) auxiliary space.

Key Insight / Why This Solution Works

The key idea is to combine Kahn’s topological sort with dynamic programming on a directed graph. Topological sorting makes a task ready only after all of its prerequisites have been processed. For each task, max_prereq_finish[task] stores the largest finish time among its processed prerequisites. This value is the task’s earliest legal start time because prerequisites may run in parallel. When the task’s indegree becomes zero, its finish time is max_prereq_finish[task] + durations[task]. The invariant is that every task removed from the queue already has its final earliest finish time.

Code
from collections import deque
from typing import List, Tuple


def earliest_completion_times(
    n: int,
    durations: List[int],
    prerequisites: List[Tuple[int, int]],
) -> List[int]:
    """Return the earliest completion time for every task.

    Each prerequisite pair is written as (pre, task), which means
    task `pre` must finish before `task` can start.

    Raises:
        ValueError: If the dependency graph contains a cycle.
    """

    # Step 1: Build the directed adjacency list.
    # graph[pre] contains every task that directly depends on pre.
    graph: List[List[int]] = [[] for _ in range(n)]

    # indegree[task] is the number of unfinished prerequisites.
    indegree = [0] * n

    # max_prereq_finish[task] stores the largest finish time
    # among the prerequisites processed so far.
    max_prereq_finish = [0] * n

    for pre, task in prerequisites:
        graph[pre].append(task)
        indegree[task] += 1

    # Step 2: Store the earliest finish time for every task.
    finish = [0] * n

    # Step 3: Start with every task that has no prerequisites.
    queue = deque()

    for task in range(n):
        if indegree[task] == 0:
            # A task with no prerequisites starts at time 0.
            finish[task] = durations[task]
            queue.append(task)

    processed = 0

    # Step 4: Process tasks in topological order.
    while queue:
        task = queue.popleft()
        processed += 1

        # Step 5: Update every task that depends on the current task.
        for nxt in graph[task]:
            max_prereq_finish[nxt] = max(
                max_prereq_finish[nxt],
                finish[task],
            )
            indegree[nxt] -= 1

            # Step 6: When all prerequisites are complete,
            # calculate the dependent task's earliest finish time.
            if indegree[nxt] == 0:
                finish[nxt] = max_prereq_finish[nxt] + durations[nxt]
                queue.append(nxt)

    # Step 7: Fewer processed tasks means a cycle blocked progress.
    if processed != n:
        raise ValueError("Invalid dependency cycle")

    return finish


if __name__ == "__main__":
    task_count = 5
    task_durations = [3, 2, 4, 2, 1]
    task_prerequisites = [
        (0, 2),
        (1, 2),
        (1, 3),
        (2, 4),
        (3, 4),
    ]

    result = earliest_completion_times(
        task_count,
        task_durations,
        task_prerequisites,
    )

    print(result)  # [3, 2, 7, 4, 8]
Time & Space Complexity

Let n be the number of tasks and m be the number of prerequisite edges. Building the graph takes O(n + m) time. During the topological traversal, each task is placed in the queue once, removed once, and each edge is examined once. The total time is O(n + m). The adjacency list uses O(n + m) memory. The indegree, finish, maximum-prerequisite-finish arrays, and queue use O(n) memory. Therefore, the auxiliary space is O(n + m).

Where it is used

This pattern is useful in project scheduling, build systems, workflow engines, course prerequisite planning, data pipelines, and job orchestration. It applies when work items have durations, dependencies form a directed acyclic graph, and independent tasks may run in parallel.

Why Interviewers Ask This

This question tests whether the candidate can recognize a directed dependency graph, choose topological sorting, and combine it with a dynamic programming state. The interviewer also wants to see whether the candidate understands parallel prerequisites and uses a maximum instead of a sum. Other important signals are correct edge direction, careful indegree updates, cycle detection, readable Python, and an accurate O(n + m) time and space analysis.

Common interview mistakes

A common mistake is to add all prerequisite finish times. That is wrong because prerequisites can run in parallel. The task waits for the maximum finish time, not the sum. Another mistake is calculating a task’s finish time before its indegree reaches zero. At that point, some prerequisite may still be missing. Candidates may also reverse the edge direction, forget to initialize indegree-zero tasks with their own durations, or return partial results without checking whether processed == n. It is also incorrect to claim O(n) time while ignoring the prerequisite edges.

Interview tip

State the invariant before coding: when a task enters the queue, every prerequisite has finished, and max_prereq_finish already contains its earliest legal start time. This makes the update rule and the cycle check easy to explain.

Interviewer may ask next
How would you also return one critical prerequisite path that determines each task’s completion time?

Store another array such as parent. When max_prereq_finish[nxt] increases because of finish[task], set parent[nxt] = task. After the topological traversal, follow the parent links backward from a chosen task to reconstruct one prerequisite chain that produced its earliest finish time. The topological processing and correctness rule stay the same. The time remains O(n + m). The graph and arrays still use O(n + m) space, with an additional O(n) parent array. When two prerequisites have the same finish time, either one may be chosen unless a tie rule is required.

What changes if a new prerequisite edge is added after the finish times have already been calculated?

The current result may no longer be valid because the new edge can change indegrees, create a longer prerequisite chain, or introduce a cycle. The simple and safe approach is to rebuild the indegree array and run the same O(n + m) algorithm again. Correctness is preserved because every task is reconsidered using the complete updated graph. The time is O(n + m), and the auxiliary space is O(n + m). The tradeoff is that recomputing everything is simple but may be expensive when updates are very frequent.

23. Compute subtree sums with tree DFS.CodingHardNetflix

Question Details

Given a rooted tree and an integer value for each node, compute the sum of values in every node's subtree using depth-first search.

Short Interview Answer (30-60 seconds)

I would use post-order depth-first search. For each node, I start with its own value. I recursively compute each child’s subtree sum and add the returned value to the current total. After every child is finished, I store the total for the node and return it to the parent. This works because each parent uses complete child results. The time complexity is O(n). The recursion stack uses O(h) auxiliary space, where h is the tree height.

Detailed Explanation

See the Code while reading this explanation.

The input contains a root node, a children adjacency list, and one integer value for every node. We must return a dictionary that maps each node to the sum of its own value and every descendant value. Post-order DFS fits this problem because a parent’s result depends on the completed results of its children.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Compute subtree sums with tree DFS. diagram
How to Explain It in an Interview
1. Understand the input and required output

The root tells us where traversal begins.

The children dictionary stores each node and its direct children. The edges are directed from parent to child.

The values dictionary stores the integer value of each node.

The output is a dictionary that maps each node to its complete subtree sum.

In the example, the edges are 1 to 2, 1 to 3, 2 to 4, 2 to 5, and 3 to 6. The node values are 1 to 5, 2 to 2, 3 to -3, 4 to 4, 5 to 1, and 6 to 6.

2. Choose post-order DFS

I use recursive depth-first search.

Each call solves one node’s complete subtree. The call returns one integer, which is that subtree’s sum.

The central invariant is: when dfs(node) returns, it has computed and stored the correct sum for node and every descendant in that node’s subtree.

The children must finish before their parent. This processing order is called post-order traversal.

3. Initialize the state

I create an empty dictionary named sums. It stores node to completed subtree sum.

Inside dfs(node), I set total to values[node]. This first includes the current node’s own value.

Then I visit every child. Each child returns its complete subtree sum, which I add to total.

A leaf has no children. Its loop does not run, so it stores and returns its own value.

4. Walk through the example

Start with dfs(1). Node 1 begins with total 5 and visits node 2.

Node 2 begins with total 2 and visits node 4.

Node 4 is a leaf. It stores sums[4] = 4 and returns 4.

Back at node 2, total changes from 2 to 2 + 4 = 6. Node 2 then visits node 5.

Node 5 is a leaf. It stores sums[5] = 1 and returns 1.

Node 2 now computes 6 + 1 = 7. It stores sums[2] = 7 and returns 7.

Back at node 1, total changes from 5 to 5 + 7 = 12. Node 1 then visits node 3.

Node 3 begins with total -3 and visits node 6.

Node 6 is a leaf. It stores sums[6] = 6 and returns 6.

Node 3 computes -3 + 6 = 3. It stores sums[3] = 3 and returns 3.

Finally, node 1 computes 12 + 3 = 15. It stores sums[1] = 15 and returns 15.

The post-order return order is 4, 5, 2, 6, 3, 1.

The final mapping is {1: 15, 2: 7, 3: 3, 4: 4, 5: 1, 6: 6}.

5. Explain why the result is correct

Every child call returns the exact sum of that child’s subtree.

The parent starts with its own value and adds the returned sum from every child.

Therefore, after all children finish, total contains the parent’s value and every descendant value exactly once.

Storing sums[node] after the children finish gives the correct answer for that node.

6. Explain the Python implementation

The outer function creates the sums dictionary.

The inner dfs function computes one subtree. It starts total with the current node’s value.

It reads the current node’s children with children.get(node, []). This also works when a leaf is missing from the dictionary.

For each child, it calls dfs(child) and adds the returned subtree sum to total.

After the loop, the subtree is complete. The function stores sums[node] = total and returns total to the parent.

The outer function calls dfs(root) and returns the completed sums dictionary.

7. Explain complexity and edge cases

Let n be the number of nodes and h be the tree height.

The time complexity is O(n). Each node is visited once, and each parent-to-child edge is processed once.

The recursion stack uses O(h) auxiliary space. A balanced tree has a smaller height. A skewed tree can have h equal to n.

The output dictionary uses O(n) space because it stores one result for every node.

Important edge cases are a leaf node, a single-node tree, negative or zero values, and a skewed tree with deep recursion.

Key Insight / Why This Solution Works

The key idea is to compute child subtree sums before computing the parent’s sum. Each recursive DFS call returns one integer: the complete subtree sum for that node. The invariant is that when dfs(node) returns, sums[node] has already been stored and equals values[node] plus the completed sums returned by all children. Post-order DFS is suitable because the parent cannot be finished until all child results are available.

Code
from typing import Dict, List


def compute_subtree_sums(
    root: int,
    children: Dict[int, List[int]],
    values: Dict[int, int],
) -> Dict[int, int]:
    # Store the completed subtree sum for every node.
    sums: Dict[int, int] = {}

    def dfs(node: int) -> int:
        # Begin with the current node's own value.
        total = values[node]

        # Process every child before finishing the current node.
        for child in children.get(node, []):
            # The recursive call returns the child's complete subtree sum.
            total += dfs(child)

        # All child subtrees are complete, so this total is final.
        sums[node] = total

        # Return the completed subtree sum to the parent.
        return total

    # Start the post-order DFS from the root.
    dfs(root)

    # Return one subtree sum for every node.
    return sums


if __name__ == "__main__":
    root = 1

    children = {
        1: [2, 3],
        2: [4, 5],
        3: [6],
        4: [],
        5: [],
        6: [],
    }

    values = {
        1: 5,
        2: 2,
        3: -3,
        4: 4,
        5: 1,
        6: 6,
    }

    result = compute_subtree_sums(root, children, values)

    # Display the mapping in node order, matching the diagram.
    ordered_result = {node: result[node] for node in sorted(result)}
    print(ordered_result)
    # Expected: {1: 15, 2: 7, 3: 3, 4: 4, 5: 1, 6: 6}
Time & Space Complexity

Let n be the number of nodes and h be the height of the tree. The time complexity is O(n) because every node is visited once and every parent-to-child edge is processed once. The recursion stack uses O(h) auxiliary space because the active calls form one root-to-leaf path. In a skewed tree, h can equal n. The output dictionary uses O(n) space because it stores one subtree sum for each node.

Where it is used

This pattern is useful when a parent’s result depends on completed results from its children. Examples include calculating folder sizes, organization totals, category totals, expression-tree values, and other tree dynamic programming problems.

Why Interviewers Ask This

This question tests whether you recognize post-order tree processing. The interviewer wants to see whether you can define what each recursive call returns, combine child results correctly, and maintain a clear invariant. It also checks whether you can represent a rooted tree with an adjacency list, handle leaves and negative values, write clean recursive Python, and explain O(n) time and O(h) recursion-stack space accurately.

Common interview mistakes

A common mistake is storing sums[node] before visiting the children. That stores an incomplete total. Another mistake is forgetting to return total to the parent. Candidates may forget that a leaf still stores and returns its own value. They may incorrectly use one shared total across all calls instead of one local total per call. They may also claim O(1) auxiliary space and forget the O(h) recursion stack.

Interview tip

State the invariant before writing code: when dfs(node) returns, it has stored and returned the correct sum for that complete subtree. This explains both the recursion order and why sums[node] is stored after the child loop.

Interviewer may ask next
How would you handle a tree so deep that recursive DFS may exceed Python’s recursion limit?

I would simulate post-order traversal with an explicit stack. I could store pairs such as (node, processed). On the first visit, I push the node again as processed and then push its children. On the processed visit, every child sum is already available, so I calculate and store the node’s sum. The time remains O(n). The explicit stack can use O(n) space in the worst case, and the output dictionary uses O(n). The tradeoff is more code, but it avoids recursion-limit errors.

How would the solution change if the tree edges were undirected?

I would pass the parent into each DFS call or maintain a visited set. While visiting a node’s neighbors, I would skip the parent so the traversal does not move back along the same edge. The subtree calculation remains the same because the chosen root defines the parent-child direction. The time complexity stays O(n). The recursion stack uses O(h), and the output dictionary uses O(n).

24. Implement a versioned key-value store.CodingHardNetflix

Question Details

Implement an in-memory store supporting put(key, value, timestamp) and retrieval of the value for a key at a requested timestamp.

Short Interview Answer (30-60 seconds)

I would keep two structures for each key. One stores its timestamps in sorted order. The other maps each timestamp to its value. On put, I use bisect_left to insert the timestamp in the correct place or update an existing version. On get, I use bisect_right to find the newest timestamp that is less than or equal to the query time. Put is O(m) in the worst case, get is O(log m), and total auxiliary space is O(V).

Detailed Explanation

See the Code while reading this explanation.

This problem asks us to store several versions of the same key and return the newest version that existed at a requested time. Timestamps may arrive out of order. I keep the timestamps sorted for each key and use binary search to find the correct historical version.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Implement a versioned key-value store. diagram
How to Explain It in an Interview
1. Understand the input and required output

The store supports two operations.

put(key, value, timestamp) saves a value for one key at one timestamp.

get(key, timestamp) returns the value stored at the latest timestamp that is less than or equal to the requested timestamp.

If the key does not exist, or if every stored timestamp is later than the requested time, get returns an empty string.

2. Choose the data structures

I use two dictionaries.

timestamps[key] stores a sorted list of timestamps for that key.

values[key] stores another dictionary. It maps each timestamp to the value written at that time.

The central invariant is that timestamps[key] always stays sorted. Also, values[key][t] always stores the exact value written at timestamp t.

3. Process a put operation

When a key appears for the first time, I create an empty timestamp list and an empty timestamp-to-value dictionary.

I use bisect_left to find the position where the timestamp belongs.

If that timestamp already exists, I update its value. I do not insert a duplicate timestamp.

If it does not exist, I insert it into the sorted list and store the value in the dictionary.

4. Walk through the verified example

The first operation is put("volume", "low", 5).

The key does not exist yet. I create its structures. bisect_left([], 5) returns 0. I insert timestamp 5 and store "low".

The state becomes timestamps["volume"] = [5] and values["volume"] = {5: "low"}.

The second operation is put("volume", "high", 1).

The current timestamp list is [5]. bisect_left([5], 1) returns 0. I insert timestamp 1 before 5 and store "high".

The state becomes timestamps["volume"] = [1, 5]. The value map contains 1: "high" and 5: "low".

The third operation is put("volume", "mute", 8).

The current list is [1, 5]. bisect_left([1, 5], 8) returns 2. I insert timestamp 8 at the end and store "mute".

The timestamp list becomes [1, 5, 8].

The fourth operation is get("volume", 6).

bisect_right([1, 5, 8], 6) returns 2. This is the insertion position after all timestamps that are less than or equal to 6.

I subtract 1, so the predecessor index is 1. The timestamp at index 1 is 5. Timestamp 5 maps to "low".

The method returns "low".

5. Explain why the result is correct

For every key, its timestamp list is always sorted.

bisect_right returns the position after all timestamps that are less than or equal to the requested time.

The timestamp immediately before that position is therefore the latest valid version.

For query time 6, timestamp 8 is too new. Timestamp 5 is the latest valid timestamp, so the correct returned value is "low".

6. Explain the Python implementation

The put method uses bisect_left to find the exact sorted position of a timestamp. It checks whether the timestamp already exists. If it does, the method updates the stored value. Otherwise, it inserts the timestamp into the list.

The get method uses bisect_right and moves one position left. That gives the newest timestamp that does not exceed the query time.

7. Explain complexity and edge cases

Let m be the number of versions stored for one key.

Finding the position during put takes O(log m). Inserting into a Python list can shift up to m elements, so put takes O(m) in the worst case.

The get operation takes O(log m).

Python dictionary lookup and update are O(1) on average.

If V is the total number of stored versions across all keys, the auxiliary space is O(V).

Important edge cases are a missing key, a query before the first timestamp, an exact timestamp match, and a repeated put for the same key and timestamp.

Key Insight / Why This Solution Works

The key insight is to separate timestamp ordering from value storage. For each key, a sorted list keeps all version timestamps in order. A second dictionary maps each timestamp to its exact value. The invariant is that timestamps[key] is always sorted and values[key][t] always contains the value written at time t. Because of this invariant, bisect_right can find the position after every timestamp that is less than or equal to the query. The predecessor is exactly the newest valid version.

Code
from bisect import bisect_left, bisect_right
from typing import Dict, List


class VersionedKVStore:
    def __init__(self) -> None:
        # For each key, keep all version timestamps in sorted order.
        self.timestamps: Dict[str, List[int]] = {}

        # For each key, map a timestamp to the value stored at that time.
        self.values: Dict[str, Dict[int, str]] = {}

    def put(self, key: str, value: str, timestamp: int) -> None:
        # Create empty storage when the key appears for the first time.
        if key not in self.timestamps:
            self.timestamps[key] = []
            self.values[key] = {}

        times = self.timestamps[key]

        # Find where this timestamp belongs in sorted order.
        idx = bisect_left(times, timestamp)

        # Update the value when the exact timestamp already exists.
        if idx < len(times) and times[idx] == timestamp:
            self.values[key][timestamp] = value
        else:
            # Insert a new timestamp and store its value.
            times.insert(idx, timestamp)
            self.values[key][timestamp] = value

    def get(self, key: str, timestamp: int) -> str:
        # A missing key has no valid version.
        if key not in self.timestamps:
            return ""

        times = self.timestamps[key]

        # Find the newest timestamp that is less than or equal to the query.
        idx = bisect_right(times, timestamp) - 1

        # The query is earlier than the first stored timestamp.
        if idx < 0:
            return ""

        # Return the value stored at the selected timestamp.
        return self.values[key][times[idx]]


if __name__ == "__main__":
    store = VersionedKVStore()

    store.put("volume", "low", 5)
    store.put("volume", "high", 1)
    store.put("volume", "mute", 8)

    result = store.get("volume", 6)
    print(result)  # low
Time & Space Complexity

Let m be the number of versions stored for one key. bisect_left finds an insertion position in O(log m) time. However, inserting into a Python list may shift up to m elements, so put takes O(m) in the worst case. If the timestamp already exists, updating the dictionary value is O(1) on average. get uses bisect_right and takes O(log m). Python dictionary lookup is O(1) on average. If V is the total number of stored versions across all keys, the auxiliary space is O(V).

Where it is used

This pattern is useful for configuration history, feature flags, document revisions, account settings, price history, and other systems that must answer, "What value was active at this time?" It is especially useful when timestamps can arrive out of order but historical reads still need fast lookup.

Why Interviewers Ask This

This question tests whether you can design a small data structure with both write and historical-read behavior. The interviewer wants to see whether you can maintain sorted state, choose the correct binary-search operation, handle timestamps that arrive out of order, update duplicate timestamps, and reason about edge cases. It also checks whether you understand the difference between O(log m) binary search and O(m) Python list insertion.

Common interview mistakes

A common mistake is appending timestamps without keeping them sorted. Binary search would then return the wrong position. Another mistake is using the result of bisect_right without subtracting one. Some candidates insert the same timestamp twice instead of updating its existing value. Others forget to return an empty string when the key is missing or when the query is earlier than the first timestamp. It is also incorrect to claim that put is O(log m), because inserting into a Python list can take O(m).

Interview tip

State the invariant before writing code: each key has a sorted timestamp list, and each stored timestamp maps to exactly one value. Then explain that bisect_right minus one returns the newest valid version.

Interviewer may ask next
How would the design change if timestamps were guaranteed to arrive in increasing order for each key?

The put method could append each new timestamp instead of using bisect_left and inserting into the middle of the list. The increasing-order guarantee preserves the invariant. A new put would take O(1) amortized time, while get would remain O(log m) with bisect_right. The auxiliary space would remain O(V). The tradeoff is that this faster write depends on the ordered-timestamp guarantee.

How would you improve write performance if one key had millions of versions and timestamps arrived out of order?

A Python list is expensive because inserting into the middle takes O(m). I would use an ordered structure that supports both insertion and predecessor search, such as a balanced search tree. Then put and get could both take O(log m), while space would remain O(V). Correctness is preserved because the structure still keeps timestamps ordered and still returns the greatest timestamp that does not exceed the query. The tradeoff is greater implementation complexity.

25. Design the interfaces for an ad frequency-capping service.API DesignHardNetflix

Question Details

Define the request and response contract used by an ad server to decide whether a candidate ad may be shown under user, campaign, line-item, creative, or category caps.

Short Interview Answer (30-60 seconds)

At a high level, I would use one synchronous check before showing an ad. The Ad Server sends POST /v1/frequency-cap/check to the Frequency-Capping Service using mTLS and a JWT. The request contains the user, candidate ad, timestamp, and cap scopes. The service reads active rules and exposure counters, then returns a 200 OK eligibility decision with matched limits and remaining capacity. If the ad is shown, an impression event updates counters asynchronously. This keeps decisions fast, but recent impressions may take a short time to appear in the counters.

Detailed Explanation

The API decides whether one candidate ad may be shown to one user. The main challenge is checking several cap scopes quickly while counting only real impressions. I would explain the synchronous decision first, then the asynchronous update path shown in the diagram.

Useful Questions to Ask the Interviewer
  1. Which clients and core use cases must the API support?
  2. What authentication, authorization, and data-validation rules should I assume?
  3. What scale, error handling, idempotency, and versioning requirements matter?
Design the interfaces for an ad frequency-capping service. diagram
How to Explain It in an Interview
1. Define the API boundary

I would begin by saying that the Ad Server is the caller. The Frequency-Capping Service owns the eligibility decision.

The Ad Server calls POST /v1/frequency-cap/check. The connection uses mTLS and a JWT. mTLS encrypts the connection and verifies both services. The JWT carries the caller identity used for the service request.

The check happens before the candidate ad is shown. The later impression update is a separate flow.

2. Build the request contract

The request contains requestId, userKey, and timestamp. It also contains the candidate ad identifiers.

The candidateAd object includes campaignId, lineItemId, creativeId, and categories[]. These values let the service check each relevant advertising level.

The request also contains scopesToCheck. The shown scopes are user, campaign, line item, creative, and category. This field tells the service which caps to evaluate.

The Frequency-Capping Service first validates the request. It then loads the cap definitions and reads the matching counters.

3. Read rules and exposure counters

The service reads active rules from the Cap Rules Store. Example rules include ten impressions per user each day and four per campaign each day. Other examples include two per line item each hour, one per creative each day, and twenty per category each week.

The service also reads the Exposure Counter Store. Each counter represents a user, scope, entity, and time window. For example, u123+campaign42+2026-07-26 = 3 means that user has three counted exposures for that campaign during the daily window.

The service compares each current count with its matching limit. It evaluates the requested scopes before creating one final decision.

4. Return the eligibility decision

The Frequency-Capping Service sends a separate response back to the Ad Server. The diagram shows a 200 OK decision response.

The response contains eligible, which is either true or false. It also contains blockedScopes[] when one or more caps block the ad.

The matchedCaps[] list explains the evaluated caps. Each item contains the scope, entity ID, limit, current count, remaining capacity, and window. The response also includes a reasonCode and windowEndsAt.

This contract gives the Ad Server both the result and its reason.

5. Record the decision

The Frequency-Capping Service sends a decision log event to Decision Log / Analytics. This is a supporting flow, not part of the business response.

The log records the request ID, decision, blocked scopes, latency, and timestamp. It supports monitoring and analysis. It does not decide whether the ad is eligible.

6. Update counters after a real impression

The eligibility check does not increase exposure counters. A successful check does not prove that the ad was shown.

Only after the ad is shown does the Ad Server publish an impression event to the Impression Event Stream. The Counter Updater consumes that event.

The Counter Updater then increments the user, campaign, line-item, creative, and category counters in the Exposure Counter Store. This asynchronous path keeps the synchronous decision call fast.

7. Explain failure behavior and trade-offs

If the check times out or returns an error, the Ad Server applies its configured fallback policy. A fail-open policy may show the ad. A fail-closed policy blocks it.

Fail-open protects ad delivery but may exceed a cap. Fail-closed protects cap enforcement but may block an eligible ad.

The asynchronous update path also creates a small delay. Two close requests may read the same old count before the newest impression event is processed. The benefit is lower decision latency and simpler request handling.

Practical Complexity & Trade-offs

The benefit is a small and clear decision API. One request carries the user, candidate ad, timestamp, and cap scopes. One response explains the result and the matching limits. mTLS and JWT protect the service call, but they add certificate and token management work. Reading rules and counters during each check gives a useful decision, but it adds storage latency. Updating counters through events keeps the check fast and counts only shown ads. The downside is temporary counter delay. Two requests may see the same count before the newest impression is processed. The fallback policy also needs a business choice. Fail-open protects delivery but may break a cap. Fail-closed protects the cap but may block a valid ad.

Why Interviewers Ask This

The interviewer wants to see whether the candidate can define a clear API boundary and model the request and response correctly. They also test whether the candidate separates eligibility checks from real impression updates. Strong answers explain security, counter ownership, logging, failure behavior, and asynchronous consistency. The key skill is engineering judgment. The candidate should explain why each field and component exists, then describe the latency and accuracy trade-offs without claiming perfect consistency.

Interviewer may ask next
How would this design handle a sudden traffic spike?

I would keep the same POST /v1/frequency-cap/check contract and add more Frequency-Capping Service instances. Each instance would validate the request, read active cap rules, read the required counters, and return the same response contract.

The Cap Rules Store and Exposure Counter Store would need enough read capacity for the higher request rate. The synchronous path would still avoid counter writes.

The asynchronous path would remain the same. The Impression Event Stream would buffer bursts of shown-ad events. More Counter Updater workers could consume those events in parallel and update the Exposure Counter Store.

Correctness still depends on using the same user, scope, entity, and time-window counter keys. Decision logs should continue recording latency and blocked scopes so operators can detect overload.

The main downside is greater temporary counter lag during a burst. Checks may read counters before every recent impression is applied. The API remains fast, but short-lived cap overshoot becomes more likely.

How would you choose between fail-open and fail-closed behavior?

I would keep fallback as a configured Ad Server policy because the diagram places that decision with the caller. The Frequency-Capping Service still owns every normal eligibility decision. The fallback is used only when the check times out or returns an error.

Fail-open means the Ad Server may show the candidate ad without a successful cap decision. This protects delivery. However, it may exceed a user, campaign, line-item, creative, or category cap.

Fail-closed means the Ad Server does not show the candidate ad. This protects cap enforcement and user experience. However, it may block an ad that was actually eligible.

The request contract, response contract, rule reads, counter reads, decision logs, and asynchronous impression updates remain unchanged.

The main downside is that neither policy avoids all harm. The business must choose whether lost delivery or a possible cap violation is more costly.

26. Design the interfaces for publisher configuration rules.API DesignHardNetflix

Question Details

Define how publishers create, update, retrieve, validate, version, and roll out publisher-specific rules across websites, mobile apps, channels, and ad inventory.

Short Interview Answer (30-60 seconds)

At a high level, I would separate rule authoring from runtime delivery. Publishers use the Publisher Configuration API to create, update, retrieve, validate, version, and roll out rules. The API stores drafts in the Rule Store, creates immutable versions in the Version Catalog, and sends checks to the Validation Service. The Rollout Manager activates an approved version through the Runtime Config Service. Websites, mobile apps, channels, and ad inventory then fetch their specific rule JSON. OAuth2 tokens and JWT-protected HTTPS secure authoring. The trade-off is safer releases with more operational complexity.

Detailed Explanation

The goal is to manage publisher-specific rules from editing through runtime delivery. The main challenge is preventing draft or invalid rules from becoming active. I would explain the design by following each request and response in the diagram.

Useful Questions to Ask the Interviewer
  1. Which clients and core use cases must the API support?
  2. What authentication, authorization, and data-validation rules should I assume?
  3. What scale, error handling, idempotency, and versioning requirements matter?
Design the interfaces for publisher configuration rules. diagram
How to Explain It in an Interview
1. Separate authoring from runtime delivery

I would first separate the system into two main areas.

The Publisher Control Plane handles authoring, validation, storage, versions, auditing, and rollouts. The Runtime Delivery area serves active rules to applications.

A Publisher Admin can manage rules manually. An API Client or CI Pipeline can automate rule changes. Both use the same Publisher Configuration API, so the system keeps one consistent authoring contract.

This separation protects runtime consumers from unfinished draft changes.

2. Authenticate the authoring clients

The request first goes through the Identity Provider.

The Publisher Admin uses login with OAuth2 and receives a token. OAuth2 is a standard process for issuing access tokens. The API Client or CI Pipeline uses client credentials and receives a service token.

The clients then call the Publisher Configuration API over HTTPS with a JWT. A JWT is a signed token that carries the caller identity. The diagram shows the API returning rule payloads, status, or version information to the requesting client.

The identity flow is separate from the business rule flow.

3. Define the publisher rule interfaces

The Publisher Configuration API is the main authoring entry point.

POST /publishers/{id}/rules creates a publisher-specific rule. PATCH /rules/{ruleId} updates an existing rule. GET /rules/{ruleId} retrieves one rule.

POST /rules/{ruleId}/validate validates the rule. GET /rules/{ruleId}/versions returns its versions. POST /rules/{ruleId}/rollouts starts a rollout.

The Publisher Admin uses these interfaces for direct changes. The API Client or CI Pipeline uses them for automation and bulk updates. The API returns the matching rule payload, status, or version information to the original caller.

4. Validate and store the draft

Before activation, the Publisher Configuration API sends the rule to the Validation Service.

The validation request contains the schema, targeting information, and scope. The Validation Service returns a pass result, warnings, or errors.

Errors mean the rule should not move toward activation. Warnings tell the publisher that review may be needed. A passing result allows version creation and rollout to continue.

The API also saves and reads editable drafts through the Rule Store. The Rule Store returns the requested draft rule to the API.

This lets publishers revise a draft without changing an active version.

5. Create immutable versions and audit changes

When a rule is ready, the Publisher Configuration API creates a version in the Version Catalog.

The Version Catalog stores immutable versions. Immutable means the stored version cannot be edited later. It also returns version history when the API requests it.

This provides a stable record of every approved rule state. A later rollout can therefore load an exact version instead of reading a changing draft.

The Publisher Configuration API also sends an audit event to the Audit Log. The Audit Log records the activity, but it does not produce the business response.

6. Roll out the approved version

The Publisher Configuration API sends a rollout request to the Rollout Manager.

The request includes the environment, selected targets, and schedule. The Rollout Manager asks the Version Catalog to load the approved version. The Version Catalog returns the version payload.

The Rollout Manager then sends an activation and rollout event to the Runtime Config Service. Validation therefore happens before version activation. Rollouts can also be staged for selected targets and environments.

The benefit is controlled releases. The downside is additional rollout state and coordination.

7. Serve rules to runtime targets

The Runtime Config Service serves only active configuration.

The Website sends GET rules and receives website rules JSON. The Mobile App sends GET rules and receives mobile rules JSON. The Channel App receives channel rules JSON. The Ad Inventory Service receives ad rules JSON.

Each request and response is a separate flow. The runtime target sends the request. The Runtime Config Service sends the matching JSON response back.

This allows each publisher target to receive a different active rule set while sharing one controlled management process.

Practical Complexity & Trade-offs

The benefit is strong separation between drafts, versions, rollouts, and active runtime rules. Publishers can edit a draft without changing production behavior. Validation catches bad schema, targeting, or scope before activation. Immutable versions make rollouts easier to review and trace. The Rollout Manager can release one approved version to selected targets and environments. JWT-protected HTTPS calls also protect the authoring interfaces. The downside is more operational work. The team must run the configuration API, validation service, stores, audit log, rollout manager, and runtime service. It must also keep rollout state and active versions consistent. We accept this complexity because configuration mistakes could affect websites, mobile apps, channels, and ad inventory at the same time.

Why Interviewers Ask This

Interviewers use this question to test API boundaries and engineering judgment. They want to see whether the candidate separates editable drafts from immutable versions and active runtime configuration. They also check request and response direction, HTTP method choices, token-based access, validation ownership, audit logging, and rollout control. A strong answer explains why each component exists and clearly states the safety-versus-complexity trade-off.

Interviewer may ask next
How would you roll out one rule gradually to only the mobile application?

I would keep the existing design and change only the rollout parameters sent through POST /rules/{ruleId}/rollouts. The Publisher Configuration API would still use the validated immutable version stored in the Version Catalog. The rollout request would select the Mobile App target, the required environment, the schedule, and the rollout percentage. The Rollout Manager would load that approved version from the Version Catalog and receive the version payload. It would then send the activation and rollout event to the Runtime Config Service for the selected mobile scope. The Website, Channel App, and Ad Inventory Service would keep their current active versions. The Mobile App would continue sending GET rules, and the Runtime Config Service would return the active mobile rules JSON for its rollout group. Security remains unchanged because authoring still uses OAuth2 tokens, HTTPS, and JWTs. The main downside is more rollout state. The Rollout Manager must track the selected target, percentage, schedule, environment, and active version.

What should happen when the Validation Service returns warnings or errors?

The Publisher Configuration API should return the validation result to the original Publisher Admin or API Client. The flow begins with POST /rules/{ruleId}/validate. The API sends the rule schema, targeting information, and scope to the Validation Service. The service returns pass, warnings, or errors. Errors should prevent that rule from moving into version activation or rollout. The publisher can update the editable draft through PATCH /rules/{ruleId} and validate it again. Warnings should be returned for review before the publisher continues. A passing result allows the normal version and rollout flow to proceed. The Rule Store still owns the editable draft. The Version Catalog still owns immutable versions. The Audit Log remains a side path for recording activity. Security also stays unchanged because the authoring request still uses HTTPS and a JWT. The downside is slower publishing, but the extra step reduces the risk of distributing invalid rules to runtime targets.

27. Design the interfaces for advertiser campaign intake.API DesignHardNetflix

Question Details

Define how advertisers create campaigns, ad groups, creatives, targeting settings, budgets, dates, pacing rules, and statuses, including validation and error behavior.

Short Interview Answer (30-60 seconds)

At a high level, I would use one Campaign Intake API for creating and updating advertiser campaigns. The console sends a POST or PATCH request over HTTPS with a JWT. The payload includes campaigns, ad groups, creatives, targeting, budgets, dates, pacing, and the desired status. The API validates the request, stores creative references, and writes normalized campaign records. It then requests creative review and scheduling. Success returns IDs and current statuses. Invalid or unauthorized requests return clear errors. The trade-off is extra workflow complexity for safer validation and lifecycle control.

Detailed Explanation

The goal is to accept a complete advertiser campaign through one clear interface. The main challenge is validating related resources while controlling review and status changes. I would explain the design by following the request and response paths shown in the diagram.

Useful Questions to Ask the Interviewer
  1. Which clients and core use cases must the API support?
  2. What authentication, authorization, and data-validation rules should I assume?
  3. What scale, error handling, idempotency, and versioning requirements matter?
Design the interfaces for advertiser campaign intake. diagram
How to Explain It in an Interview
1. Start with the advertiser request

I would begin with the advertiser using the Advertiser Console UI. The advertiser can create a campaign or edit an existing campaign.

The console first uses the Identity / Auth Service for login and account access. The service returns a JWT or session. A JWT is a signed token that identifies the logged-in advertiser.

The console then sends a POST or PATCH campaign payload over HTTPS with the JWT. HTTPS protects the request while it travels across the network.

The payload contains the campaign, ad groups, creatives, targeting, budgets, dates, pacing, and desired status. The diagram does not define a URL path. Therefore, I would describe this as a conceptual contract with the Campaign Intake API.

2. Validate the complete campaign

The Campaign Intake API sends the campaign data to the Validation & Policy Engine. This component checks both the data shape and the business rules.

Required fields must be present. The campaign needs at least one ad group and one creative. The start date must be earlier than the end date. The budget must be greater than zero.

Pacing must be DAILY or LIFETIME. Targeting values must be valid. Each creative needs a supported format and landing URL. The requested status transition must also be allowed.

The validation engine returns either an approved result or field errors. This prevents invalid campaign data from reaching storage and later workflows.

3. Store creative and campaign data

After validation succeeds, the API handles the main domain resources. These are Campaign, Ad Group, Creative, Targeting, and Budget & Schedule.

The API sends creative metadata and asset references to the Creative Asset Store. The store returns an asset ID after storage succeeds.

The API then inserts or updates normalized entities in the Campaign DB. Normalized means each resource type is stored separately instead of repeating the same data.

The database stores campaigns, ad_groups, creatives, targeting_rules, budget_schedule, and status_history. It returns IDs and persisted state to the Campaign Intake API.

4. Start review and scheduling

After persistence, the API sends a campaign.created or review requested event to the Review & Status Workflow.

This workflow owns creative review and scheduling. A campaign starts in DRAFT. It may move to READY or PENDING_REVIEW. A review or policy failure may move it to REJECTED, which is terminal.

An approved campaign may become ACTIVE. An ACTIVE campaign may move to PAUSED and later return to ACTIVE. The scheduler activates the campaign at the start date and ends it at the end date.

The workflow also emits status updates. The stored campaign data includes status_history so lifecycle changes can remain traceable.

5. Return success and error responses

The Campaign Intake API returns the business response to the Advertiser Console UI. A successful creation returns 201 Created. A successful update returns 200. The response contains IDs and current statuses.

A 400 response means fields are malformed or missing. A 401 response means authentication is missing or invalid. A 403 response means the advertiser is authenticated but not allowed.

A 409 response means the request conflicts with the current state or duplicates another submission. A 422 response means the request is readable, but a business rule failed.

These separate errors help the console show a useful message to the advertiser.

6. Record audit events and explain the trade-off

The Campaign Intake API also sends audit events to the Audit Log. The log records intake events and important system actions. It supports compliance and debugging. It does not own the business response.

The main benefit is one controlled entry point for complex campaign data. Validation, asset storage, persistence, review, and scheduling have clear owners.

The trade-off is additional coordination. A campaign may be stored before review finishes. Therefore, a successful intake response does not always mean the campaign is already active.

Practical Complexity & Trade-offs

The benefit is that one intake API gives advertisers a simple entry point. The API accepts the complete campaign structure and applies the same rules every time. HTTPS protects network traffic, while the JWT identifies the advertiser. Validation blocks invalid dates, budgets, pacing values, targeting, creatives, and status changes. Separate creative storage keeps asset handling apart from normalized campaign data. The Campaign DB keeps related entities and status history. The downside is more coordination between validation, asset storage, persistence, review, and scheduling. Review may finish after the original API response. Therefore, clients must use the returned status instead of assuming immediate activation. We accept this because controlled review and scheduled activation are safer than publishing every accepted request immediately.

Why Interviewers Ask This

Interviewers use this question to test API boundaries and engineering judgment. They want to see whether the candidate can model related campaign resources clearly. They also check request and response direction, authentication, validation ownership, persistence, review workflows, and status transitions. Strong answers distinguish invalid input, failed authentication, denied access, state conflicts, and business-rule failures. The interviewer also expects a clear trade-off between a simple advertiser experience and the workflow needed for safe activation.

Interviewer may ask next
How would the design handle a large increase in campaign submissions without changing the advertiser contract?

I would keep the advertiser-facing contract unchanged. The Advertiser Console UI would still send the POST or PATCH campaign payload to the Campaign Intake API over HTTPS with its JWT.

The existing components would keep their current responsibilities. The Validation & Policy Engine would still reject invalid fields and business rules. The Creative Asset Store would still receive creative metadata and asset references. The Campaign DB would still store normalized entities and return IDs and persisted state.

The Review & Status Workflow would continue handling review and scheduling after the campaign is stored. The API could return 201 for creation or 200 for an update without waiting for the campaign to become ACTIVE. The returned status may remain READY or PENDING_REVIEW while review continues.

Correctness is maintained through persisted campaign state, status history, and allowed status transitions. Conflicting or duplicate submissions still return 409. Semantic rule failures still return 422.

The main downside is longer review time during heavy load. Advertisers must follow status updates instead of expecting immediate activation.

How would you prevent an advertiser from making an invalid status change?

I would enforce every requested status change in the Validation & Policy Engine before the API persists it. The request still reaches the Campaign Intake API through HTTPS with the advertiser JWT.

The validation engine compares the desired status with the allowed lifecycle. A campaign can move from DRAFT to READY or PENDING_REVIEW. An approved campaign may become ACTIVE. An ACTIVE campaign may become PAUSED and later ACTIVE again. The scheduler moves the campaign to ENDED at the end date. A review or policy failure may move it to terminal REJECTED.

The Campaign Intake API does not accept an unsupported transition. It returns 422 when the requested change violates a business rule. It may return 409 when the request conflicts with the current stored state.

The Review & Status Workflow continues to own review-driven and scheduled changes. The Campaign DB keeps status_history for traceability.

The downside is stricter client behavior. The console must use the current status before requesting another change.

28. Design the interfaces for a scalable file backup system.API DesignHardNetflix

Question Details

Define operations for starting a backup, listing progress, retrying failed work, restoring files, and reporting changed, deleted, partial, or corrupted data.

Short Interview Answer (30-60 seconds)

At a high level, I would separate fast control requests from slower backup work. The client starts a job with POST /backups and receives a backupId and statusUrl. A Backup Agent scans files, creates chunks and checksums, and sends them through the Upload API. Workers store encrypted chunks and update progress. The client can list progress, retry failed work, restore files, and request an integrity report. HTTPS with JWT or mTLS protects client requests. The trade-off is more coordination, but queues and workers make long-running work scalable and recoverable.

Detailed Explanation

The API manages long-running backups without making the client wait. The main challenge is separating quick control requests from slower upload, retry, restore, and analysis work. I would explain the design in the same order as the diagram.

Useful Questions to Ask the Interviewer
  1. Which clients and core use cases must the API support?
  2. What authentication, authorization, and data-validation rules should I assume?
  3. What scale, error handling, idempotency, and versioning requirements matter?
Design the interfaces for a scalable file backup system. diagram
How to Explain It in an Interview
1. Start a backup job

I would begin with the main control path. The Operator, App, or CLI sends POST /backups through HTTPS with JWT or mTLS. A JWT is a signed token that carries caller identity. mTLS encrypts traffic and lets both sides verify certificates.

The API Gateway and Auth component checks the caller. It then forwards the authenticated request to the Backup API. The Backup API asks the Backup Coordinator to create a backup session.

The coordinator writes the new backupId and queued state into the Metadata and Progress Store. It also enqueues the initial scan and upload work in the Retry Queue. The Backup API returns 201 Created with the backupId and statusUrl.

This response is quick because the backup continues asynchronously.

2. Scan files and upload chunks

The Backup Agent reads the Source Files. It scans each file, splits it into chunks, and calculates a checksum. A checksum is a small value used to detect changed or damaged data.

The agent sends PUT /backups/{backupId}/chunks directly to the Upload API. The request contains the manifest delta and chunk data. The manifest records which files and chunks belong to the backup.

The Upload API validates the request and forwards valid chunks to an Upload Worker. The worker stores encrypted chunks in Object Storage. It also updates the manifest and progress in the Metadata and Progress Store.

The Upload API returns a chunk acknowledgement or a retryable error to the Backup Agent. The agent can continue after success or retry that chunk later.

3. List backup progress

The client checks the job with GET /backups/{backupId}/progress. The request passes through the API Gateway and Auth component. It then reaches the Backup API as an authenticated request.

The Backup API reads progress counters from the Metadata and Progress Store. It returns 200 OK with the state, uploaded bytes, completed items, and failed items.

The client reads one consistent progress view. It does not contact individual workers.

4. Retry failed work

The client sends POST /backups/{backupId}/retry through the API Gateway and Auth component. The authenticated request reaches the Backup API.

The Backup API reads failed files or chunks from the Metadata and Progress Store. It requeues that failed work in the Retry Queue. The API returns 202 Accepted with a message that the retry was scheduled.

The Retry Queue dispatches retry work to an Upload Worker. The worker retries the failed upload to Object Storage. It then updates the file state in the Metadata and Progress Store.

This keeps retry processing asynchronous. The client does not wait for every chunk to finish.

5. Restore files

The client sends POST /restores through the API Gateway and Auth component. The Restore API receives the authenticated request and creates a restore job for the Restore Worker.

The Restore Worker reads the manifest and file versions from the Metadata and Progress Store. It fetches the required chunks from Object Storage. It then streams the restored files to the Restore Target.

The Restore API returns 202 Accepted or 200 OK. The response contains a restore identifier or the restored files, as shown in the diagram.

The restore data path stays separate from the normal upload path.

6. Report changed or damaged data

The client sends GET /backups/{backupId}/report through the API Gateway and Auth component. The Report API receives the authenticated request and asks the Integrity and Diff Analyzer to generate the report.

The analyzer compares the current snapshot with the previous manifest. It also verifies stored chunk checksums in Object Storage. It records changed, deleted, partial, or corrupted results in the Metadata and Progress Store.

Changed means a file differs from the previous backup. Deleted means the file existed before but is now missing. Partial means the upload or backup is incomplete. Corrupted means checksum validation failed.

The Report API returns 200 OK with the report summary and affected files.

Practical Complexity & Trade-offs

The benefit of this design is that short API calls stay separate from slow file work. POST /backups creates the job quickly, while the queue and workers handle later processing. More Upload Workers can process more chunks without changing the client API. The Metadata and Progress Store keeps job state, manifests, progress, and failed-item records in one place. The downside is extra coordination between APIs, workers, the queue, and storage. HTTPS with JWT or mTLS protects client requests, but identity and certificate handling add operational work. Chunking supports large files and targeted retries, but it also requires manifest and checksum tracking. We accept this complexity because backups are long-running and failures should not restart the whole job.

Why Interviewers Ask This

The interviewer wants to see whether the candidate can design clear API boundaries for long-running work. They test correct HTTP methods, status codes, request directions, response directions, and asynchronous processing. A strong answer explains how authentication, queues, workers, metadata, object storage, retries, restores, and integrity reporting work together. The interviewer also checks whether the candidate can discuss scalability, failure recovery, and trade-offs without making unsupported guarantees.

Interviewer may ask next
How would this design handle many concurrent backup jobs and much larger files?

I would keep the same endpoints and scale the worker path. POST /backups would still create a session and quickly return the backupId and statusUrl. The Backup Coordinator would continue writing the queued state and placing initial work into the Retry Queue.

The main change would be running more Upload Workers. The queue would distribute pending and retry work across those workers. Each worker would store encrypted chunks in Object Storage and update the Metadata and Progress Store. Large files would still be split into chunks, so different chunks could be processed without sending the whole file again.

The client would continue using GET /backups/{backupId}/progress. It would not need to know how many workers are running. HTTPS with JWT or mTLS would remain unchanged.

The main downside is more concurrent updates to progress and manifest data. The system must keep each update linked to the correct backupId, file, and chunk. This adds coordination work, but the public API remains stable.

What happens when uploads fail or stored chunks later become corrupted?

I would use the retry and reporting flows already shown. During upload, the Upload API can return a retryable error to the Backup Agent. The agent can retry that chunk. The client can also call POST /backups/{backupId}/retry for recorded failures.

The Backup API reads failed files or chunks from the Metadata and Progress Store. It places them into the Retry Queue and returns 202 Accepted. The queue dispatches those items to an Upload Worker. The worker retries the failed upload to Object Storage and updates the file state.

For later integrity checks, the client calls GET /backups/{backupId}/report. The Integrity and Diff Analyzer verifies chunk checksums and compares the current snapshot with the previous manifest. It marks items as changed, deleted, partial, or corrupted. The Report API then returns 200 OK with the summary and affected files.

The downside is extra storage reads and checksum work. We accept that cost because it detects incomplete or damaged backup data before restore time.

29. Should a logging-events product own its client libraries or let other teams build their own clients?API DesignHardNetflix

Question Details

Walk through whether the team should own client libraries that ingest logging events or provide documentation and let other teams build clients. Explain the API contract, compatibility, support, adoption, and maintenance tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, I would own thin official client libraries for common languages. Producer apps use these SDKs to send batches of structured log events to POST /v1/log-events over HTTPS. The SDKs handle authentication, batching, compression, retries with exponential backoff, schema validation, and version compatibility. The ingest API returns 202 Accepted or a 4xx or 5xx error through the client. Custom clients remain useful for unsupported languages or special runtimes. The trade-off is higher SDK maintenance in exchange for faster adoption, stronger compatibility, and lower support cost.

Detailed Explanation

The goal is to make logging-event ingestion simple and consistent across many producer teams. The main challenge is deciding who owns the client-side behavior. I would compare both choices around the same product-owned ingest API.

Useful Questions to Ask the Interviewer
  1. Which clients and core use cases must the API support?
  2. What authentication, authorization, and data-validation rules should I assume?
  3. What scale, error handling, idempotency, and versioning requirements matter?
Should a logging-events product own its client libraries or let other teams build their own clients? diagram
How to Explain It in an Interview
1. Define the shared API boundary

I would begin with one stable API owned by the logging-events product team. The endpoint is POST /v1/log-events over HTTPS. It accepts a batch of structured log events.

The product team also owns the API contract and versioning rules. The current contract is versioned as v1. Backward compatibility is part of that contract.

The same team publishes Docs / OpenAPI / Examples. It also owns the Compatibility / Deprecation Policy. These shared assets support both client approaches.

A successful request returns 202 Accepted. A failed request returns a 4xx or 5xx error.

2. Explain the recommended official SDK path

For common languages, I would provide thin official client libraries. Examples include Python, Java, Go, and Node.

The Producer Apps belong to other teams. They call the Official Client Libraries first. The SDK then calls the Logging Events Ingest API.

Before sending the request, the SDK handles authentication using API keys or OAuth. It also handles batching, compression, retries with exponential backoff, schema validation, and version compatibility.

The request path is Producer Apps to Official Client Libraries. It then continues to POST /v1/log-events over HTTPS.

The response follows the reverse path. The API returns 202 Accepted or a 4xx or 5xx error to the SDK. The SDK then returns that result to the Producer App.

3. Explain why official SDKs are the default

Official SDKs give high contract consistency. Supported teams use the same request format and client behavior.

They also make backward compatibility easier to manage. The product team can update supported libraries when the contract evolves.

Adoption is faster because teams do not rebuild common client behavior. They receive authentication, retries, validation, and examples in one supported package.

Centralized fixes are another benefit. The product team can correct shared bugs and security problems in the official libraries.

The main downside is maintenance. The product team must build, test, release, and support several language libraries.

4. Explain the team-built custom client path

The alternative is to publish documentation and let producer teams build custom clients. These teams use the OpenAPI specification, guides, code examples, and SDK usage samples.

The Producer App calls the Team-built Custom Client. That client sends the same structured event batch to POST /v1/log-events over HTTPS.

The API returns 202 Accepted or a 4xx or 5xx error to the custom client. The custom client then returns the result to the Producer App.

This option gives flexibility for special needs and unusual runtimes. It also lowers the SDK ownership burden for the product team.

However, client behavior can vary. Authentication, batching, compression, retries, schema validation, and version compatibility may be implemented differently.

5. Compare support and compatibility risks

Official SDKs make support easier because their behavior is predictable. Problems are usually easier to reproduce across producer teams.

Custom clients increase support and debugging work. Each team may use a different implementation and may handle errors differently.

Custom clients can also create compatibility drift. A team may not follow new guidance or deprecation timelines correctly.

The Compatibility / Deprecation Policy reduces this risk. It defines a clear deprecation process, minimum support windows, and change announcements.

Contract tests also help teams check whether their clients follow the shared API contract.

6. Give the final recommendation

My recommended default is to own thin official client libraries for common languages. I would expose the same stable, versioned ingest API to every client.

I would also maintain OpenAPI documentation, examples, compatibility rules, and contract tests.

Custom clients should be exceptions. They make sense for unsupported languages, special runtimes, or unique requirements.

The producer team should build and maintain its custom client. It should also own support for that implementation.

This approach balances consistency and fast adoption with limited flexibility where it is needed.

Practical Complexity & Trade-offs

The benefit of official SDKs is consistent behavior. Teams receive the same authentication, batching, compression, retry, validation, and compatibility rules. This improves adoption and makes support easier. The downside is maintenance. The product team must release and support several language libraries. Custom clients reduce that SDK work and give teams more flexibility. However, their behavior can vary. One team may retry correctly, while another may not. Compatibility can also drift over time. Clear OpenAPI documentation, deprecation rules, and contract tests reduce this risk. We accept the SDK maintenance cost because most teams gain a faster and more predictable integration path. Custom clients remain exceptions for unsupported languages or special runtimes.

Why Interviewers Ask This

Interviewers want to see whether you can define a clear API boundary and assign ownership correctly. They also test your understanding of request and response flow, versioning, compatibility, retries, validation, adoption, and support costs. A strong answer compares both choices fairly. It explains why official SDKs improve consistency and why custom clients still help in special cases.

Interviewer may ask next
What would you do if an official SDK started causing repeated failed requests?

I would keep the same API contract and fix the problem in the official SDK. The affected flow is Producer Apps to Official Client Libraries to POST /v1/log-events over HTTPS.

First, I would identify whether the failure comes from authentication, batching, compression, retries, schema validation, or version compatibility. The ingest API would continue returning 202 Accepted or the existing 4xx or 5xx error.

The product team would correct the shared SDK behavior and release an updated library. This is a major benefit of product-owned clients because one fix can help every team using that SDK.

The Docs / OpenAPI / Examples should also explain the correct behavior. Contract tests should verify that the corrected SDK follows the stable API contract.

The downside is that producer teams must upgrade to the corrected SDK release. The product team must also maintain and test that release across the supported language ecosystem.

How would you handle an API contract change without breaking producer teams?

I would keep the existing v1 contract backward compatible while it remains supported. The affected components are API Contract & Versioning, Official Client Libraries, Docs / OpenAPI / Examples, and the Compatibility / Deprecation Policy.

The product team would publish the contract change and explain it in the OpenAPI specification and examples. Official SDKs would be updated so supported producer teams can adopt the change through a normal library upgrade.

Teams with custom clients would update their own implementations. They would use the published contract tests to check compatibility.

The request path would remain Producer App to client to POST /v1/log-events over HTTPS. The response would still return 202 Accepted or a 4xx or 5xx error through the client.

The main downside is added maintenance during the transition. The benefit is that producer teams receive clear migration guidance and are less likely to break unexpectedly.

30. Design an ad frequency capping system.System DesignMediumNetflix

Question Details

Design a frequency-capping system for an advertising platform that limits how often a user sees an ad from the same advertiser within a time window and supports caps at line-item, campaign, and category levels.

Short Interview Answer (30-60 seconds)

At a high level, this system prevents one user from seeing ads from the same advertiser too often. The main challenge is making a fast decision while checking several limits. I would explain it in three parts: request ingestion, candidate generation, and frequency-cap evaluation. The Capping Service checks line-item, campaign, and category caps using fast counters and stored rules. It allows an eligible candidate or blocks it and tries another. The trade-off is that fresher counters improve accuracy but add work to every decision.

Detailed Explanation

The goal is to control how often one user sees ads from the same advertiser. The system must apply limits within a time window. It must also support line-item, campaign, and category caps. The difficult part is making this decision quickly for every ad candidate. The diagram solves this with five clear areas: request ingestion, candidate generation, cap evaluation, the final outcome, and the supporting data layer.

Useful Questions to Ask the Interviewer
  1. Which user flows and system capabilities are required for the first version?
  2. What traffic, data volume, latency, and availability targets should I design for?
  3. Which consistency, security, geographic, and cost constraints matter most?
Design an ad frequency capping system. diagram
How to Explain It in an Interview
1. Explain the goal and cap priority

I would begin by explaining that one candidate can have several limits. The line-item cap is the most specific limit. The campaign cap covers a larger group of line-items. The category cap is the broadest limit shown in the diagram.

The checks happen in that priority order. A candidate must remain under every applicable limit. If one check is over its cap, the system should stop evaluating that candidate.

2. Explain request ingestion

The ad request first enters the Request Ingestion area. The user device sends the request through the Edge or CDN.

The Edge or CDN is the platform entry point near the user. It receives the request and passes it to Candidate Generation. This keeps the first step small and focused.

3. Explain candidate generation

Candidate Generation produces possible ads for the request. These candidates may match the user and placement, but they are not approved yet.

Each candidate is sent to the Capping Service. This separation is important. A candidate can be relevant while still exceeding a frequency limit.

4. Explain the frequency-capping decision

The Capping Service checks the Line-item Cap first. If that limit is exceeded, the candidate goes to “Block (Try Next).” The system can then evaluate another candidate.

If the line-item remains under its limit, the service checks the Campaign Cap. It then checks the Category Cap. A candidate reaches “Allow Select Ad” only when all required checks remain within their caps.

The service uses the Real-time Counter Store for fast access to recent impression counts. The Configuration Store provides the caps, rules, and time windows. The counts must represent the user, advertiser, and relevant line-item, campaign, or category scope.

5. Explain the data layer and trade-off

The Real-time Counter Store supports the fast decision path. The Event Log Store keeps a durable record of impression-related events. The Configuration Store keeps the rules used during evaluation.

The benefit is flexible control at several levels. The downside is that every candidate needs several reads and checks. Fresh counters make the decision more accurate, but updating them adds work. If the counter data is slightly old, two close requests may both appear eligible. A stricter design can protect each counter update, but that makes the decision slower.

Engineering Considerations / Design Trade-offs

The benefit is that the platform can limit exposure at three useful levels. Line-item caps give precise control. Campaign and category caps provide wider protection. The Real-time Counter Store makes checks fast because recent counts are easy to read. The downside is extra work on every candidate. The service must read counters, load rules, and evaluate several limits. The counters must also stay fresh. Old counts may allow an extra impression. Stronger counter updates reduce this risk, but they make the fast path slower. We accept this trade-off because the cap decision must happen before the candidate is selected.

Why Interviewers Ask This

Interviewers use this question to test how you break a fast decision system into clear stages. They want to see whether you understand layered limits, time-window counters, rule storage, and fallback behavior. They also look for good judgment about speed versus counting accuracy. A strong answer explains why candidate generation and final eligibility are separate decisions.

Interviewer may ask next
How would the design change if the advertiser requires a strict cap with no extra impression allowed?

I would keep the same main flow, but I would make each counter check and counter update one protected operation. The main change is inside the Capping Service and Real-time Counter Store.

Without this protection, two requests may read the same count at nearly the same time. Both could believe that one slot remains. A protected update checks the count and reserves the next impression together. Only one request can claim the final allowed slot.

The service must apply this rule to the line-item, campaign, and category counters. If any protected update cannot succeed, the candidate goes to “Block (Try Next).” This keeps the strict cap correct.

The main downside is speed. Protected updates create more waiting and more work in the counter store. The system may handle fewer decisions during very busy periods.

What should the Capping Service do if the Real-time Counter Store is unavailable?

I would keep the same architecture, but I would define a clear failure rule for the cap decision. The affected path is the counter lookup inside the Capping Service.

For strict advertisers, I would send the candidate to “Block (Try Next).” The service cannot prove that the candidate is under its cap, so blocking protects the advertiser’s rule. The platform can continue by testing another candidate.

For less strict traffic, the business could choose to allow the candidate. That protects ad delivery, but it may exceed the frequency limit. The Configuration Store and Event Log Store cannot replace the missing real-time counts because they have different jobs.

The main downside is a business trade-off. Blocking protects correctness but may reduce available ads. Allowing protects delivery but may show the user too many impressions.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Company Notice: This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.