Apple Python Developer Interview Questions & Answers

apple icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

21. Evaluate a Basic-Calculator-II-style arithmetic expression.CodingMediumApple

Question Details

Given a string arithmetic expression containing nonnegative integers and operators such as plus, minus, multiply, and divide, return the evaluated integer result. Define whitespace handling, operator precedence, division behavior, invalid input assumptions, and complexity.

Short Interview Answer (30-60 seconds)

I scan the expression from left to right. I build one number at a time and apply the previous operator when I reach the next operator. I keep completed additive terms in total and the current term in last_term. Multiplication and division update last_term immediately, so precedence works without a stack. Division truncates toward zero. Finally, I return total + last_term. The solution takes O(n) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a valid arithmetic-expression string containing nonnegative integers, spaces, and the operators +, -, *, and /. The output is the evaluated integer result. The main idea is to keep the newest term separate from the completed total. This lets multiplication and division change that term before addition or subtraction is finalized.

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?
Evaluate a Basic-Calculator-II-style arithmetic expression. diagram
How to Explain It in an Interview
1. Understand the input and output

The function receives one expression string. Spaces are ignored. The expression has no parentheses. Multiplication and division have higher precedence than addition and subtraction. Division truncates toward zero. The diagram assumes valid input, so the function does not validate malformed expressions.

For the example, the input is "3 + 2*2 - 8/4". The returned result is 5.

2. Choose the state

I use four variables.

  • total stores additive terms that are already complete.
  • last_term stores the newest term. It can still change if the next operation is multiplication or division.
  • current_number stores the number currently being read.
  • operator stores the previous operator that must be applied to current_number.

The invariant is that total stores completed additive terms, while last_term stores the current term after any multiplication or division updates. Therefore, total + last_term equals the value of the processed part of the expression.

3. Initialize and scan

I start with total = 0, last_term = 0, current_number = 0, and operator = '+'.

I scan the expression from left to right. A digit updates current_number with current_number * 10 + int(ch). This builds multi-digit numbers. A space is skipped.

The code appends a final '+' sentinel. A sentinel is an extra operator used only to make the loop commit the last number.

4. Apply the previous operator

When I reach an operator, I apply the previous operator to current_number.

For '+', I add the old last_term to total and start a new positive last_term.

For '-', I add the old last_term to total and start a new negative last_term.

For '*', I multiply last_term by current_number immediately.

For '/', I divide last_term by current_number and truncate toward zero. The implementation divides the absolute values with // and then restores the sign. This avoids floating-point conversion.

Then I save the new operator and reset current_number to 0.

5. Walk through the example

Start with total = 0 and last_term = 0.

Read 3 and reach '+'. The previous operator is '+'. Before the update, the state is total = 0, last_term = 0, current_number = 3. The code performs total += 0 and last_term = 3. The state becomes total = 0, last_term = 3.

Read 2 and reach '*'. The previous operator is '+'. Before the update, the state is 0, 3, 2. The code performs total += 3 and last_term = 2. The state becomes 3, 2.

Read the next 2 and reach '-'. The previous operator is '*'. The code performs last_term = 2 * 2 = 4. The state becomes 3, 4.

Read 8 and reach '/'. The previous operator is '-'. The code performs total += 4 and last_term = -8. The state becomes 7, -8.

Read 4 and reach the sentinel at the end. The previous operator is '/'. The diagram shows int(-8 / 4) = -2. The integer-only implementation gets the same result by calculating abs(-8) // 4 = 2 and restoring the negative sign. The state becomes 7, -2.

Finally, the function returns total + last_term = 7 + (-2) = 5.

6. Explain why it is correct

Addition and subtraction finalize the previous term by moving it into total. Multiplication and division update only last_term. This keeps higher-precedence work inside the current term before that term is added to total.

At every operator boundary, total + last_term equals the value of the processed prefix. After the sentinel commits the final number, this value equals the whole expression.

7. Explain complexity and edge cases

The loop processes each character at most once, so the time complexity is O(n), where n is the string length. The algorithm uses only a fixed number of variables, so the auxiliary space complexity is O(1).

Relevant cases are spaces, multi-digit numbers such as 14-3/2, chains such as 2*3*4, subtraction that creates a negative last_term, and an expression containing one number such as 42.

Key Insight / Why This Solution Works

The key idea is to separate completed additive terms from the newest term. total stores terms that can no longer change. last_term stores the current term, which may still be multiplied or divided. When the previous operator is + or -, the old last_term is moved into total and a new signed term begins. When the operator is * or /, only last_term changes. This preserves precedence without a stack. The invariant is that total + last_term equals the value of the processed prefix.

Code
def calculate(expression: str) -> int:
    # Stores additive terms that are already complete.
    total = 0

    # Stores the newest term, which may still change after * or /.
    last_term = 0

    # Builds the current one-digit or multi-digit number.
    current_number = 0

    # Treat the first number as a positive term.
    operator = "+"

    # Add a sentinel operator so the final number is committed.
    for ch in expression + "+":
        # Ignore whitespace.
        if ch == " ":
            continue

        # Build a multi-digit number from left to right.
        if ch.isdigit():
            current_number = current_number * 10 + int(ch)
            continue

        # Apply the previous operator to current_number.
        if operator == "+":
            total += last_term
            last_term = current_number
        elif operator == "-":
            total += last_term
            last_term = -current_number
        elif operator == "*":
            last_term *= current_number
        else:  # operator == "/"
            # Divide absolute values, then restore the sign.
            # This truncates toward zero without using floating point.
            quotient = abs(last_term) // current_number
            last_term = quotient if last_term >= 0 else -quotient

        # Save the new operator and prepare for the next number.
        operator = ch
        current_number = 0

    # Add the final current term to all completed terms.
    return total + last_term


if __name__ == "__main__":
    expression = "3 + 2*2 - 8/4"
    print(calculate(expression))  # 5
Time & Space Complexity

Let n be the number of characters in the expression. The algorithm scans from left to right and processes each character at most once, so the time complexity is O(n). It stores only total, last_term, current_number, operator, quotient, and the loop character. The amount of extra memory does not grow with n, so the auxiliary space complexity is O(1).

Where it is used

This pattern is useful in simple calculator features, expression evaluators, configuration parsers, and interview problems that need operator precedence without parentheses. It works when the supported operations are addition, subtraction, multiplication, and division.

Why Interviewers Ask This

This problem tests whether the candidate can preserve operator precedence during one left-to-right scan. It also checks state design, multi-digit parsing, whitespace handling, signed intermediate terms, division semantics, and careful Python implementation. The interviewer wants to see a clear invariant, a walkthrough that matches the code, accurate reasoning about the previous operator, and correct O(n) time and O(1) auxiliary-space analysis.

Common interview mistakes

A common mistake is adding every number directly into total, which loses multiplication and division precedence. Another mistake is applying the new operator instead of the previous operator at an operator boundary. Candidates may forget to process the final number, so the sentinel is important. Using a // b directly when a is negative is wrong because // floors instead of truncating toward zero. Other mistakes are not resetting current_number and not skipping spaces.

Interview tip

State the invariant before coding: total contains completed additive terms, while last_term contains the current term that multiplication or division may still change. Then connect every operator case to that invariant.

Interviewer may ask next
How would the solution change if parentheses were allowed?

The four-variable scan is not enough because parentheses create nested expressions. I would use recursion or stacks to evaluate each nested section. A closing parenthesis would finish one subexpression and pass its value back to the outer expression. The overall time can remain O(n), but the auxiliary space becomes O(n) in the worst case because of nested calls or stack entries.

Why not use Python's // operator directly for division?

Python's // operator rounds down. That differs from truncation toward zero when the current term is negative. For example, -3 // 2 is -2, but truncation toward zero should produce -1. Dividing absolute values and restoring the sign gives the required behavior. This keeps O(n) time and O(1) auxiliary space.

22. Build a trie-like fast VIN lookup with wildcard and null dimensions.CodingHardApple

Question Details

Design and implement lookup logic for vehicle identifiers or records where some dimensions may be exact, wildcard, or null. Explain insertion, matching priority, ambiguous matches, missing fields, and complexity.

Short Interview Answer (30-60 seconds)

I would use a trie with the fixed order wmi, model, year, and engine. Each record follows one path. Exact values use normal edges, "*" is a wildcard edge, and missing values use "∅". During lookup, I follow every matching exact or fallback edge and track a score of exact and null matches. The highest score wins. A tie is ambiguous. Insertion is O(d). Lookup is typically O(d), worst case O(2^d), and the stored trie uses O(n·d) space.

Detailed Explanation

See the Code while reading this explanation.

The input is a vehicle query with the dimensions wmi, model, year, and engine. Stored records may contain exact values, the wildcard "*", or a missing value represented by "∅". The goal is to return the single most specific matching record. A trie fits this problem because each level represents one dimension, while exact, wildcard, and null edges capture the matching rules.

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?
Build a trie-like fast VIN lookup with wildcard and null dimensions. diagram
How to Explain It in an Interview
1. Define the input, output, and matching rules

Every record and query uses the fixed dimension order [wmi, model, year, engine].

