Netflix Python Developer Interview Questions & Answers

netflix icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

21. Calculate employee levels, balanced employees, and a level histogram from an org chart.CodingMediumNetflix

Question Details

Given an organization chart represented as a tree, calculate each employee's level, identify employees with equal numbers of nodes above and below, and produce a histogram by level.

Short Interview Answer (30-60 seconds)

I would use one depth-first search starting from the CEO at level 0. When I enter an employee, I record the current depth as the level and increase that level’s histogram count. When the recursive calls return, I calculate the employee’s subtree size. The number below is subtree size minus one. If it equals the depth, the employee is balanced. Every employee is visited once, so the time complexity is O(n), and the auxiliary space complexity is O(n).

Detailed Explanation

See the Code while reading this explanation.

The input is a rooted organization tree. We need to calculate each employee’s level, find employees whose number of managers above equals their number of descendants below, and count how many employees appear at each level. A depth-first search fits this problem because depth is available while moving down the tree, while subtree size is available when the recursive calls return.

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?
Calculate employee levels, balanced employees, and a level histogram from an org chart. diagram
How to Explain It in an Interview
1. Define the input and outputs

The organization chart is stored as an adjacency list. Each employee maps to a list of direct reports.

For the example:

CEO -> [CTO, CFO, COO] CTO -> [Dev1] CFO -> [Fin1] COO -> [Ops1] Dev1 -> [] Fin1 -> [] Ops1 -> []

The CEO is the root.

The level of an employee is the number of managers above that employee. The CEO has level

  1. CTO, CFO, and COO have level
  2. Dev1, Fin1, and Ops1 have level 2.

The number below an employee means the number of descendants in that employee’s subtree. If subtree_size includes the employee, then descendants equals subtree_size minus one.

An employee is balanced when:

depth == subtree_size - 1

The function returns a map of employee levels, a list of balanced employees, and a histogram that maps each level to its employee count.

2. Choose one depth-first search

I call DFS with the CEO and depth 0.

When DFS enters an employee, it records the employee’s level and increases the histogram count for that level.

It then visits every direct report with depth plus one.

When all child calls return, DFS adds their subtree sizes and includes the current employee. This gives the exact subtree size for the current employee.

3. Initialize the state

The levels map starts as an empty dictionary. It stores employee name to level.

The histogram starts empty. It stores level to employee count.

The balanced list starts empty. It stores employees that pass the balance condition.

The traversal starts at CEO with depth 0.

The main invariant is: after DFS returns from an employee, the returned subtree size is correct for that employee, and every employee in that subtree has already been processed.

4. Walk through the exact example

Step 1: Enter CEO at depth 0. Record CEO: 0. The histogram becomes {0: 1}.

Step 2: Enter CTO at depth 1. Record CTO: 1. The histogram becomes {0: 1, 1: 1}.

Step 3: Enter Dev1 at depth 2. Record Dev1: 2. The histogram becomes {0: 1, 1: 1, 2: 1}. Dev1 has no reports, so its subtree size is 1. Its descendant count is 0. Depth 2 does not equal 0, so Dev1 is not balanced.

Step 4: Return to CTO. CTO’s subtree contains CTO and Dev1, so its subtree size is 2. Its descendant count is 1. Its depth is also 1, so CTO is balanced.

Step 5: Enter CFO at depth 1. Record CFO: 1. The histogram becomes {0: 1, 1: 2, 2: 1}.

Step 6: Enter Fin1 at depth 2. Record Fin1: 2. The histogram becomes {0: 1, 1: 2, 2: 2}. Fin1 has subtree size 1 and 0 descendants. Depth 2 does not equal 0, so Fin1 is not balanced.

Step 7: Return to CFO. CFO has subtree size 2 and 1 descendant. Its depth is 1, so CFO is balanced.

Step 8: Enter COO at depth 1. Record COO: 1. The histogram becomes {0: 1, 1: 3, 2: 2}.

Step 9: Enter Ops1 at depth 2. Record Ops1: 2. The histogram becomes {0: 1, 1: 3, 2: 3}. Ops1 has subtree size 1 and 0 descendants. Depth 2 does not equal 0, so Ops1 is not balanced.

Step 10: Return to COO. COO has subtree size 2 and 1 descendant. Its depth is 1, so COO is balanced.

Step 11: Return to CEO. The CEO’s subtree contains all 7 employees. The CEO therefore has 6 descendants. Its depth is 0, so the CEO is not balanced.

The final result is:

levels = {CEO: 0, CTO: 1, CFO: 1, COO: 1, Dev1: 2, Fin1: 2, Ops1: 2}

balanced = [CTO, CFO, COO]

histogram = {0: 1, 1: 3, 2: 3}

5. Explain why the result is correct

The depth passed into DFS is exactly the number of managers above the current employee.

The subtree size returned by DFS includes the current employee and every descendant. Subtracting one removes the current employee and gives the exact number of descendants below.

Therefore, depth == subtree_size - 1 correctly identifies balanced employees.

The histogram is correct because every employee increases exactly one bucket for the employee’s level.

6. Explain the Python implementation

The outer function creates the levels dictionary, histogram, and balanced list.

The nested dfs function receives an employee and that employee’s depth. It records the level and histogram count before visiting direct reports.

It starts subtree_size at 1 because the subtree contains the current employee. Each recursive child call returns a child subtree size, which is added to the total.

After all children return, the code calculates descendants as subtree_size - 1. It adds the employee to balanced when descendants equals depth. It then returns the subtree size to the parent.

After DFS finishes, the code returns the levels map, balanced list, and histogram sorted by level.

7. Explain complexity and edge cases

Let n be the number of employees. Each employee is entered once and returned from once. Therefore, the time complexity is O(n).

The auxiliary space complexity is O(n). The levels map, histogram, balanced list, and recursion stack can grow with the number of employees.

Relevant edge cases are a single-employee tree, a skewed tree, employees with no reports, and employees represented by empty child lists. A single CEO has depth 0 and 0 descendants, so that CEO is balanced under the same rule.

Key Insight / Why This Solution Works

The key insight is that the two values used by the balance test become available at different parts of one DFS. The employee’s depth is known when DFS enters the node, so it gives the number of managers above. The employee’s subtree size is known after all child calls return, so subtree_size - 1 gives the number of descendants below. The invariant is that when DFS returns from a node, its complete subtree has been processed and its returned subtree size is correct. This lets one traversal produce all three required outputs.

Code
from collections import defaultdict
from typing import Dict, List, Tuple


def analyze_org_chart(
    org: Dict[str, List[str]], root: str
) -> Tuple[Dict[str, int], List[str], Dict[int, int]]:
    """Return employee levels, balanced employees, and a level histogram."""

    # employee name -> level from the root
    levels: Dict[str, int] = {}

    # level -> number of employees at that level
    histogram = defaultdict(int)

    # Employees whose managers above equal descendants below
    balanced: List[str] = []

    def dfs(employee: str, depth: int) -> int:
        # Record the employee's level when entering the node.
        levels[employee] = depth

        # Count this employee in the correct level bucket.
        histogram[depth] += 1

        # The subtree contains at least the current employee.
        subtree_size = 1

        # Visit each direct report with the next depth.
        for report in org.get(employee, []):
            subtree_size += dfs(report, depth + 1)

        # Remove the current employee to count only descendants.
        descendants = subtree_size - 1

        # depth is the number of managers above this employee.
        if depth == descendants:
            balanced.append(employee)

        # Give the complete subtree size to the parent call.
        return subtree_size

    # The root starts at level 0.
    dfs(root, 0)

    # Return the histogram in increasing level order.
    return levels, balanced, dict(sorted(histogram.items()))


if __name__ == "__main__":
    org_chart = {
        "CEO": ["CTO", "CFO", "COO"],
        "CTO": ["Dev1"],
        "CFO": ["Fin1"],
        "COO": ["Ops1"],
        "Dev1": [],
        "Fin1": [],
        "Ops1": [],
    }

    levels, balanced, histogram = analyze_org_chart(org_chart, "CEO")

    print("levels =", levels)
    print("balanced =", balanced)
    print("histogram =", histogram)
Time & Space Complexity

Let n be the number of employees. The time complexity is O(n) because DFS visits each employee once and processes each reporting relationship once. The auxiliary space complexity is O(n). The levels dictionary can hold n entries. The balanced list can contain up to n employees. The histogram can contain up to n level entries. The recursion stack can also grow to n when the organization tree is completely skewed.

Where it is used

This pattern is useful for organization charts, file-system trees, category trees, reporting hierarchies, and other rooted trees. It is helpful when a program needs information from above a node, such as depth, and information from below a node, such as subtree size or descendant count, during the same traversal.

Why Interviewers Ask This

This problem tests whether a candidate can combine top-down and bottom-up information in a tree. The level comes from the path from the root, while the descendant count comes from recursive return values. The interviewer is also checking recursion, adjacency-list traversal, invariant reasoning, management of several result structures, correct execution order, and accurate time and space analysis.