An exact stored value matches only the same query value. The wildcard "*" matches any query value. The null token "∅" matches only when the query field is also missing.

The specificity priority is exact > null > wildcard.

The lookup returns the unique most specific Record. It returns None when no complete record matches. It reports ambiguity when different records share the same best specificity score.

2. Build the trie

Each trie level represents one dimension. A complete root-to-leaf path represents one stored record. The leaf stores the Record object.

The four example paths are:

A: 1HG → Civic → 2020 → * B: 1HG → Civic → 2020 → 2.0L C: 1HG → * → 2020 → * D: 1HG → Civic → ∅ → *

Before insertion, exact values remain exact, "*" remains a wildcard, and None becomes "∅".

3. Initialize the lookup state

The example query is [1HG, Civic, 2020, 2.0L].

The lookup starts with a stack containing the root node, dimension index 0, zero exact matches, and zero null matches.

The invariant is: every active path matches all query dimensions processed so far.

4. Walk through the verified example

At WMI, the query value is 1HG. The lookup takes the exact 1HG edge. Records A, B, C, and D are still reachable. The score prefix is (1,0).

At model, the query value is Civic. The lookup follows both the exact Civic edge and the wildcard "*" edge. The exact branch keeps A, B, and D with score (2,0). The wildcard branch keeps C with score (1,0).

At year, the query value is 2020. The Civic branch follows exact 2020. Record D is removed because its stored "∅" does not match a provided year. The wildcard-model branch also follows exact 2020. A and B now have score (3,0). C has score (2,0).

At engine, the query value is 2.0L. The exact 2.0L edge reaches B with score (4,0). Wildcard edges reach A with score (3,0) and C with score (2,0).

The lookup compares the terminal matches. B has the highest score, so it returns record B.

5. Explain why the result is correct

The algorithm adds a path to the stack only when its next edge can match the current query field. Therefore, the active-path invariant remains true after every dimension.

Every terminal record reached by the traversal is a valid match for the full query. The score tuple stores exact matches first and null matches second. Python compares tuples from left to right, so more exact matches always win. When exact counts tie, more null matches win. Wildcards add no score.

This implements exact > null > wildcard. If different records have the same best score, neither is more specific, so the result is ambiguous.

6. Explain the Python implementation

The Node class stores child edges in a dictionary and records at terminal leaves. insert() normalizes the four values and creates one trie path with setdefault().

lookup() normalizes the query and uses a stack to explore every valid matching branch. Each stack entry stores the current node, the next dimension index, the exact-match count, and the null-match count.

A wildcard child is always a valid fallback. A null child is considered only when the query field is missing. Otherwise, the matching exact child is considered.

At each terminal node, the code compares the path score with the best score found so far. It replaces a lower score and collects records with an equal best score.

7. Explain complexity and edge cases

Let d be the number of dimensions and n be the number of stored records. Here d is four.

Insertion takes O(d) time per record. A typical lookup takes O(d) when few fallback branches survive. In the worst case, an exact or null branch and a wildcard branch may both survive at many levels, producing O(2^d) reachable paths.

The stored trie uses O(n·d) space. The temporary lookup stack can contain up to O(2^d) active states in the generalized case.

Important edge cases are a missing query field, no terminal match, wildcard fallback, and multiple best records with the same score.

Key Insight / Why This Solution Works

The key idea is to organize records by one fixed dimension order. Each trie edge stores an exact token, the wildcard "*", or the null token "∅". During lookup, the algorithm keeps every branch that can still match the query. It also carries a specificity score of (exact_matches, null_matches). The invariant is that every active path matches all processed query dimensions. Exact matches increase the first score value. Null matches increase the second. Wildcards increase neither. Tuple comparison therefore implements exact > null > wildcard. The unique terminal record with the highest score is returned. A shared best score is reported as ambiguity.

Code
from __future__ import annotations

from dataclasses import dataclass, field

# Special stored tokens.
ANY = "*"
NULL = "∅"

# Every record and query uses this fixed traversal order.
DIMS = ("wmi", "model", "year", "engine")


@dataclass
class Record:
    """A stored vehicle record at a terminal trie node."""

    record_id: str
    values: tuple[str, str, str, str]


@dataclass
class Node:
    """One trie node containing outgoing token edges."""

    children: dict[str, "Node"] = field(default_factory=dict)
    records: list[Record] = field(default_factory=list)


class VinTrie:
    def __init__(self) -> None:
        # Every insertion and lookup starts at the root.
        self.root = Node()

    def _norm(self, value: str | None) -> str:
        """Convert one stored value into its trie token."""

        # Preserve an explicit wildcard token.
        if value == ANY:
            return ANY

        # Convert a missing stored value to the null token.
        if value is None:
            return NULL

        # Store exact values as strings.
        return str(value)

    def insert(self, record_id: str, raw: dict[str, str | None]) -> None:
        """Insert one record in wmi, model, year, engine order."""

        node = self.root

        # Normalize all dimensions before building the path.
        values = tuple(self._norm(raw.get(dim)) for dim in DIMS)

        # Create or reuse one edge for every normalized token.
        for token in values:
            node = node.children.setdefault(token, Node())

        # Store the complete record at the terminal leaf.
        node.records.append(Record(record_id, values))

    def lookup(self, raw: dict[str, str | None]) -> Record | None:
        """Return the unique most specific record for the query."""

        # Missing query fields become NULL.
        # Provided query fields remain exact strings.
        query = [NULL if raw.get(dim) is None else str(raw.get(dim)) for dim in DIMS]

        # Stack item: current node, next dimension, exact count, null count.
        stack: list[tuple[Node, int, int, int]] = [(self.root, 0, 0, 0)]

        best_score: tuple[int, int] | None = None
        best_records: list[Record] = []

        while stack:
            node, index, exacts, nulls = stack.pop()

            # A terminal state has processed every query dimension.
            if index == len(query):
                if node.records:
                    score = (exacts, nulls)

                    # Replace the current result when this path is better.
                    if best_score is None or score > best_score:
                        best_score = score
                        best_records = list(node.records)

                    # Keep equal best records so ambiguity can be detected.
                    elif score == best_score:
                        best_records.extend(node.records)

                continue

            query_token = query[index]

            # A wildcard stored edge is a fallback for any query token.
            wildcard_child = node.children.get(ANY)
            if wildcard_child is not None:
                stack.append((wildcard_child, index + 1, exacts, nulls))

            if query_token == NULL:
                # A stored null matches only a missing query field.
                null_child = node.children.get(NULL)
                if null_child is not None:
                    stack.append((null_child, index + 1, exacts, nulls + 1))
            else:
                # A provided query value may follow only the same exact edge.
                exact_child = node.children.get(query_token)
                if exact_child is not None:
                    stack.append((exact_child, index + 1, exacts + 1, nulls))

        # No terminal path matched the complete query.
        if not best_records:
            return None

        # Different record ids with the same best score are ambiguous.
        best_ids = {record.record_id for record in best_records}
        if len(best_ids) > 1:
            raise ValueError(f"Ambiguous match: {sorted(best_ids)}")

        # One unique best record remains.
        return best_records[0]


if __name__ == "__main__":
    trie = VinTrie()

    # A: exact WMI, model, and year, with any engine.
    trie.insert(
        "A",
        {
            "wmi": "1HG",
            "model": "Civic",
            "year": "2020",
            "engine": "*",
        },
    )

    # B: exact values in all four dimensions.
    trie.insert(
        "B",
        {
            "wmi": "1HG",
            "model": "Civic",
            "year": "2020",
            "engine": "2.0L",
        },
    )

    # C: any model for the exact WMI and year, with any engine.
    trie.insert(
        "C",
        {
            "wmi": "1HG",
            "model": "*",
            "year": "2020",
            "engine": "*",
        },
    )

    # D: Civic records whose stored year is missing.
    trie.insert(
        "D",
        {
            "wmi": "1HG",
            "model": "Civic",
            "year": None,
            "engine": "*",
        },
    )

    # This query exactly matches B in all four dimensions.
    query = {
        "wmi": "1HG",
        "model": "Civic",
        "year": "2020",
        "engine": "2.0L",
    }

    result = trie.lookup(query)

    # Expected output: B
    print(result.record_id if result is not None else None)
Time & Space Complexity

Let d be the number of dimensions and n be the number of records. Insertion takes O(d) time for each record because it processes every dimension once. A typical lookup takes O(d) time when only a small number of fallback branches remain active. The worst case is O(2^d) because an exact or null branch and a wildcard branch may both survive at several levels. Python dictionary lookup and insertion are O(1) on average. The stored trie uses O(n·d) space. The temporary traversal stack can use O(2^d) space in the generalized worst case.

Where it is used

This pattern is useful for vehicle configuration rules, product catalogs, routing rules, pricing policies, access rules, and feature configuration. It works well when records contain an ordered set of fields and some records act as general fallbacks. Exact records can override wildcard records, while null tokens can represent rules that apply only when information is missing.

Why Interviewers Ask This

This problem tests whether a candidate can adapt a trie to nonstandard matching rules. It checks whether the candidate can keep insertion and lookup consistent, maintain a clear invariant, and explore several valid branches without losing correctness. The interviewer also wants careful handling of wildcards, missing values, specificity, no-match behavior, and ambiguous results. A strong answer must explain why typical lookup is fast while still recognizing the O(2^d) branching worst case.

Common interview mistakes

A common mistake is treating a stored null as a wildcard. The null token "∅" must match only a missing query field. Another mistake is following every child instead of only valid exact, null, and wildcard edges. Candidates may return the first terminal record without comparing specificity scores. They may also ignore equal best scores and choose an arbitrary record instead of reporting ambiguity. Another error is comparing only the exact count and forgetting that null matches must outrank wildcard matches. Finally, do not claim that every lookup is guaranteed O(d), because fallback branches can create O(2^d) work.

Interview tip

State the three matching rules before writing code: exact matches the same value, null matches only a missing query field, and wildcard matches anything. Then define the score tuple (exact_matches, null_matches). This makes the traversal, priority, and ambiguity rules easier to explain.

Interviewer may ask next
How does lookup change when the query year is missing?

The normalized query year becomes "∅". At that level, the lookup may follow a stored "∅" edge and increase null_matches by one. It may also follow a wildcard "*" edge without increasing the score. It must not follow an exact year such as 2020 because the query did not provide that value. The invariant remains unchanged. Typical lookup is O(d), worst-case lookup is O(2^d), and the stored trie uses O(n·d) space.

How would you return all records tied for the best specificity instead of reporting ambiguity?

Keep insertion, traversal, and score comparison unchanged. After lookup finishes, return the full best_records list instead of checking its record ids and raising ValueError. Correctness is preserved because that list contains only terminal records with the highest specificity score. Typical lookup remains O(d), worst-case lookup remains O(2^d), and returning k tied records requires O(k) output space. The tradeoff is that the caller must decide how to handle several equally specific matches.

23. Implement multi-head self-attention from scratch.CodingHardApple

Question Details

Implement the core computation for multi-head self-attention. Define input tensor shapes, projection dimensions, attention-score calculation, masking if required, output shape, numerical stability concerns, and complexity.

Short Interview Answer (30-60 seconds)

I project the input into query, key, and value tensors. Then I split the model dimension into multiple heads and compute scaled dot-product attention inside each head. I apply the optional mask before a numerically stable softmax. Next, I multiply the attention weights by the values, merge the heads, and apply the output projection. The output shape is (B, T, d_model). The full time cost is O(B * T * d_model^2 + B * T^2 * d_model), and attention memory is O(B * H * T^2).

Detailed Explanation

See the Code while reading this explanation.

The input tensor has shape (B, T, d_model). Here, B is the batch size, T is the sequence length, and d_model is the feature width. The solution projects the input into query, key, and value tensors. It splits their channels across H heads. Each head calculates attention independently. The head outputs are then joined and projected back to the original feature width.

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 multi-head self-attention from scratch. diagram
How to Explain It in an Interview
1. Define the input and output

The input X has shape (B, T, d_model).

The projection matrices W_Q, W_K, W_V, and W_O each have shape (d_model, d_model) in this implementation.

The number of heads is H. Each head uses:

head_dim = d_model // H

Therefore, d_model must be divisible by H.

The output Y has shape (B, T, d_model).

2. Project and split the tensors

First, calculate:

Q = X @ W_Q

K = X @ W_K

V = X @ W_V

Each result has shape (B, T, d_model).

Next, reshape each tensor to (B, T, H, head_dim). Then transpose it to (B, H, T, head_dim).

This layout lets NumPy calculate every head in parallel.

3. Calculate scaled attention scores

For each head, every query token is compared with every key token:

scores = Q @ K^T / sqrt(head_dim)

The score tensor has shape (B, H, T, T).

The division by sqrt(head_dim) keeps dot products from becoming too large. Without this scaling, softmax can become too sharp.

If a Boolean mask is supplied, True keeps a position and False blocks it. Blocked scores are replaced with -1e9 before softmax. Their final probabilities become close to zero.

4. Apply a numerically stable softmax

Before exponentiation, subtract the largest value in each score row:

scores = scores - scores.max(axis=-1, keepdims=True)

This does not change the softmax result. It reduces the chance of overflow.

Then calculate:

weights = exp(scores) / sum(exp(scores))

Softmax runs across the key dimension. Each row of attention weights sums to approximately 1.

5. Walk through the verified example

Use B = 1, T = 3, d_model = 4, H = 2, and head_dim = 2.

The input is:

X = [[1, 0, 1, 0], [0, 1, 1, 1], [1, 1, 0, 1]]

For this example, W_Q = W_K = W_V = W_O = I_4. Therefore, Q = K = V = X, and the output projection does not change the merged context.

Head 1 receives:

[[1, 0], [0, 1], [1, 1]]

Its score matrix is approximately:

[[0.707, 0.000, 0.707], [0.000, 0.707, 0.707], [0.707, 0.707, 1.414]]

Its attention weights are approximately:

[[0.401, 0.198, 0.401], [0.198, 0.401, 0.401], [0.248, 0.248, 0.503]]

Its output is approximately:

[[0.802, 0.599], [0.599, 0.802], [0.752, 0.752]]

Head 2 receives:

[[1, 0], [1, 1], [0, 1]]

Its score matrix is approximately:

[[0.707, 0.707, 0.000], [0.707, 1.414, 0.707], [0.000, 0.707, 0.707]]

Its attention weights are approximately:

[[0.401, 0.401, 0.198], [0.248, 0.503, 0.248], [0.198, 0.401, 0.401]]

Its output is approximately:

[[0.802, 0.599], [0.752, 0.752], [0.599, 0.802]]

Concatenating the two head outputs gives:

[[0.802, 0.599, 0.802, 0.599], [0.599, 0.802, 0.752, 0.752], [0.752, 0.752, 0.599, 0.802]]

Because W_O = I_4, this is also the final output Y.

6. Explain why it is correct

Inside each head, softmax creates one probability distribution over the key positions for every query position. The weights in each row sum to approximately 1.

Multiplying those weights by V produces a valid weighted combination of the value vectors. Each head performs this calculation independently.

Concatenating all head outputs restores d_model channels. Applying W_O keeps the required final shape (B, T, d_model).

7. Explain complexity and edge cases

The dense query, key, value, and output projections cost O(B * T * d_model^2) time.

The score calculation and weighted-value calculation cost O(B * H * T^2 * head_dim), which simplifies to O(B * T^2 * d_model).

The full time complexity is therefore O(B * T * d_model^2 + B * T^2 * d_model).

The attention scores and weights need O(B * H * T^2) memory. The projected tensors and merged context also need O(B * T * d_model) memory.

Important edge cases are an invalid head count, padding or causal masks, long sequences with quadratic attention cost, and numerical overflow if softmax is not stabilized.

Key Insight / Why This Solution Works

The key idea is to perform scaled dot-product attention in several smaller channel groups called heads. The algorithm projects X into Q, K, and V, then reshapes them from (B, T, d_model) to (B, H, T, head_dim). Every query compares itself with all keys in the same head. The central invariant is that each softmax row forms a probability distribution over key positions. Therefore, each output row is a valid weighted combination of value vectors. The head outputs are concatenated and projected with W_O, which restores the required shape (B, T, d_model).

Code
import numpy as np


def multi_head_self_attention(
    x: np.ndarray,
    w_q: np.ndarray,
    w_k: np.ndarray,
    w_v: np.ndarray,
    w_o: np.ndarray,
    num_heads: int,
    mask: np.ndarray | None = None,
) -> np.ndarray:
    """Compute multi-head self-attention with NumPy.

    Args:
        x: Input tensor with shape (B, T, d_model).
        w_q: Query projection with shape (d_model, d_model).
        w_k: Key projection with shape (d_model, d_model).
        w_v: Value projection with shape (d_model, d_model).
        w_o: Output projection with shape (d_model, d_model).
        num_heads: Number of attention heads.
        mask: Optional Boolean mask with shape (B, T, T).
              True keeps a position. False blocks it.

    Returns:
        Output tensor with shape (B, T, d_model).
    """
    # Read the input dimensions.
    batch_size, seq_len, d_model = x.shape

    # Every head must receive the same number of channels.
    if d_model % num_heads != 0:
        raise ValueError("d_model must be divisible by num_heads")

    head_dim = d_model // num_heads

    def split_heads(tensor: np.ndarray) -> np.ndarray:
        # Change (B, T, d_model) into (B, H, T, head_dim).
        return tensor.reshape(
            batch_size,
            seq_len,
            num_heads,
            head_dim,
        ).transpose(0, 2, 1, 3)

    # Project the input into queries, keys, and values.
    q = split_heads(x @ w_q)
    k = split_heads(x @ w_k)
    v = split_heads(x @ w_v)

    # Compare every query with every key inside each head.
    # Result shape: (B, H, T, T).
    scores = q @ k.transpose(0, 1, 3, 2)
    scores = scores / np.sqrt(head_dim)

    # Block masked positions before softmax.
    if mask is not None:
        scores = np.where(mask[:, None, :, :], scores, -1e9)

    # Subtract the row maximum for numerical stability.
    scores = scores - scores.max(axis=-1, keepdims=True)

    # Apply softmax across the key dimension.
    weights = np.exp(scores)
    weights = weights / weights.sum(axis=-1, keepdims=True)

    # Use the attention weights to combine the value vectors.
    # Result shape: (B, H, T, head_dim).
    context = weights @ v

    # Move the head axis back and join all head channels.
    # Result shape: (B, T, d_model).
    context = context.transpose(0, 2, 1, 3).reshape(
        batch_size,
        seq_len,
        d_model,
    )

    # Apply the output projection.
    return context @ w_o