Common interview mistakes

A common mistake is counting only direct reports instead of all descendants. Another mistake is comparing depth with subtree size without subtracting the current employee. Some candidates check the balance condition before all child calls return, when the final subtree size is still unknown. Others forget to update the histogram on every node entry. It is also incorrect to claim O(1) auxiliary space because the result structures and recursion stack can grow with the tree.

Interview tip

Explain the solution with one sentence before coding: depth gives the number above, and subtree size minus one gives the number below. Then show that DFS provides both values in one traversal.

Interviewer may ask next
How would you handle an organization tree that is too deep for Python recursion?

I would replace recursive DFS with an explicit stack. Each stack item would store the employee, depth, and whether the node is being entered or returned from. On entry, I would record the level and histogram. On return, I would combine the child subtree sizes and run the same balance test. Correctness is preserved because the explicit stack follows the same entry and postorder-return order. The time remains O(n), and the auxiliary space remains O(n). The main tradeoff is more implementation detail, but it avoids Python recursion-depth errors.

Can the algorithm work if the organization chart is not a valid tree?

The original algorithm assumes one rooted tree. If reporting links can contain cycles, I would add a visit-state map and reject a cycle because subtree size is not well-defined for a cycle. If one employee can have multiple managers, the employee can be reached through more than one path, so levels and descendant subtrees may no longer have the same tree meaning. Validation takes O(n + e) time and O(n) space, where e is the number of reporting links. The tradeoff is extra validation and a need to define the desired graph semantics.

22. Find the k closest points to the origin.CodingMediumNetflix

Question Details

Given points in a plane and an integer k, return the k points closest to the origin and explain the data structure, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I use a size-limited max heap to keep the k closest points seen so far. Python heapq is a min heap, so I store each squared distance as a negative value. For every point, I compute x * x + y * y, push the negative distance with the point, and pop when the heap grows beyond k. This removes the farthest candidate. The solution takes O(n log k) time and O(k) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem gives a list of points in a two-dimensional plane and an integer k. We must return the k points closest to the origin, which is (0, 0). We compare squared distances, x² + y², because taking a square root would not change their order. A size-limited heap lets us keep only the best k points while processing the input.

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?
Find the k closest points to the origin. diagram
How to Explain It in an Interview
1. Understand the input and output

The input contains points in the form [x, y] and an integer k. The output contains point values, not indices.

For the example:

points = [[1, 3], [-2, 2], [4, 0], [0, 1]] k = 2

One valid output is:

[[-2, 2], [0, 1]]

The returned order does not matter.

2. Choose a size-limited max heap

We want to remove the farthest candidate whenever we have more than k points. A max heap is useful because its root represents the largest distance among the kept points.

Python heapq implements a min heap. To simulate a max heap, we store each squared distance as a negative value.

Each heap item has this form:

(-distance_sq, point)

A larger real distance becomes a smaller negative number. Therefore, heapq removes the farthest point first.

The invariant is that after each insertion and optional pop, the heap contains the closest min(k, processed points) points seen so far. When the heap contains k points, its root represents the farthest kept point.

3. Initialize and process each point

Start with an empty heap:

max_heap = []

For each point [x, y], calculate:

distance_sq = x * x + y * y

Push (-distance_sq, [x, y]) into the heap.

If the heap size becomes greater than k, pop one item. The popped item is the farthest point among the current candidates.

4. Walk through the verified example

For [1, 3], the squared distance is 1² + 3² = 10. Push (-10, [1, 3]). The heap contains one point.

For [-2, 2], the squared distance is (-2)² + 2² = 8. Push (-8, [-2, 2]). The heap now contains two points, so nothing is removed.

For [4, 0], the squared distance is 4² + 0² = 16. Push (-16, [4, 0]). The heap size becomes 3, which is greater than k. Pop once. The point [4, 0] is removed because its squared distance, 16, is the largest among the three candidates. The heap keeps [1, 3] and [-2, 2].

For [0, 1], the squared distance is 0² + 1² = 1. Push (-1, [0, 1]). The heap size again becomes 3. Pop once. The farthest kept point is [1, 3], with squared distance 10, so it is removed.

The final heap contains [-2, 2] and [0, 1]. Their squared distances are 8 and 1. These are the two smallest distances in the example.

5. Explain why the algorithm is correct

After every insertion and optional pop, the heap contains the closest min(k, processed points) points seen so far.

If a new point is farther than the current kept set, that new point becomes the farthest candidate and is removed. If the new point is closer, the previous farthest kept point is removed instead.