if __name__ == "__main__":
    # Use the exact example shown in the diagram.
    x = np.array(
        [
            [
                [1.0, 0.0, 1.0, 0.0],
                [0.0, 1.0, 1.0, 1.0],
                [1.0, 1.0, 0.0, 1.0],
            ]
        ]
    )

    # Identity projections make Q = K = V = X.
    identity = np.eye(4)

    output = multi_head_self_attention(
        x=x,
        w_q=identity,
        w_k=identity,
        w_v=identity,
        w_o=identity,
        num_heads=2,
    )

    # Expected rounded output:
    # [[[0.802, 0.599, 0.802, 0.599],
    #   [0.599, 0.802, 0.752, 0.752],
    #   [0.752, 0.752, 0.599, 0.802]]]
    print(np.round(output, 3))
Time & Space Complexity

Let B be the batch size, T the sequence length, H the number of heads, and d_model the model width. Each head has head_dim = d_model / H. The query-key scores and the weighted-value calculation cost O(B * H * T^2 * head_dim), which is O(B * T^2 * d_model). The four dense projections cost O(B * T * d_model^2). The full time cost is O(B * T * d_model^2 + B * T^2 * d_model). The attention score and weight tensors use O(B * H * T^2) extra memory. Projected and context tensors also use O(B * T * d_model) memory.

Where it is used

Multi-head self-attention is a core part of Transformer models. It is used in language models, machine translation, document search, vision Transformers, speech systems, and other sequence models. Different heads can learn different relationships, such as nearby patterns, long-range dependencies, or different feature interactions.

Why Interviewers Ask This

The interviewer wants to know whether you understand the main computation behind Transformers instead of only knowing a library call. This problem tests tensor-shape reasoning, batched matrix multiplication, scaling, masking, and stable softmax. It also tests whether you can explain why each output is a weighted combination of values. Finally, the interviewer checks whether you understand the quadratic sequence-length cost and can write clear NumPy code that matches the mathematics.

Common interview mistakes

Common mistakes include forgetting that d_model must be divisible by the number of heads, reshaping or transposing the wrong axes, and normalizing across the wrong dimension. Softmax must run across the key dimension. Another mistake is forgetting the sqrt(head_dim) scaling. The mask must be applied before softmax, not after it. Candidates may also call exp directly without subtracting the row maximum, which can cause overflow. Finally, all heads must be merged before applying W_O, and the full complexity must include both attention and projection costs.

Interview tip

Say the tensor shape after every major operation. The most important checkpoints are (B, H, T, head_dim) after splitting, (B, H, T, T) for the scores, and (B, T, d_model) after merging. This makes transpose and reshape errors easy to catch.

Interviewer may ask next
How would you add a causal mask so a token cannot attend to future tokens?

Create a lower-triangular Boolean matrix of shape (T, T). Positions on and below the diagonal are True, while future positions are False. Broadcast it to (B, T, T) or combine it with a padding mask. Apply it before softmax by replacing blocked scores with -1e9. Correctness is preserved because every query normalizes only over allowed keys. The time complexity remains O(B * T * d_model^2 + B * T^2 * d_model). Attention memory remains O(B * H * T^2). The tradeoff is that each token loses access to future context.

How can memory usage be reduced for very long sequences?

Standard attention stores a (B, H, T, T) score or weight tensor, so memory grows quadratically with T. A memory-efficient implementation can process score blocks and combine softmax statistics without storing the full matrix. This can preserve the same mathematical result, but the code is more complex. The attention time is still generally O(B * T^2 * d_model), while peak attention memory can be reduced below O(B * H * T^2). Sparse or local attention can reduce both work and memory further, but it changes which token pairs may interact.

24. Implement rate limiting for a public API.API DesignMediumApple

Question Details

Design the API-facing behavior for a public service with rate limits. Define limits, identity keys, request and response headers, 429 errors, burst behavior, retries, client guidance, abuse handling, observability, and compatibility.

Short Interview Answer (30-60 seconds)

At a high level, I would enforce rate limits at the API gateway before requests reach the backend. The gateway identifies each caller using an API key, user ID, OAuth client ID, or IP fallback. It loads the caller’s per-plan policy and checks a token bucket in Redis. Allowed requests reach the backend and return with RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers. When no token remains, the gateway returns 429 with Retry-After. This protects the service, but the gateway and Redis become important dependencies.

Detailed Explanation

The goal is to protect a public API from overload and abusive traffic. The main challenge is limiting each caller while still allowing normal bursts. I would explain the design by following the request and response paths.

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?
Implement rate limiting for a public API. diagram
How to Explain It in an Interview
1. Place rate limiting at the public API boundary

At a high level, the API client sends an HTTPS API request. The request first reaches the API Gateway / Rate Limiting Layer. The gateway applies the limit before the backend performs business work.

This gives every public client one consistent enforcement point. It also keeps rate-limit logic outside the Backend API Service. The backend can focus on processing allowed requests.

2. Identify the caller

The gateway authenticates or identifies the caller first. The Identity Key may be an API key or user ID. It may also be an OAuth client ID. An IP address is used as a fallback identity.

The selected identity determines the caller’s policy and counter. This prevents unrelated callers from sharing the same usage record. A stronger identity is better than IP when one is available.

The Abuse / WAF Rules also support this stage. They detect patterns such as botnets, spikes, and malicious IPs. They send blocking decisions to the gateway.

3. Fetch the caller’s limit policy

The gateway fetches the rule from the Rate Limit Policy Store. The store contains per-plan limits and burst capacity. Different plans can therefore receive different request allowances.

Burst capacity allows a short traffic spike. It avoids rejecting a caller after one small burst. The policy store owns the rule definition. It does not own the live request counter.

Keeping policy separate makes plan changes easier. It also keeps policy management separate from request enforcement.

4. Check and update the token bucket

The gateway checks the Counter Store in Redis. Redis stores token bucket counters and their TTL values. TTL means an unused counter can expire automatically.

A token bucket represents the caller’s current allowance. If a token is available, the gateway uses one token. The request is then allowed to continue. Short bursts are accepted until the bucket becomes empty.

The shared Redis store keeps counts consistent across gateway instances. The downside is that Redis becomes part of request admission. The diagram therefore uses a safe failure decision. For abuse-sensitive traffic, the gateway can fail closed. That means it rejects the request instead of risking overload.

5. Forward allowed requests and return the response

When the request is allowed, the gateway forwards it. The Backend API Service performs the requested business operation. The backend sends its API response back to the gateway. The gateway then returns the response to the API client.

A successful response uses a 200 or another 2xx status. It includes RateLimit-Limit, which shows the configured limit. It includes RateLimit-Remaining, which shows remaining capacity. It also includes RateLimit-Reset, which helps the client plan later calls.

These stable headers improve compatibility between clients and the service. Clients can use the same response contract as policies change.

6. Reject requests when the bucket is empty

If the bucket is empty, the gateway rejects the request early. The Backend API Service is not called. The gateway returns 429 Too Many Requests. It also returns Retry-After guidance.

The client should wait before trying again. It should back off instead of retrying immediately. This prevents repeated retries from creating more load.

The gateway owns this error response. The backend does not generate it for rejected requests.

7. Observe usage and explain the trade-off

The gateway sends rate-limit events to Observability. These events include 429 responses and latency information. The Counter Store also sends usage metrics.

Observability collects logs, metrics, alerts, and dashboards. It helps operators detect abuse and capacity problems. It is not part of the business response path.

The main benefit is centralized and consistent protection. The main downside is added gateway and Redis dependency. We accept this trade-off because protecting the backend is critical.

Why Interviewers Ask This

Interviewers use this question to test practical API judgment. They want to see whether you place rate limiting at the correct boundary, identify callers fairly, and model request and response directions correctly. They also check your understanding of token buckets, burst behavior, shared counters, 429 responses, retry guidance, abuse protection, observability, compatibility, and failure trade-offs. A strong answer clearly explains which component owns each decision.

Interviewer may ask next
How would this design scale across many API gateway instances?

I would keep the same request and response flow, but make the shared Redis counter path the main scaling concern. Every API Gateway / Rate Limiting Layer instance would still identify the caller, fetch the Rate Limit Policy Store rule, and check the same logical token bucket. The check and update operation must be atomic. This means two gateway instances cannot consume the same final token at the same time. Redis remains the shared source for token bucket counters and TTL values. Allowed requests still reach the Backend API Service. Successful responses still return RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Rejected requests still return 429 with Retry-After. Observability should track Redis latency, usage metrics, gateway latency, and 429 volume. The main downside is greater dependence on Redis. Gateway instances can scale horizontally, but the shared counter store must also handle the combined load safely.

How would you handle a sudden bot attack without blocking normal clients?

I would keep the existing rate-limit flow and strengthen the Abuse / WAF Rules. Those rules would detect botnets, sudden spikes, and known malicious IP addresses before requests reach the backend. The gateway would still prefer an API key, user ID, or OAuth client ID when available. IP would remain the fallback identity. This reduces the chance that many valid users behind one shared IP receive one combined limit. Normal callers would continue through the per-plan policy and token bucket checks. Allowed requests would reach the Backend API Service and receive the normal rate-limit headers. Exhausted callers would receive 429 with Retry-After. Clearly abusive traffic could be blocked by the WAF decision before backend work begins. Observability would record blocking events, 429 rates, latency, alerts, and counter usage. The main downside is false positives. Stronger rules may reject some valid traffic, so operators must review metrics and alerts carefully.

25. Design API boundaries for parsing and serving an event stream with random-access reads.API DesignMediumApple

Question Details

Design APIs for ingesting an event stream, storing parsed events, and later serving random-access reads. Explain request formats, event identifiers, validation, indexing assumptions, pagination, errors, idempotency, and schema evolution.

Short Interview Answer (30-60 seconds)

At a high level, I would separate event ingestion from event reading. Producers send events through the API Gateway to the Ingest API. The Parse and Validate Service checks required fields, loads the requested schema, prevents duplicate writes, stores the parsed event, and updates the random-access index. Consumers use the Read API to find events by event ID, stream offset, or cursor. The gateway handles JWT authentication and authorization. The main trade-off is extra coordination during writes, but indexed reads become much faster.

Detailed Explanation

The goal is to ingest events safely and serve them later through fast random-access reads. The main challenge is keeping validation, ordering, indexing, and schema versions consistent. I would explain the write flow first, followed by the read flow.

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 API boundaries for parsing and serving an event stream with random-access reads. diagram
How to Explain It in an Interview
1. Define the API boundary

I would separate the system into an ingest path and a read path. Both paths enter through the API Gateway plus AuthN and AuthZ. Authentication proves the caller's identity. Authorization decides whether that caller may perform the operation.

The producer sends write requests through the gateway. The gateway routes approved writes to the Ingest API. The consumer sends read requests through the same gateway. The gateway routes approved reads to the Read API.

This creates one clear security boundary for both APIs.

2. Accept an event through the ingest API

The producer calls POST /v1/streams/{stream_id}/events over HTTPS. The request carries a JWT and an Idempotency-Key header. The idempotency key helps prevent duplicate writes.

The body contains source_event_id, schema_version, occurred_at, and payload. The source_event_id identifies the event at the producer. The schema_version identifies the expected payload format. The occurred_at field records when the event happened.

After authorization, the Ingest API sends the request to the Parse and Validate Service.

3. Validate the payload and check idempotency

The Parse and Validate Service checks required fields and parses the payload. It sends a schema lookup to the Schema Registry and Compatibility Rules component. The registry returns the schema and compatibility information.

The service also sends a dedupe-key check to the Idempotency Store. The store returns whether the request is new or already processed. For a duplicate request, it can return the prior result.

The visible ingest errors are 400, 401, 409, and 422. A 400 or 422 response represents invalid request data. A 401 response represents missing or invalid authentication. A 409 response represents a duplicate or conflicting request. Each response returns through the gateway to the producer.

4. Store the event and update the index

For a valid new request, the service assigns an event_id and stream_offset. The event_id is the service's canonical event identifier. The stream_offset gives the event an ordered position in its stream.

The service writes the parsed event to the Parsed Event Store. It also updates the Random-Access Index Store. The index supports lookup by event ID, offset, and timestamp.

The service sends ingest metrics and audit records to Observability and Audit Logs. Invalid events follow the rejection path into the Failure Log.

On success, the gateway returns 201 Created. The response contains event_id, stream_offset, and schema_version.

5. Serve random-access reads

A consumer can request one event using GET /v1/streams/{stream_id}/events/{event_id}. It can also request a page using GET /v1/streams/{stream_id}/events?offset=&limit=&cursor=.

After authorization, the gateway sends the request to the Read API. The Read API first asks the Random-Access Index Store to locate matching events. The index returns event locations or pointers to the Read API.

The Read API then requests the parsed events from the Parsed Event Store. The store returns the stored event data. This two-step flow avoids scanning the full event store.

6. Handle pagination and schema evolution

The limit value controls the page size. The cursor identifies where the next page should continue. A successful response returns 200 OK with events[], next_cursor, and schema_version. An invalid cursor returns 400. A missing event returns 404.

For older event versions, the Read API checks the Schema Registry. It requests an upcast when the reader expects a newer schema. Upcasting converts an older event into a reader-compatible representation. The registry returns that compatible representation to the Read API.

The Read API also sends read metrics and access logs to Observability and Audit Logs.

7. Explain the main trade-off

The main benefit is fast lookup without scanning every stored event. The downside is that each accepted write updates two data structures. The Parsed Event Store and Random-Access Index Store must remain consistent. This adds write-path complexity, but it improves read performance.

Practical Complexity & Trade-offs

The design separates writes from reads because each path has different work. The write path validates fields, checks the schema, prevents duplicates, stores the event, and updates the index. The read path uses the index before loading full event data. The benefit is faster random access. The downside is more write coordination. Idempotency reduces duplicate writes, but it adds another lookup and another store to maintain. Cursor pagination works well for long streams, but invalid cursors need clear errors. Versioned schemas protect older data. Upcasting helps newer readers understand older events, but conversion rules must be maintained. We accept these costs because the system needs ordered storage, safe ingestion, and efficient lookup.

Why Interviewers Ask This

Interviewers use this question to test API boundaries and engineering judgment. They want correct request and response flows, clear ownership, and sensible event identifiers. They also evaluate validation, idempotency, indexing, pagination, schema evolution, and error handling. A strong answer separates authentication from authorization and explains why the event store and index are different. The candidate should also explain the main consistency trade-off without claiming unsupported guarantees.

Interviewer may ask next
What happens if the Random-Access Index Store cannot return an event location?

The Read API cannot safely complete the normal random-access flow. It first asks the Random-Access Index Store for event locations or pointers. Without that result, it does not know which parsed event record to fetch. I would not guess a location or scan the full Parsed Event Store, because that fallback is not part of this design. The request therefore remains unsuccessful, while the existing read metrics and access logging flow records the attempt. Authentication and authorization at the gateway remain unchanged. The read endpoint and response contract also remain unchanged. Once the index can return a valid location, the Read API continues with the normal event fetch. This preserves correctness because the API never returns an unrelated event. The downside is that the index becomes an important dependency for random-access reads. The design gains fast lookup, but read availability depends on that index path.

How does the design support a new schema without breaking older stored events?

The existing Schema Registry and compatibility flow handles that change. Producers continue sending schema_version with each event. During ingestion, the Parse and Validate Service looks up that version and receives the matching schema and compatibility information. An invalid payload follows the existing validation error path and is not stored as a valid event. The Parsed Event Store keeps the accepted event with its original schema version. During a read, the Read API can request an upcast when the reader expects a newer schema. The Schema Registry returns a reader-compatible representation to the Read API. The original stored event does not need to be rewritten. The ingest endpoint, read endpoints, gateway checks, index lookup, pagination, and logging paths stay the same. The benefit is backward compatibility. The downside is maintaining and testing conversion rules between supported schema versions.

26. Design a time-based key-value store API with TTL expiry.API DesignHardApple

Question Details

Design an API for a read-heavy time-based key-value store with TTL expiry. Define put, get, historical lookup or time-aware reads, expiration semantics, error behavior, concurrency, pagination if needed, and data consistency.

Short Interview Answer (30-60 seconds)

At a high level, I would build a read-heavy key-value API that keeps several versions of each key and gives every version its own TTL. Clients send HTTPS JSON requests through the API Gateway. The gateway handles JWT authentication and authorization, rate limiting, validation, and routing. It forwards requests over mTLS to the KV Service Cluster. The cluster supports PUT, GET, GET_AT, DELETE, and LIST operations. It stores versions in distributed shards and returns JSON through the gateway. Strong consistency protects each key. Background workers remove expired versions and compact tombstones. The trade-off is higher storage use for faster historical reads.

Detailed Explanation

The API stores changing values and supports reads at different times. The main challenge is combining fast reads, TTL expiry, and safe writes. I would explain the design by following the diagram from client to storage.

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 a time-based key-value store API with TTL expiry. diagram
How to Explain It in an Interview
1. Start with the client request path

I would begin with the API boundary. The client sends an HTTPS JSON request to the API Gateway. The gateway checks authentication and authorization using a JWT. A JWT is a signed token that identifies the caller. The gateway also applies rate limits, validates the request, and routes it.

The gateway forwards the accepted request to the KV Service Cluster over mTLS. mTLS encrypts the connection and lets both systems verify each other. The response follows the reverse path. The cluster returns JSON to the gateway. The gateway then returns an HTTPS JSON response to the client.