After all points are processed, the heap contains the k closest points from the input.

6. Explain the Python implementation

The function returns an empty list when k is zero or negative. It then creates an empty heap and processes every input point.

For each point, it calculates the squared distance and pushes the negative distance with the point. If the heap grows beyond k, it pops once.

After the loop, it extracts and returns the point from every heap item. A heap is not a fully sorted structure, so the returned order may vary.

7. Explain complexity and edge cases

Each of the n points is pushed into a heap whose size is kept near k. A push or pop costs O(log k), so the total time is O(n log k).

The heap stores at most k points after trimming, so the auxiliary space is O(k).

Relevant edge cases include k = 0, k equal to or greater than the number of points, negative coordinates, duplicate points, and equal distances. If k is at least the number of points, all points are returned. When distances tie, any valid set of k closest points may be returned.

Key Insight / Why This Solution Works

The key idea is to keep only the best k points instead of sorting all n points. The algorithm uses a size-limited max heap. Each entry stores (-distance_sq, point), where distance_sq is x * x + y * y. Python heapq is a min heap, so the negative distance makes the farthest real point appear at the root. After each push, the algorithm pops once if the heap size is greater than k. The invariant is that the heap contains the closest min(k, processed points) points seen so far.

Code
from typing import List
import heapq


class Solution:
    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        # Return no points when k is zero or negative.
        if k <= 0:
            return []

        # Python heapq is a min heap.
        # Negative distances make it behave like a max heap.
        max_heap: list[tuple[int, List[int]]] = []

        # Process each point.
        for x, y in points:
            # Squared distance is enough for comparison.
            distance_sq = x * x + y * y

            # Store the negative distance and the point.
            heapq.heappush(max_heap, (-distance_sq, [x, y]))

            # Keep only the k closest points seen so far.
            # This removes the current farthest candidate.
            if len(max_heap) > k:
                heapq.heappop(max_heap)

        # Return the points left in the heap.
        # Any output order is acceptable.
        return [point for _, point in max_heap]


if __name__ == "__main__":
    points = [[1, 3], [-2, 2], [4, 0], [0, 1]]
    k = 2

    result = Solution().kClosest(points, k)
    print(result)  # One valid output: [[-2, 2], [0, 1]]
Time & Space Complexity

Let n be the number of input points. The algorithm pushes every point into the heap and may pop one point after each push. Because the heap stays near size k, each push or pop takes O(log k) time. The total time is O(n log k). The heap stores at most k points after trimming, so the auxiliary space is O(k). Computing x * x + y * y takes O(1) time, and no square root is needed.

Where it is used

This top-k heap pattern is useful when a program must keep only the best k items from a large input or stream. Examples include finding nearby locations, selecting the lowest prices, keeping the highest scores, and tracking the most important events without sorting every candidate.

Why Interviewers Ask This

The interviewer is checking whether you recognize a top-k problem and choose a suitable heap. They want to see that you understand Python heapq is a min heap and can simulate a max heap with negative distances. The question also tests whether you can maintain a clear invariant, compare distances without unnecessary square roots, handle ties and duplicates, write correct Python, and explain O(n log k) time with O(k) auxiliary space.

Common interview mistakes

A common mistake is pushing positive distances into heapq. That creates a normal min heap and causes the closest point to be removed when the size exceeds k. Another mistake is forgetting to pop when the heap grows beyond k. Some candidates calculate square roots even though squared distances are enough. Others claim the heap output is sorted, but heap order is not fully sorted. It is also incorrect to claim that the displayed output order is the only valid order.

Interview tip

Explain the invariant before writing code: after every push and optional pop, the heap contains the closest min(k, processed points) points seen so far. This makes the reason for using negative distances and removing one point easy to defend.

Interviewer may ask next
How would the solution work if the points arrived as a continuous stream?

Keep the same size-limited heap between arrivals. For each new point, calculate its squared distance, push the negative distance and point, and pop once if the heap size becomes greater than k. The invariant remains unchanged, so the heap contains the closest min(k, points seen) points. Processing m streamed points takes O(m log k) total time and O(k) auxiliary space. The main benefit is that the full stream does not need to be stored.

What changes if the returned points must be ordered from closest to farthest?

Use the same heap process to select the k closest points. Then sort only those k points by x * x + y * y before returning them. The heap phase takes O(n log k), and sorting the result adds O(k log k). The total time is O(n log k + k log k), and the heap still uses O(k) auxiliary space. The tradeoff is extra work to guarantee the output order.

23. 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.

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.