2. Define the API operations

PUT /v1/kv/{key} creates a new version. Its body contains value and ttl_seconds. The service records a write timestamp and sets expire_at to the current time plus the TTL. Previous versions remain stored because writes are append-only.

GET /v1/kv/{key} returns the latest valid version. A version is valid when the current time is earlier than expire_at.

GET_AT /v1/kv/{key}?at=timestamp performs a historical read. It returns the latest version where write_ts is at or before the requested timestamp and that timestamp is before expire_at.

DELETE /v1/kv/{key} creates a tombstone version. A tombstone is a marker that says the key was deleted. It hides the earlier versions from normal reads.

LIST /v1/kv?prefix=xxx&cursor=yyy returns keys matching a prefix. The cursor supports optional pagination, so the service does not return every result at once.

3. Explain the KV Service Cluster

The KV Service Cluster handles reads and writes. It owns versioning, TTL checks during reads, and concurrency control. It sends read and write operations to the distributed storage layer.

The design partitions keys across shards. The diagram uses Hash(key) modulo N to select the shard. Each shard stores key versions sorted by timestamp. It also stores each value, expire_at, and other metadata. This structure supports fast latest reads and historical lookups.

4. Explain TTL and background cleanup

TTL is enforced during every read. This means expired data becomes invisible immediately, even before physical deletion. GET returns 404 when the key is missing or expired. GET_AT also returns 404 when no version was valid at the requested time.

The KV Service Cluster sends expiry tasks to the TTL Expiry Worker. The worker scans data, deletes expired versions, and emits expiry events or metrics. The Compaction Worker later removes old tombstones and optimizes storage. These jobs run asynchronously. They are not part of the synchronous client response path.

5. Explain consistency and concurrent writes

I would use strong consistency per key. Writes for one key are linearizable. This means every accepted write appears in one clear order.

The API may support optimistic concurrency using an if-match version or timestamp. The client sends the version it expects to update. The service compares it with the current version. A match allows the write. A mismatch can return 409 Conflict. This prevents one writer from silently replacing another writer's newer value.

Different keys can still scale across separate shards because they do not share the same write order.

6. Explain errors and trade-offs

The API returns 400 for invalid input. It returns 401 when authentication is missing or invalid. It returns 403 when the caller is authenticated but not allowed. It returns 404 when a key is missing or expired. It may return 409 when a conditional write conflicts with the current version.

The main benefit is fast latest and historical reads with clear expiry behavior. The downside is additional storage for old versions, indexes, tombstones, and background cleanup. We accept this cost because time-aware reads are a core requirement.

Practical Complexity & Trade-offs

The API is simple for callers, but the storage model adds internal work. PUT always creates a new version. The benefit is safe history and easy time-aware reads. The downside is higher storage use. GET is fast because versions are ordered by timestamp. GET_AT uses the same ordering to find the latest version valid at a past time. TTL is checked during reads, so expired data disappears immediately. Background workers can delete it later. This is reliable, but it needs expiry scanning and compaction. Strong consistency per key prevents confusing write order. Optional if-match checks prevent lost updates, but callers may receive 409 and retry. Sharding improves scale across many keys. The downside is that changing the shard count requires careful data movement.

Why Interviewers Ask This

Interviewers use this question to test engineering judgment rather than endpoint memorization. They want clear rules for PUT, latest GET, historical GET_AT, DELETE, TTL expiry, and pagination. They also check whether request and response paths are correct, whether authentication and authorization have clear owners, and whether concurrent writes can lose data. A strong answer explains storage layout, consistency, scaling, failure behavior, and the cost of keeping version history.

Interviewer may ask next
How would you scale the design when the number of keys grows significantly?

I would keep the same API and add more storage shards. The KV Service Cluster would continue routing each key using the partitioning rule. The main change would be the shard mapping used by Hash(key) modulo N. Existing data would need to move from old shards to new shards during rebalancing. Reads and writes must continue using one authoritative mapping so the same key is not written to two places by mistake. Strong consistency per key would remain unchanged. PUT, GET, GET_AT, DELETE, and LIST would keep the same contracts. Historical versions and expire_at values would move together because they belong to the same key. The TTL Expiry Worker and Compaction Worker would also use the current shard mapping when scanning data. The main downside is operational complexity. Moving large amounts of version history can use network and storage capacity. A gradual migration reduces risk, but it takes longer and temporarily requires extra storage.

How would you handle many concurrent PUT requests for the same key?

I would keep strong consistency per key and order those writes on the key's owning shard. PUT /v1/kv/{key} would still create an append-only version with a write timestamp and expire_at value. For blind writes, the service would assign every accepted version a clear order. For compare-and-set behavior, the client would send the expected version or timestamp as an if-match value. The KV Service Cluster would compare that value with the latest stored version before writing. A match creates the next version. A mismatch returns 409 Conflict. The client can then read the latest value and decide whether to retry. GET and GET_AT continue using the same timestamp rules, so readers do not see a partly written version. Authentication, mTLS, TTL checks, and background cleanup remain unchanged. The main downside is lower throughput for one hot key because its writes must be ordered. Unrelated keys still scale across other shards.

27. Implement a robust REST API method.API DesignHardApple

Question Details

Design a robust REST API method for creating or updating a resource. Explain the request contract, authentication, validation, idempotency, duplicate retries, optimistic concurrency, error responses, and backward compatibility.

Short Interview Answer (30-60 seconds)

At a high level, I would expose a versioned create-or-update API at /v1/resources. The client first obtains a JWT, then sends a POST, PUT, or PATCH request over HTTPS. The request includes JSON, an Idempotency-Key, and optional If-Match. The Resource Service authenticates and authorizes the caller, validates the contract, checks duplicate retries, and verifies the ETag before updating. It returns 201 Created, 200 OK, or a standard JSON error. The trade-off is extra storage and processing for safer writes.

Detailed Explanation

The goal is to create or update a resource without duplicate writes or lost updates. The main challenge is handling retries, concurrent clients, invalid input, and older API clients. I would explain the design by following the request path 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?
Implement a robust REST API method. diagram
How to Explain It in an Interview
1. Define the request contract

I would start with the versioned endpoint /v1/resources.

The client uses POST to create a resource. It uses PUT or PATCH to update one. All communication uses HTTPS.

The request contains a JSON body and Content-Type: application/json. It also includes Authorization: Bearer <JWT> and an Idempotency-Key.

An update may include If-Match. This header carries the ETag or version the client expects.

The /v1 contract remains stable. New fields should be optional and additive. Existing field meanings should not change inside version one.

2. Authenticate and authorize the caller

The client first obtains a JWT from the Identity Provider or Auth Server.

The identity system authenticates the client and issues a signed token. The token contains scopes, roles, or other claims.

The client sends the JWT with the resource request. The Resource Service validates the token and checks the caller's scopes, roles, and permissions.

Authentication proves the caller's identity. Authorization decides whether that caller may create or update the resource.

An invalid token returns 401 Unauthorized. A valid caller without permission returns 403 Forbidden.

3. Validate the body and contract

The Resource Service checks the request with the Validation and Contract Compatibility component.

This component checks required fields, the JSON schema, and business rules. It also checks that the request remains compatible with /v1.

A malformed request returns 400 Bad Request. A request with invalid field values can return 422 Unprocessable Entity.

If the contract is valid, the service continues to the idempotency check. Otherwise, it returns a standard error response.

4. Handle idempotency and duplicate retries

The Resource Service looks up the Idempotency-Key in the Idempotency Store.

The store keeps the key, a request hash, the response status, the response body, and an expiry time.

If the key is not found, the service continues with the write. After completion, it stores the result for later retries.

If the same key and request appear again, the service returns the stored response. It does not perform another create or update.

This is useful when the client loses the first response because of a network failure. The client can retry safely without creating duplicate data.

5. Check concurrency and write the resource

The Resource Service reads the current resource and its version or ETag from the Resource Store.

For an update, it compares that value with If-Match. This is optimistic concurrency control, which prevents stale clients from overwriting newer data.

When the values match, the service applies its business logic. It writes the resource and stores a new version or ETag.

When the values do not match, the service rejects the update. It returns 409 Conflict or 412 Precondition Failed.

The database also uses unique constraints and indexes to protect stored resource data.

6. Return the response and record events

A successful create returns 201 Created. The response includes the resource JSON, an ETag, and a Location header.

A successful update returns 200 OK. It includes the updated resource and its new ETag.

Errors use application/problem+json. The body includes code, message, details, requestId, timestamp, and path.

Unexpected failures return 500 Internal Server Error. Temporary service failures return 503 Service Unavailable.

The Resource Service separately emits structured logs, audit events, metrics, and traces. The requestId connects those records to the original request. Observability helps investigation, but it does not own the business response.

Why Interviewers Ask This

Interviewers use this question to test practical API judgment. They want to see correct request and response modeling, clear authentication and authorization ownership, careful validation, and suitable HTTP status codes. They also evaluate whether the candidate understands duplicate retries, idempotency, optimistic concurrency, backward compatibility, and standard error handling. A strong answer explains the full flow clearly and discusses the cost of adding production safety controls.

Interviewer may ask next
What happens when many clients update the same resource at the same time?

I would keep the same optimistic concurrency flow. Each client sends its latest ETag or version in the If-Match header with the PUT or PATCH request to /v1/resources. The Resource Service reads the current value from the Resource Store and compares it with the client's value. A matching request may update the resource and create a new ETag. A request with an older value returns 409 Conflict or 412 Precondition Failed.

The rejected client must obtain the latest resource state before preparing another update. It should use a new Idempotency-Key when the requested change is different. Reusing the old key is correct only for the same logical request.

Authentication, authorization, validation, error mapping, and audit logging stay unchanged. The benefit is that one client cannot silently overwrite another client's work. The downside is that frequently updated resources may produce more conflicts and client retries.

How would you add a new request field without breaking existing clients?

I would keep /v1/resources unchanged and add the field as optional. The Validation and Contract Compatibility component would accept requests that do not include it. The Resource Service would use a safe default when the field is missing. New clients could include the field in their POST, PUT, or PATCH JSON body.

I would not remove an existing field, change its type, or change its meaning inside /v1. Those are breaking changes and should use a new API version. The same authentication, authorization, idempotency, ETag checks, response codes, and application/problem+json error contract would remain in place.

The benefit is that old and new clients can use the API together. The downside is that the service must support more than one request shape and test both behaviors. We accept that cost because gradual client upgrades are safer than forcing every client to change at once.

28. Design a centralized logging system.System DesignMediumApple

Question Details

Design a centralized logging system for many services. Explain log ingestion, producers, collectors, buffering, indexing, querying, retention, access control, alerting, failure handling, and tradeoffs for latency, cost, and reliability.

Short Interview Answer (30-60 seconds)

At a high level, this system collects logs from many services and makes recent logs searchable in one place. The main challenge is handling traffic spikes and failures without losing logs. I would explain three flows: log ingestion and indexing, secure search, and background retention and alerting. Collectors send logs through parsing, durable buffering, and indexing into the Hot Search Index. Failed work uses retries or the DLQ. The trade-off is better reliability with some indexing delay and storage cost.

Detailed Explanation

The goal is to collect logs from many services and make them useful for debugging and monitoring. The difficult part is accepting logs reliably while keeping searches fast. The diagram solves this by separating ingestion and processing from secure search, retention, alerting, failure handling, and monitoring.

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 a centralized logging system. diagram
How to Explain It in an Interview
1. Explain the goal and the main paths

I would begin by saying that every service produces logs. These logs help engineers understand errors and system behavior.

The design has two main paths. The first path collects and indexes logs. The second path lets authorized users search those indexed logs.

Several side paths handle retention, alerts, failures, and monitoring. These paths should not block the main log flow.

2. Explain log ingestion and processing

Services first ship logs to Log Collectors. The collectors can send logs in batches or as a stream.

The Ingestion component accepts the incoming records. Parse + Enrich then validates each record, parses its fields, and adds useful context.

The next step is the Buffering Queue. A queue is a waiting line that stores work until the next component is ready. This durable buffer helps protect logs during short traffic spikes or slower indexing.

The Indexer consumes records from the queue. It prepares them for search and writes indexed logs into the Hot Search Index.

The Hot Search Index keeps recent logs ready for fast searching. The diagram does not show another primary log database, so this design treats the index as the searchable recent-log store.

3. Explain secure querying

For the query path, a user starts with the Search UI. The request then goes through Access Control.

Access Control checks whether the user may search the requested logs. Only an authorized request reaches the Query API.

The Query API searches the Hot Search Index and returns matching results. This prevents users from reaching the index directly. It also keeps authorization separate from search execution.

4. Explain retention and alerting

The Hot Search Index sends older-log work to the Retention Manager. The manager applies the retention policy and archives older logs in Archive Storage.

This keeps the hot index focused on recent data. It also lowers the cost of long-term storage.

The Hot Search Index also feeds Alerting Rules. These rules look for matching alert conditions. Notification Channels then send the alerts to their destinations.

5. Explain failures and retries

If Ingestion fails, the failed work goes to Retry with Backoff. Backoff means waiting longer between repeated attempts.

If those retries are exhausted, the record moves to the DLQ. A DLQ is a dead-letter queue that keeps failed records for later review.

The diagram shows index failures going directly to the DLQ. This means failed indexing records remain visible instead of disappearing silently.

6. Explain monitoring and trade-offs

Observability & Monitoring receives signals from Ingestion, the Buffering Queue, the Query API, and Alerting Rules. These signals help operators find delays, failures, and unhealthy components.

The latency trade-off comes from buffering and indexing. Better durability may cause a small delay before logs appear in search.

The cost trade-off comes from retention. Keeping fewer logs in the Hot Search Index lowers cost, but older logs move to Archive Storage.

The reliability benefit comes from collectors, durable buffering, retries, and the DLQ. The downside is more components to operate and monitor.

Engineering Considerations / Design Trade-offs

The benefit is that Services do not wait for indexing to finish. Log Collectors and the Buffering Queue absorb short traffic spikes. This reduces the chance of losing logs. The downside is that new logs may appear in search after a small delay. The Hot Search Index makes recent searches fast, but hot storage costs more. Archive Storage lowers long-term cost, but older logs are no longer kept in the hot index. Retries help temporary ingestion failures. The DLQ protects records that still fail, but engineers must review and handle those records later.

Why Interviewers Ask This

Interviewers use this question to test how you divide a large system into clear flows. They want to see whether you understand producers, collectors, buffering, indexing, secure querying, retention, alerts, and failure handling. They also check whether you can explain why background work should not block ingestion. Most importantly, they want clear judgment about latency, storage cost, and reliability.

Interviewer may ask next
What would you change if log traffic became much larger during a production outage?

I would keep the same architecture, but the Buffering Queue would become even more important. Log Collectors should continue accepting logs and sending them to Ingestion. The queue would hold extra records while the Indexer catches up.

Observability & Monitoring should watch queue growth and indexing delay. A growing queue means logs are arriving faster than they are being indexed. Search would still work for records already inside the Hot Search Index, but the newest logs could appear later.

Ingestion failures would continue using Retry with Backoff. Exhausted retries would move to the DLQ. Index failures would still go directly to the DLQ, matching the diagram.

This change protects reliability during the spike. The main downside is higher temporary storage use and slower search freshness.

How would you stop one team from reading another team’s logs?

I would keep the same search path and enforce the rule in Access Control. The Search UI would still send the user query through Access Control before it reaches the Query API.

Access Control would check which services or log groups the user may view. It would pass only an authorized request to the Query API. The Query API would then search the Hot Search Index within that allowed scope.

This keeps security outside the index itself. It also prevents users from sending direct searches to the Hot Search Index.

Observability & Monitoring should record denied and unusual query activity through the existing query monitoring path. The diagram does not show a separate audit service, so I would not add one.

The main downside is extra checking on every query. This may add a small amount of search latency.

29. Design an object-oriented hotel room booking system.System DesignMediumApple

Question Details

Design a hotel room booking system with rooms, room types, reservations, availability checks, check-in and check-out, cancellations, pricing, payment state, concurrency around inventory, and failure cases.

Short Interview Answer (30-60 seconds)

At a high level, this system lets guests and front-desk staff search rooms, reserve inventory, pay, cancel, and manage each stay. The main challenge is stopping two users from booking the same room inventory. I would explain three flows: search and pricing, reservation or cancellation, and check-in or check-out. The Reservation API routes each request through the Application Core. Inventory Hold + Lock protects availability changes. The main trade-off is extra locking work in exchange for correct bookings.

Detailed Explanation

The goal is to manage the full hotel booking lifecycle. A guest must find an available room type, receive a price, make a reservation, pay, check in, and check out. Front-desk staff use the same system for in-person work. The hardest problem is inventory correctness. Two users must not reserve the same remaining inventory. The diagram handles this with one Reservation API, an Application Core, Inventory Hold + Lock, shared domain objects, and four focused data stores.

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 object-oriented hotel room booking system. diagram
How to Explain It in an Interview
1. Explain the entry point and main objects

I would start with the two Booking Channels. Web / Mobile serves guests, while Front Desk serves hotel staff. Both send requests to the Reservation API.

The Reservation API routes work into the Application Core. The core contains the Availability Checker, Pricing Engine, Reservation Manager, Cancellation Manager, and Stay Manager.

The Domain Objects describe the hotel business. They include Guest, Room Type, Room, Reservation, Payment, and Stay Record. A reservation links the requested room type, assigned room, payment, and stay information.

2. Explain search and pricing

For a search, the Reservation API calls the Availability Checker. It reads room details from the Room Catalog. It reads current inventory from the Availability Ledger.

The Pricing Engine creates the price quote. The diagram keeps pricing inside the Application Core. It does not show a separate pricing database or external pricing service.

The search result tells the user which room types are available. It also provides the price needed before booking.

3. Explain reservation and cancellation

For a booking, the Reservation API calls the Reservation Manager. The manager asks Inventory Hold + Lock to hold room-type inventory.

The lock performs an atomic update on the Availability Ledger. Atomic means the inventory change completes as one safe step. This prevents two requests from taking the same final inventory.

After the hold succeeds, the Reservation Manager saves the booking in the Reservation Store. The Application Core also uses the Payment Gateway to authorize payment. The gateway returns the payment result and saves it in the Payment Store.

For cancellation, the Cancellation Manager marks the reservation cancelled. It releases the hold or restores inventory through Inventory Hold + Lock. It may also request a refund through the Payment Gateway.

4. Explain check-in and check-out

For arrival, the Reservation API calls the Stay Manager. The manager assigns the actual Room and updates the reservation status.

It changes the Availability Ledger so the room is occupied. It also records the stay information represented by Stay Record.

For departure, the Stay Manager closes the stay and releases the room. The reservation then moves to the checked-out state. The visible reservation states are pending, confirmed, cancelled, checked-in, and checked-out.

5. Explain failures and the main trade-off

The main failure is double-booking. Inventory Hold + Lock prevents this by controlling every inventory change.

Other visible failures include payment decline, an expired hold, and cancellation after checkout. A declined payment should not leave inventory blocked. An expired hold should return inventory to the Availability Ledger.

The benefit is correct room inventory. The downside is more coordination around holds, payments, and reservation states. This added work is reasonable because selling the same room twice would create a serious customer problem.

Engineering Considerations / Design Trade-offs

The benefit is clear responsibility. The Availability Checker reads room and inventory data. The Reservation Manager creates bookings. The Cancellation Manager restores inventory. The Stay Manager handles arrival and departure. Inventory Hold + Lock prevents double-booking. The downside is that one booking needs several connected steps. A hold may expire before payment finishes. A payment may fail after inventory was held. A cancellation may require both an inventory update and a refund. These steps make the system harder to build, but they keep room counts and reservation states correct.

Why Interviewers Ask This

Interviewers use this question to test how you turn real business rules into objects and flows. They want to see whether you separate search, booking, cancellation, payment, and stay management clearly. They also check whether you notice concurrency, which means several requests changing inventory together. Strong answers explain correctness, failure handling, and trade-offs without adding unnecessary parts.

Interviewer may ask next
How would you handle many users trying to book the last available room type at the same time?

I would keep the same design and make Inventory Hold + Lock the required gate for every booking attempt. The Reservation Manager must not save a confirmed reservation before the inventory hold succeeds.

Each request would try one atomic update against the Availability Ledger. Atomic means the availability check and inventory reduction happen as one safe operation. Only one request can successfully take the last available inventory. The other requests receive a no-availability result.

Those losing requests must stop before payment authorization and reservation confirmation. This keeps the Availability Ledger and Reservation Store consistent. It also prevents the system from accepting money for inventory it cannot provide.

The main downside is contention. Contention means many requests are waiting to change the same inventory. Booking may become slower during high demand. That delay is acceptable because preventing double-booking is more important than confirming every request immediately.

What should happen when payment is declined after room inventory has already been held?

I would keep the reservation pending until the Payment Gateway returns a successful result. The held inventory should not become a confirmed booking before payment succeeds.

When payment is declined, the Application Core records the failed result in the Payment Store. The Reservation Manager keeps the reservation unconfirmed or marks the attempt as failed. It then asks Inventory Hold + Lock to release the hold.

The lock performs an atomic update on the Availability Ledger. This makes the room-type inventory available for another guest. No check-in work begins, and no active Stay Record is created.

A hold timeout protects the system if the payment result never arrives. When the hold expires, the inventory is released through the same lock path.

The main downside is temporary inventory blocking. Another guest cannot use that inventory while payment is being checked. Short hold times reduce this problem, but very short holds may expire during a slow payment.

30. Design an ad click aggregation system.System DesignMediumApple

Question Details

Design a system that ingests ad click events, deduplicates them, stores raw and aggregated data, supports near-real-time reporting, handles late or duplicate events, scales ingestion, and provides reliable metrics.

Short Interview Answer (30-60 seconds)

At a high level, this system turns a large stream of ad clicks into reliable reports. The main challenge is counting each valid click once while still showing fresh metrics quickly. I would explain three flows: ingestion, aggregation, and later correction. A partitioned Event Stream scales ingestion. Deduplication protects the counts, while raw events support replay. The Reporting API reads prepared totals. The trade-off is that late events can update an earlier report.

Detailed Explanation

The goal is to accept many ad click events and produce useful counts quickly. The difficult part is keeping those counts correct. Events may be malformed, duplicated, delayed, or processed again after a failure. The diagram separates the solution into a fast ingestion path, a near-real-time aggregation path, and a repair path that rebuilds totals from saved raw events.

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 click aggregation system. diagram
How to Explain It in an Interview
1. Explain the goal and main idea

I would start by saying that every valid click should be counted once. Reports should also become available soon after clicks arrive.

The design uses a partitioned Event Stream for scale. Partitioning means splitting the stream so several workers can process different parts in parallel.

Raw events are saved for later checks. Prepared metrics are stored separately for fast reporting.

2. Explain the ingestion and validation path

Ad Click Sources send events to the Tracking Endpoint. The endpoint places those events into the Event Stream.

The stream absorbs traffic spikes and separates event producers from processing. Invalid schema or malformed events go from the Event Stream to the Dead Letter Queue. This keeps broken events away from the normal counting path.

Validation + Enrichment checks accepted events and adds needed information. It also saves those events in the Raw Event Store. This raw history supports backfills and later repairs.

3. Explain duplicate and late-event handling

Validated events move into the Deduplication Service. This service checks fingerprints or click IDs in the Dedup State Store.

A fingerprint is a value used to recognize the same click again. If an ID was already seen, that duplicate should not increase the count.

Normal new events continue to the Stream Aggregator. Events with older timestamps also go to the Late Event Handler. The handler identifies affected time windows and sends window corrections to the aggregator.

4. Explain aggregation and reporting

The Stream Aggregator groups clicks into time windows. A time window is a short reporting period, such as one minute.

It keeps active window data in the Aggregation State Store. It writes fresh totals into the Aggregated Metrics Store.

The Reporting API reads those prepared totals. It returns near-real-time metrics to the Reporting Dashboard.

This makes report queries fast. The dashboard does not need to scan every raw click.

5. Explain repair, monitoring, and trade-offs

The Replay / Reconciliation Worker reads saved events from the Raw Event Store. Reconciliation means checking totals again and correcting them.

The worker writes corrected totals into the Aggregated Metrics Store. This path helps after late arrivals or processing mistakes.

Observability & Monitoring receives signals from the Tracking Endpoint, Deduplication Service, Aggregated Metrics Store, and Reporting API. These signals help detect slow ingestion, duplicate-check problems, stale metrics, or reporting failures.

The benefit is fast reporting with duplicate protection and repair support. The downside is that an early total may change after late events are processed. We accept this because the corrected metric is more reliable.

Engineering Considerations / Design Trade-offs

The benefit is that each part has one clear job. The Event Stream absorbs traffic spikes. The Deduplication Service protects counts from repeated clicks. The Aggregated Metrics Store makes reports fast, while the Raw Event Store supports later repair. The downside is more system state to manage. Dedup data and aggregation windows must stay available. Late events can also change totals after a report was first shown. Replay work uses extra processing capacity. We accept this because fresh reports are useful, while saved raw events let the system produce more reliable final numbers.

Why Interviewers Ask This

Interviewers ask this to test how you break a streaming problem into clear flows. They want to see how you scale ingestion, reject bad events, prevent duplicate counts, and handle late data. They also check whether you separate raw storage from reporting storage and explain why fresh metrics may later be corrected.

Interviewer may ask next
How would the design change if click traffic became ten times larger?

I would keep the same architecture and increase parallel work around the Event Stream. The stream is already partitioned, so more partitions can spread events across more processing workers.

The Deduplication Service and Stream Aggregator would run as multiple instances. Events for the same counting group should follow a stable partitioning rule. This keeps related dedup state and aggregation state together.

The Raw Event Store, Dedup State Store, Aggregation State Store, and Aggregated Metrics Store must also handle the higher load. Their data can be divided using the same stable routing idea.

Correctness stays the same. Every event still passes through duplicate checking before it affects an aggregate. Raw events remain available for replay.

The main downside is uneven traffic. One very popular ad may create a busy partition, while other partitions remain quiet. The routing rule and partition count must therefore be reviewed as traffic grows.

What happens if many click events arrive several minutes late?

I would keep the normal ingestion path unchanged. Late events would still pass through the Deduplication Service, so repeated events would not be counted twice.

Events with older timestamps would go to the Late Event Handler. It would identify the affected windows and send window corrections to the Stream Aggregator. The aggregator would then update the Aggregated Metrics Store.

For a large correction, the Replay / Reconciliation Worker could read the Raw Event Store. It would rebuild the affected totals and write corrected values into the metrics store.

The Reporting API would continue reading the latest stored totals. This keeps the reporting path simple and fast.

Correctness comes from two places. The Dedup State Store prevents repeated counting, and the Raw Event Store preserves the original event history. The downside is that users may first see a fresh number and later see that number change.

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.