30 Amazon JavaScript Frontend Developer Interview Questions & Answers

amazon icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. Find the largest square whose sum does not exceed a threshold.CodingHardAmazon

Question Details

Implement maxSquareSide(matrix, threshold). matrix is a rectangular array with 1 through 500 rows and columns; every entry and threshold is a non-negative safe integer. Return the greatest side length k for which at least one contiguous k × k submatrix has a sum less than or equal to threshold; return 0 if no 1 × 1 square qualifies. Do not mutate the matrix, and reject ragged rows or non-integer values. The contract permits duplicate values and requires exact integer arithmetic within the safe-integer range. Example: for [[1,1,3,2,4,3,2],[1,1,3,2,4,3,2],[1,1,3,2,4,3,2]] with threshold 4, return 2. Aim for O(rows × columns × log(min(rows,columns))) time or better using standard JavaScript.

Short Interview Answer (30-60 seconds)

I would build a 2D prefix-sum table with BigInt so every tested square sum is exact. Then I binary-search the side length k from 1 to min(rows, columns). For each k, I scan all possible k × k squares and get each sum in O(1) from the prefix table. Because all values are non-negative, feasibility is monotone, so binary search is valid. The total time is O(rows × columns × log(min(rows, columns))) with O(rows × columns) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We need to find the largest square inside the matrix whose total is at most the threshold. The square must use consecutive rows and columns. We return only its side length. We do not change the input matrix. I first build a table that lets me get any square sum quickly. Then I binary-search the possible side lengths. This works because every matrix value is non-negative, so if a larger square works, a smaller square inside it also works.

Useful Questions to Ask the Interviewer
  1. Should I reject rows with different lengths? Yes. The contract says the matrix must be rectangular.
  2. Can matrix values or the threshold be negative? No. They are non-negative safe integers.
  3. Should the sum calculations remain exact even when many safe integers are added together? Yes. The shown solution uses BigInt for the prefix sums.
Find the largest square whose sum does not exceed a threshold. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a rectangular matrix with 1 through 500 rows and 1 through 500 columns, plus a non-negative threshold. Every matrix entry and the threshold is a safe integer. We return the greatest side length k for which at least one contiguous k × k square has sum at most the threshold. If no 1 × 1 square qualifies, we return 0. Ragged rows and non-integer values are rejected.

2. Build the 2D prefix-sum table

I create a prefix table P with one extra row and one extra column. P[r][c] stores the sum of the rectangle from matrix[0][0] through matrix[r-1][c-1]. The table uses BigInt so the sums stay exact. The original matrix is never modified.

For the example matrix [[1,1,3,2,4,3,2],[1,1,3,2,4,3,2],[1,1,3,2,4,3,2]], the prefix table is: 0 0 0 0 0 0 0 0 0 1 2 5 7 11 14 16 0 2 4 10 14 22 28 32 0 3 6 15 21 33 42 48

3. Binary-search the side length

The possible side lengths are 1 through min(rows, columns). In the example, that is 1 through 3. I keep low, high, and best. For each midpoint k, I call hasSquare(k), which checks whether any k × k square has sum at most the threshold.

The key property is monotonicity. If a k × k square is feasible, any smaller square contained inside it has a sum no larger because all entries are non-negative. So after a feasible k, I can search larger sizes. After an infeasible k, I can discard all larger sizes.

4. Walk through the example

Start with low = 1, high = 3, and best = 0. The midpoint is k = 2. The first 2 × 2 square starts at (0,0). Its sum is P[2][2] - P[0][2] - P[2][0] + P[0][0] = 4 - 0 - 0 + 0 = 4. Since 4 ≤ 4, hasSquare(2) returns true immediately. We set best = 2 and low = 3.

Now k = 3. The five possible 3 × 3 squares start at (0,0), (0,1), (0,2), (0,3), and (0,4). Their sums are 15, 18, 27, 27, and 27. None is at most 4, so hasSquare(3) returns false. We set high = 2. Now low = 3 and high = 2, so the binary search stops and returns best = 2.

5. Explain why the result is correct

The prefix table gives the exact sum of every square that hasSquare(k) tests. The helper returns true exactly when at least one square of side k meets the threshold. Feasibility is monotone because all entries are non-negative. The binary search keeps best as the largest feasible size found so far and removes only sizes that cannot contain a better answer. Therefore the final best is the greatest valid side length.

6. Explain the JavaScript implementation

The code validates the matrix dimensions, rectangular shape, entries, and threshold. It then builds the BigInt prefix table. The helper hasSquare(k) scans every possible k × k square and returns true as soon as one qualifies. The outer binary search records k and moves right after success. It moves left after failure. When low becomes greater than high, the function returns best.

7. Explain complexity and edge cases

Building the prefix table takes O(rows × columns) time. One feasibility test checks at most O(rows × columns) positions, and each square sum takes O(1). Binary search performs O(log(min(rows, columns))) feasibility tests. Total time is O(rows × columns × log(min(rows, columns))). Auxiliary space is O(rows × columns). Important cases are no qualifying 1 × 1 cell, all zeros, one row or one column, duplicate values, ragged rows, and non-integer values.

Key Insight / Why This Solution Works

The key insight is to combine 2D prefix sums with binary search on the answer. The prefix table makes each k × k sum an O(1) inclusion-exclusion calculation. The central invariant is that best is the largest feasible side length found so far, and the remaining binary-search interval contains the only sizes that could still change the answer. Binary search is safe because all entries are non-negative. If a square of side k is feasible, every smaller square contained inside it is also feasible. If k is infeasible, every larger size is infeasible too.

Code
function maxSquareSide(matrix, threshold) {
  // Validate the number of rows before reading matrix[0].
  if (!Array.isArray(matrix) || matrix.length < 1 || matrix.length > 500) {
    throw new TypeError('matrix must have 1 through 500 rows');
  }

  // Validate the threshold from the problem contract.
  if (!Number.isSafeInteger(threshold) || threshold < 0) {
    throw new TypeError('threshold must be a non-negative safe integer');
  }

  // Validate the first row and establish the required column count.
  if (!Array.isArray(matrix[0]) || matrix[0].length < 1 || matrix[0].length > 500) {
    throw new TypeError('matrix must have 1 through 500 columns');
  }

  const rows = matrix.length;
  const cols = matrix[0].length;

  // Reject ragged rows and invalid values without mutating the matrix.
  for (const row of matrix) {
    if (!Array.isArray(row) || row.length !== cols) {
      throw new TypeError('matrix must be rectangular');
    }

    for (const value of row) {
      if (!Number.isSafeInteger(value) || value < 0) {
        throw new TypeError('matrix values must be non-negative safe integers');
      }
    }
  }

  // Use BigInt prefix sums so all accumulated sums remain exact.
  // The extra zero row and zero column simplify inclusion-exclusion.
  const prefix = Array.from({ length: rows + 1 }, () => Array(cols + 1).fill(0n));

  // Build the 2D prefix table without changing matrix.
  for (let r = 1; r <= rows; r++) {
    let rowSum = 0n;

    for (let c = 1; c <= cols; c++) {
      rowSum += BigInt(matrix[r - 1][c - 1]);
      prefix[r][c] = prefix[r - 1][c] + rowSum;
    }
  }

  // Convert once so every sum comparison is BigInt-to-BigInt.
  const limit = BigInt(threshold);

  // Test whether at least one k × k square satisfies the threshold.
  function hasSquare(k) {
    for (let r = k; r <= rows; r++) {
      for (let c = k; c <= cols; c++) {
        // Inclusion-exclusion gives this square sum in O(1).
        const sum = prefix[r][c] - prefix[r - k][c] - prefix[r][c - k] + prefix[r - k][c - k];

        // Stop immediately after the first qualifying square.
        if (sum <= limit) {
          return true;
        }
      }
    }

    return false;
  }

  // Binary-search the inclusive side-length range.
  let low = 1;
  let high = Math.min(rows, cols);
  let best = 0;

  while (low <= high) {
    const mid = low + Math.floor((high - low) / 2);

    if (hasSquare(mid)) {
      // mid is feasible. Record it and try a larger size.
      best = mid;
      low = mid + 1;
    } else {
      // mid is infeasible. By monotonicity, discard all larger sizes.
      high = mid - 1;
    }
  }

  // If no 1 × 1 square qualifies, best is still 0.
  return best;
}

// Run the same example shown in the diagram.
const matrix = [
  [1, 1, 3, 2, 4, 3, 2],
  [1, 1, 3, 2, 4, 3, 2],
  [1, 1, 3, 2, 4, 3, 2],
];
const threshold = 4;

console.log(maxSquareSide(matrix, threshold)); // 2
Time & Space Complexity

Let R be the number of rows and C be the number of columns. Building the prefix table takes O(R × C) time. For one candidate size k, hasSquare(k) may inspect O(R × C) positions, and each square sum takes O(1) time. Binary search tries O(log(min(R, C))) side lengths. So total time is O(R × C × log(min(R, C))). The prefix table stores O(R × C) BigInt values, so auxiliary space is O(R × C).

Where it is used

This pattern is useful when a program needs many fast sum queries on rectangular grid areas. Examples include image-processing grids, heatmaps, game maps, and analytics tables. A 2D prefix sum makes each region-sum query fast. Binary search can be added when the answer is a size and the valid sizes form a monotone true-or-false range.

Why Interviewers Ask This

This problem checks whether you can combine two ideas correctly. You need 2D prefix sums for fast square-sum queries. You also need to recognize that non-negative values make feasibility monotone, which allows binary search on the side length. The interviewer can evaluate boundary handling, early-return reasoning, exact JavaScript arithmetic with BigInt, input validation, and whether you can state the O(R × C × log(min(R, C))) time and O(R × C) auxiliary space correctly.

Common interview mistakes
  1. Using Number for accumulated prefix sums and forgetting that adding many safe integers can exceed the exact Number integer range. The shown solution uses BigInt for accumulated sums.
  2. Using the wrong plus and minus signs in the prefix-sum inclusion-exclusion formula.
  3. Continuing to process more squares after hasSquare(k) already found a qualifying square, even though the implementation returns immediately.
  4. Using binary search without explaining why non-negative entries make feasibility monotone.
  5. Forgetting to reject ragged rows or non-integer values.
  6. Claiming O(1) auxiliary space even though the prefix table grows with the matrix.
Interview tip

Explain the monotone property before writing the binary search. Say that because every value is non-negative, a feasible k × k square contains smaller squares whose sums cannot be larger. That directly explains why searching the side length with binary search is safe.

Interviewer may ask next
Why is binary search on the side length valid here?

It is valid because every matrix entry is non-negative. If one k × k square has sum at most the threshold, any smaller square contained inside it has a sum no larger. So feasible sizes form a monotone range. If k works, we can search larger sizes. If k does not work, every larger size also fails. The time remains O(R × C × log(min(R, C))) and the auxiliary space remains O(R × C).

What changes if matrix values can be negative?

The current binary search is no longer valid because feasibility may stop being monotone. A larger square can include negative values and have a smaller sum than one of its contained smaller squares. The 2D prefix table is still useful because it keeps each square-sum query at O(1). A direct version can test every possible side length, which takes O(R × C × min(R, C)) time with O(R × C) auxiliary space. The tradeoff is higher running time because we can no longer discard half of the side lengths.

2. Determine whether a bracket string is valid.CodingEasyAmazon

Question Details

Write isValidBrackets(text). text is a string of length 0 through 100,000 containing only (, ), [, ], {, and }. Return true exactly when every opening bracket is closed by the same bracket type in properly nested order and no closing bracket appears without a matching opener. The empty string is valid. Treat JavaScript strings as sequences of these ASCII code units; Unicode handling is otherwise out of scope. Do not modify the input or use a parser library. Examples: isValidBrackets('([]{})') returns true, isValidBrackets('(]') returns false, and isValidBrackets('([)]') returns false. The required target is O(n) time and O(n) worst-case auxiliary space.

Short Interview Answer (30-60 seconds)

I would use a stack to track opening brackets that still need a match. I process the string from left to right. When I see an opening bracket, I push it. When I see a closing bracket, I make sure the stack is not empty, pop the most recent opener, and verify that the types match. Any failure returns false immediately. At the end, the stack must be empty. This takes O(n) expected time and O(n) worst-case auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a string that contains only parentheses, square brackets, and curly brackets. We return true only when every opening bracket has the correct closing bracket and the brackets close in the correct nested order. A closing bracket cannot appear without its matching opener. The empty string is valid. A stack fits this problem because the most recently opened bracket must be the first one closed.

Useful Questions to Ask the Interviewer
  1. Can I assume the input contains only (, ), [, ], {, and } as stated?
  2. Should the empty string return true? The stated contract says yes.
Determine whether a bracket string is valid. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives text. Its length is from 0 through 100,000. Every character is one of the six allowed ASCII bracket characters. We return a Boolean. We return true only when every opening bracket is closed by the same type in properly nested order. We return false when a closing bracket has no opener, has the wrong opener, or when an opening bracket remains unmatched at the end.

2. Choose the algorithm and data structure

Use a stack. Each stack entry is an opening bracket that has not been matched yet. The top of the stack is the most recent unmatched opener. That is the only opener the current closing bracket is allowed to match. The implementation also uses a small Set containing (, [, and {, plus a closing-to-opening lookup: ) maps to (, ] maps to [, and } maps to {.

3. Initialize the state

Start with an empty stack. An empty stack means there are no unmatched opening brackets yet. Then process the string from left to right. Push opening brackets. For a closing bracket, first verify that the stack is not empty. Then pop the top opener and compare its type with the opener required by the closing bracket.

4. Walk through the verified example

The diagram uses text = "([{}])", which has length 6.

At index 0, the character is (. Push it. The stack becomes ['('].

At index 1, the character is [. Push it. The stack becomes ['(', '['].

At index 2, the character is ]. The stack is not empty. Pop [. It matches the opener required by ]. The stack becomes ['('].

At index 3, the character is {. Push it. The stack becomes ['(', '{'].

At index 4, the character is }. Pop {. It matches the opener required by }. The stack becomes ['('].

At index 5, the character is ). Pop (. It matches the opener required by ). The stack becomes empty.

All six characters have been processed. The stack is empty, so the function returns true.

5. Explain why the result is correct

The invariant is that after each processed character, the stack contains exactly the opening brackets that have not been matched yet, in their nesting order. Because a stack is last in, first out, its top is always the most recent unmatched opener. Therefore, every closing bracket must match that top value. A mismatch or an empty stack proves the string is invalid immediately. If the stack is empty at the end, every opener was matched correctly.

6. Explain the JavaScript implementation

The code creates an empty array as the stack. It creates a Set for the three opening brackets and a lookup from each closer to its matching opener. The loop processes each character from left to right. An opener is pushed. For a closer, the code checks for an empty stack before popping. It then pops the most recent opener and compares it with the required opener. Any failure returns false immediately. After the loop, the code returns whether the stack is empty.

7. Explain complexity and edge cases

We process each character at most once. Set membership is O(1) on average, and stack push and pop are constant-time operations in the normal dense-array case, so the expected running time is O(n). The stack can hold up to n opening brackets, so the worst-case auxiliary space is O(n). Important cases are the empty string, a closing bracket with no opener, mismatched types such as (], incorrect nesting such as ([)], and inputs containing only opening brackets.

Key Insight / Why This Solution Works

The key insight is that valid brackets follow last-in, first-out order. The most recently opened bracket must be the next one closed. A stack models this rule directly. Each stack entry represents an opening bracket that is still unmatched. The central invariant is: after processing any prefix of the string, the stack contains exactly the unmatched opening brackets from that prefix in nesting order. When a closing bracket appears, it must match the stack top. If it does not, the string is invalid. If the stack is empty after the full input, every opener was matched.

Code
function isValidBrackets(text) {
  // Store opening brackets that have not been matched yet.
  const stack = [];

  // Recognize the three possible opening bracket characters.
  const open = new Set(['(', '[', '{']);

  // Map each closing bracket to the exact opener it must match.
  const match = {
    ')': '(',
    ']': '[',
    '}': '{',
  };

  // Process each input character from left to right at most once.
  for (const ch of text) {
    if (open.has(ch)) {
      // This opener is now the most recent unmatched bracket.
      stack.push(ch);
    } else {
      // A closing bracket is invalid when there is no opener to match it.
      if (stack.length === 0) {
        return false;
      }

      // LIFO order means the newest unmatched opener must close first.
      const top = stack.pop();

      // Reject the string when the bracket type does not match.
      if (top !== match[ch]) {
        return false;
      }
    }
  }

  // Any opening bracket still on the stack is unmatched.
  // This also makes the empty string valid because its stack stays empty.
  return stack.length === 0;
}

// Run the same verified example shown in the diagram.
console.log(isValidBrackets('([{}])')); // true
Time & Space Complexity

Let n be the number of characters in text. We process the input at most once. Checking the three-item JavaScript Set is O(1) on average. Each opening bracket is pushed once, and each matched opening bracket is popped once. Therefore, the expected time is O(n). Auxiliary space means extra memory used by the algorithm. In the worst case, such as a string containing only opening brackets, the stack can hold n entries. Therefore, worst-case auxiliary space is O(n). The Set and matching lookup contain only three bracket types, so their sizes are constant.

Where it is used

This stack pattern is useful when things must finish in reverse order from how they started. Practical examples include checking nested delimiters in code editors, validating simple expression syntax, checking nested template structures, and processing other last-opened-first-closed data.

Why Interviewers Ask This

This problem tests whether you recognize a last-in, first-out pattern and choose a stack naturally. It also checks whether you validate both bracket type and nesting order, handle early failure safely, and remember the final unmatched-opener check. For JavaScript, it also tests whether you can use an array correctly as a stack, write simple control flow, explain the invariant, and state the O(n) expected-time and O(n) worst-case auxiliary-space costs accurately.

Common interview mistakes

One mistake is checking only whether the numbers of opening and closing brackets are equal. Equal counts do not guarantee correct nesting. Another mistake is matching a closer with any earlier opener instead of the top of the stack. Candidates may also pop without first checking whether the stack is empty. Another common error is forgetting the final empty-stack check, which would accept leftover opening brackets. It is also incorrect to claim O(1) auxiliary space because the stack can grow to O(n).

Interview tip

Explain the invariant before writing the loop: the stack contains exactly the unmatched opening brackets, and the top is the only opener that the current closing bracket may match. Then make every branch in the code maintain that rule.

Interviewer may ask next
How would the solution change if the bracket characters arrived as a stream instead of one complete string?

Keep the same stack between chunks and process characters in arrival order. Push every opening bracket. For a closing bracket, reject immediately if the stack is empty or if the popped opener has the wrong type. Do not decide that the stream is valid until the stream is finished. At that point, return true only if the stack is empty. The invariant stays the same. Expected time remains O(n) for n total characters, and worst-case auxiliary space remains O(n). The tradeoff is that unmatched openers must stay in memory until later data arrives.

Can the worst-case auxiliary space be reduced below O(n) for arbitrary bracket strings?

Not with this general stack method while supporting arbitrary nesting depth and all three bracket types. A prefix can contain n unmatched opening brackets, and their types and order may be needed to validate future closing brackets. The stack therefore needs O(n) space in the worst case. The expected running time remains O(n). Reducing the memory bound would require additional restrictions on the input, such as a fixed maximum nesting depth, which changes the problem's assumptions.

3. Return an array concatenated with a second copy of itself.CodingEasyAmazon

Question Details

Implement duplicate(values) in modern JavaScript. values is an owned array containing arbitrary JavaScript values, may be empty, and may contain duplicate object references. Return a new array containing every element of values in original order followed immediately by the same sequence again. Preserve each element by reference rather than deep-cloning it, and do not mutate the input. Sparse holes, when present, must remain holes in both corresponding positions of the result rather than becoming explicit undefined values. Inputs other than arrays are outside the contract and must throw TypeError. Do not use an external library. Example: duplicate([1, 'x', null]) must return [1, 'x', null, 1, 'x', null]; duplicate([]) returns []. The implementation should run in O(n) time and allocate O(n) result space.

Short Interview Answer (30-60 seconds)

I would first validate that the input is an array. Then I create a new array with twice the original length. I visit each source index from left to right. If that index exists, I copy the same value reference into positions i and i + n. If the source has a hole, I leave both result positions untouched, so they stay holes. This does not mutate the input. The time complexity is O(n), and the returned result uses O(n) space.

Detailed Explanation

See the Code while reading this explanation.

The goal is to return a new array that contains the input sequence two times in the same order. The original array must stay unchanged. Values such as objects are copied as the same references, not cloned. Empty arrays must work. Sparse arrays also need special care because an empty slot must stay an empty slot instead of becoming an explicit undefined value. The solution creates a new array twice as long and copies only the source positions that really exist.

Useful Questions to Ask the Interviewer
  1. Should sparse holes remain holes instead of becoming explicit undefined values?
  2. Should object values keep the same references rather than being deep-cloned?
  3. Should a non-array input throw TypeError?
Return an array concatenated with a second copy of itself. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an owned JavaScript array called values. It may be empty. It may contain arbitrary JavaScript values, including repeated references to the same object. The result must be a new array. Its first half must match values, and its second half must repeat the same sequence. The input must not be changed. If values is not an array, the function throws TypeError.

2. Choose the algorithm and state

Let n be values.length. Create result with length 2 * n. A new Array(2 * n) starts with holes, which is useful for sparse input. Then visit source indices from 0 through n - 1. For each index i, check whether i exists in values. If it exists, write the same value reference to result[i] and result[i + n]. If it does not exist, perform no write, so both result positions remain holes.

The central rule is simple: after processing an index i, every existing source position processed so far has been copied to the matching position in both halves, while every processed source hole is still a hole in both halves.

3. Initialize the state

For the diagram example, values is [1, 'x', null]. Its length is n = 3. We create result = new Array(6). At this point, all six result positions are holes. Traversal starts at i = 0.

4. Walk through the example

At i = 0, index 0 exists and its value is 1. We write 1 to result[0] and result[3].

At i = 1, index 1 exists and its value is 'x'. We write the same value to result[1] and result[4].

At i = 2, index 2 exists and its value is null. We write null to result[2] and result[5].

After the loop, result is [1, 'x', null, 1, 'x', null]. The function returns this new array.

5. Explain why the result is correct

Every existing source element at index i is written to exactly two required positions: i in the first half and i + n in the second half. This keeps the original order and keeps object values as the same references. A source hole causes no assignment because the code checks i in values first. Since the result array starts with holes, both matching positions stay holes. The input is only read, so it is not mutated.

6. Explain the JavaScript implementation

The function first uses Array.isArray to validate the contract. It stores values.length in n and allocates new Array(n * 2). A for loop visits each possible source index. The condition i in values distinguishes a real array entry from a sparse hole. Existing values are assigned to both corresponding positions. Finally, the function returns result.

7. Explain complexity and edge cases

The loop considers n indices, so the time complexity is O(n). The returned array has length 2n, so the result space is O(n). The working variables use O(1) additional space beyond the required result. An empty array returns an empty array. Sparse holes remain holes. Duplicate object references stay references to the same objects. Non-array inputs throw TypeError.

Key Insight / Why This Solution Works

The key insight is to allocate the final 2n-length array first and write each existing source position directly into both halves. This avoids changing the input and preserves the original order. The invariant is that after processing index i, every existing source position processed so far appears at the same index in the first half and at index i + n in the second half, while source holes remain holes in both places. The i in values check is essential because reading a hole and assigning that value would create an explicit undefined property instead of preserving the hole.

Code
function duplicate(values) {
  // Validate the input contract before reading array-specific state.
  if (!Array.isArray(values)) {
    throw new TypeError('values must be an array');
  }

  // Save the original length so the second copy starts exactly at index n.
  const n = values.length;

  // Allocate the final size up front. New Array creates holes by default,
  // which lets source holes remain holes when we intentionally skip writes.
  const result = new Array(n * 2);

  // Visit each possible source index exactly once.
  for (let i = 0; i < n; i++) {
    // Only copy a position when that property exists in the source array.
    // Skipping a missing index keeps both matching result positions as holes.
    if (i in values) {
      // Read the existing value once. Objects and functions keep their references.
      const value = values[i];

      // Put the value in the corresponding position of the first copy.
      result[i] = value;

      // Put the same value reference in the corresponding position of the second copy.
      result[i + n] = value;
    }
  }

  // Return a new array. The input array was never modified.
  return result;
}

// Run the same example shown in the diagram.
console.log(duplicate([1, 'x', null]));
// [1, 'x', null, 1, 'x', null]
Time & Space Complexity

Let n be values.length. The loop checks each index from 0 to n - 1 once, so the time complexity is O(n). The returned array has length 2n, so the required result space is O(n). Apart from that returned array, the algorithm keeps only a few variables such as n, i, and the current value, so its additional working space is O(1).

Where it is used

This direct indexed-copy pattern is useful when software needs to build a repeated sequence without changing the original array. The existence check is especially useful when JavaScript sparse arrays must keep their empty slots as true holes instead of converting them into explicit undefined values.

Why Interviewers Ask This

This problem checks whether a candidate can turn a simple array requirement into precise JavaScript behavior. The interviewer can evaluate input validation, immutability, reference preservation, sparse-array semantics, correct index arithmetic, and complexity analysis. The sparse-hole requirement is especially useful because it shows whether the candidate understands that a missing array property is different from an existing property whose value is undefined.

Common interview mistakes

A common mistake is using unconditional assignments such as result[i] = values[i]. For a sparse hole, that creates an explicit undefined property and breaks the requirement. Another mistake is mutating the original array instead of returning a new one. Deep-cloning object values is also wrong because the contract requires the same references. Candidates may forget to reject non-array inputs with TypeError. Finally, claiming O(1) total space is incorrect because the required returned array contains 2n positions and therefore uses O(n) result space.

Interview tip

Call out the sparse-array detail before writing the loop. Explain that i in values lets the code distinguish a real element whose value happens to be undefined from a missing array position, so true holes stay holes in both copies.

Interviewer may ask next
What changes if sparse holes do not need to be preserved?

The loop can use unconditional indexed assignments. For each i from 0 to n - 1, set result[i] = values[i] and result[i + n] = values[i]. A source hole would then become an explicit undefined value in both copies. The algorithm still takes O(n) time and the returned array still uses O(n) space. The tradeoff is that the sparse structure is no longer preserved.

Can we reduce the extra working memory while still returning the duplicated array?

The algorithm already uses O(1) working memory beyond the required result. It only keeps variables such as n, i, and the current value. The result itself must use O(n) space because the required output has length 2n. We cannot remove that output-space cost while still returning a separate duplicated array. The time complexity remains O(n).

4. Find two array indexes whose values add to a target.CodingEasyAmazon

Question Details

Implement twoSum(values, target). values is an array of 2 through 100,000 finite safe integers, and target is a finite safe integer. Exactly one pair of distinct indexes is guaranteed to sum to target; the same array element may not be used twice, while equal values at different indexes are allowed. Return the two zero-based indexes as [smallerIndex, largerIndex]. Do not mutate the input, sort it, or return the values themselves. Inputs outside the stated contract need not be supported. Example: twoSum([2, 7, 11, 15], 9) must return [0, 1]; twoSum([3, 3], 6) must return [0, 1]. Target O(n) time with O(n) auxiliary space in standard JavaScript.

Short Interview Answer (30-60 seconds)

I would use a JavaScript Map that stores each previously seen value and an earlier index. For each element, I compute the complement, which is the target minus the current value. I check for that complement before inserting the current value, so I cannot reuse the same element. If it exists, I return its stored index and the current index. I process each item at most once and stop when the answer is found. This gives O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given an array of numbers and a target number. We need to find two different positions whose values add to the target. We must return the positions, not the values. The smaller position must come first. We cannot sort or change the input. Exactly one valid pair is guaranteed. A good approach is to remember values that we have already processed. Then, for each new value, we check whether the matching value needed to reach the target has already appeared.

Useful Questions to Ask the Interviewer
  1. Should I return the indexes rather than the values? Yes, the required result is [smallerIndex, largerIndex].
  2. Can equal values at different indexes form the pair? Yes. For example, [3, 3] with target 6 returns [0, 1].
  3. Is a valid pair guaranteed? Yes. Exactly one pair of distinct indexes is guaranteed.
Find two array indexes whose values add to a target. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives values and target. values contains 2 through 100,000 finite safe integers. We need two different zero-based indexes whose values add to target. We return the smaller index first. We must not mutate or sort the input, and we return indexes rather than values.

For the diagram example, values = [2, 7, 11, 15] and target = 9. The correct returned pair is [0, 1] because values[0] + values[1] = 2 + 7 = 9.

2. Choose the algorithm and data structure

I use a JavaScript Map. It stores value → earlier index. Before processing index i, the map contains only values from earlier indexes. This is the main invariant.

For each current value, I calculate complement = target - currentValue. Complement means the value needed to reach the target. I check whether that complement is already in the map before inserting the current value. This prevents the current element from being used twice.

3. Initialize the state

I start with const seen = new Map(). The map is empty because no values have been processed yet. Traversal starts at index 0.

4. Walk through the example

At index 0, the current value is 2. The complement is 9 - 2 = 7. The map before this step is {}. The map does not contain 7, so I store 2 → 0. The map becomes {2 → 0} and processing continues.

At index 1, the current value is 7. The complement is 9 - 7 = 2. The map before this step is {2 → 0}. The complement 2 is present at index 0. I return [0, 1] immediately and stop. Indexes 2 and 3 are not processed.

5. Explain why the result is correct

Before index i, the map contains only values from earlier indexes. If target - values[i] is present, its stored index is smaller than i, so the two indexes are distinct. Their values add to the target. Therefore returning [storedIndex, i] gives the required [smallerIndex, largerIndex] order.

6. Explain the JavaScript implementation

The code creates the map and loops from left to right. It calculates the needed complement and checks seen.has(need). If the complement exists, seen.get(need) gives an earlier index and the function returns immediately. Otherwise, it stores the current value and index with seen.set(value, i). The final empty-array return is only a defensive fallback because the problem guarantees a valid pair.

7. Explain complexity and edge cases

JavaScript Map lookup and insertion are O(1) on average. We process the input at most once, so the overall expected time is O(n). The map may grow to O(n) entries, so auxiliary space is O(n). Equal values at different indexes work because lookup happens before insertion. Negative values, zero, and finite safe integers also work with the same subtraction and lookup logic.

Key Insight / Why This Solution Works

The key idea is to turn the search for a pair into a fast lookup. For each current value, compute complement = target - currentValue. If that complement was seen earlier, the required pair has been found. The map stores each previously seen value and an earlier index. The central invariant is that, before processing index i, every map entry comes from an index smaller than i. Checking the complement before storing the current value prevents reuse of the same array element. This is better than checking every pair, which would take O(n²) time.

Code
function twoSum(values, target) {
  // Store each previously processed value and an earlier index where it appeared.
  const seen = new Map();

  // Process the array from left to right and stop as soon as the pair is found.
  for (let i = 0; i < values.length; i++) {
    const value = values[i];

    // Complement means the value needed with the current value to reach target.
    const need = target - value;

    // Check before insertion so the current element cannot match with itself.
    if (seen.has(need)) {
      // The stored index is earlier than i, so the smaller index comes first.
      return [seen.get(need), i];
    }

    // No match yet. Store the current value and its index for later elements.
    seen.set(value, i);
  }

  // Defensive fallback; the stated problem guarantees a solution.
  return [];
}

// Run the same example used in the diagram.
const result = twoSum([2, 7, 11, 15], 9);
console.log(result); // [0, 1]
Time & Space Complexity

Let n be the number of values. We process the array at most once. Each JavaScript Map lookup and insertion is O(1) on average, so the total expected time is O(n). This is an expected bound because hash-based operations are not guaranteed worst-case O(1). The map may store up to n entries, so the auxiliary space is O(n). Auxiliary space means the extra memory used by the algorithm.

Where it is used

This pattern is useful when software processes data from left to right and needs to quickly check whether a matching earlier value exists. Examples include finding complementary values, matching IDs, detecting previously seen values, and building lookup tables while streaming through a collection.

Why Interviewers Ask This

This question checks whether you recognize a hash-map lookup pattern instead of checking every possible pair. It tests whether you preserve original indexes, handle duplicate values such as [3, 3], and understand why lookup must happen before insertion. It also evaluates early-return reasoning, maintaining a simple invariant, writing correct JavaScript with Map, and explaining O(n) expected time and O(n) auxiliary space accurately.

Common interview mistakes

A common mistake is returning the values 2 and 7 instead of their indexes [0, 1]. Another mistake is inserting the current value before checking its complement, which can allow the same array element to be reused. Sorting is also not allowed by this problem and can lose the original index relationship. Candidates may continue processing after the valid pair has already been found instead of returning immediately. Another mistake is claiming guaranteed O(n) time instead of O(n) expected time when the solution depends on average O(1) JavaScript Map operations.

Interview tip

State the invariant before writing the loop: before processing index i, the map contains only values from earlier indexes. Then explain that checking the complement before insertion is what prevents using the same element twice.

Interviewer may ask next
What changes if no valid pair is guaranteed?

The main algorithm can stay the same. We still check the complement before storing the current value. The difference is that the fallback becomes part of the normal contract instead of being defensive. For example, the function could return [] or null after the loop when no pair exists. Correctness is preserved because every current value was checked against all relevant earlier values through the map. Expected time remains O(n), and auxiliary space remains O(n). The main tradeoff is that callers must handle the no-result case.

What changes if all valid index pairs must be returned?

We can no longer stop after the first match. We must process the whole array and collect every valid pair. Because duplicate values can create several pairs, the map should store all earlier indexes for each value instead of only one index. When the complement is found, we add a pair for each matching earlier index, then store the current index. This preserves correctness because every emitted pair uses two distinct indexes. The expected time is O(n + k), where k is the number of returned pairs. Auxiliary working space is O(n), plus O(k) space for the output. The tradeoff is higher memory use and no early return.

5. Implement `Array.prototype.map` behavior in JavaScript.CodingEasyAmazon

Question Details

Implement arrayMap(array, callback, thisArg) in ECMAScript 2026 without calling Array.prototype.map. array is a genuine JavaScript array whose length is at most 100,000; it may be sparse and may contain arbitrary values. Invoke callback(value, index, array) only for indexes that exist when they are visited, bind thisArg as the callback receiver, and return a new array with the same length and holes in the same positions. Do not mutate array; mutations performed by the callback follow ordinary array-iteration rules, including capturing the initial length and skipping properties deleted before visitation. Throw TypeError when array is not an array or callback is not callable. Use standard JavaScript only. Example: arrayMap([1, , 3], (value, index) => value + index) must produce an array of length 3 whose present values are 1 at index 0 and 5 at index 2, with index 1 still absent.

Short Interview Answer (30-60 seconds)

I would validate the array and callback first. Then I capture the array's initial length and create a new array with that same length. I visit indexes from 0 to length minus 1 and use i in array to check whether each index exists at visit time. For existing indexes, I call the callback with thisArg, the value, index, and original array. Missing indexes stay as holes. This takes O(n) time, O(1) auxiliary space, and O(n) output space.

Detailed Explanation

See the Code while reading this explanation.

The task is to build our own version of JavaScript's map behavior without calling the built-in Array.prototype.map. We receive an array, a callback, and an optional thisArg. We must return a different array with the same starting length. Existing positions are transformed by the callback. Missing positions stay missing. We also save the starting length before iteration because the callback is allowed to change the original array while the loop is running.

Useful Questions to Ask the Interviewer
  1. Should sparse-array holes remain holes in the returned array?
  2. Should mutations made by the callback follow normal JavaScript map iteration rules?
  3. Should invalid array or callback inputs throw TypeError?
Implement `Array.prototype.map` behavior in JavaScript. diagram
How to Explain It in an Interview
1. Validate the inputs

First, I check the required inputs. array must be a genuine JavaScript array. callback must be callable. If either check fails, I throw TypeError before starting the mapping work.

2. Capture the initial state

I save array.length in len once before the loop. This is important because the callback may append or delete properties while iteration is running. I create result = new Array(len). This gives the output the required length, but all of its positions initially remain holes.

I also store the callback receiver in T. It is the supplied thisArg when a third argument was provided. Otherwise it is undefined.

3. Visit each index in order

I process indexes from 0 through len - 1. At each index, I test i in array. This checks whether the property exists at the moment that index is visited. If the property does not exist, I do nothing. The same position in result therefore remains a hole.

If the property exists, I read array[i] and call callback.call(T, value, i, array). This passes the current value, current index, and original array while using T as the callback receiver. I store the returned value at result[i].

4. Walk through the verified example

The example input is arrayMap([1, , 3], (value, index) => value + index).

The captured length is 3. The new result is therefore a length-3 array containing three holes.

At index 0, the property exists and its value is 1. The callback computes 1 + 0 = 1, so result[0] = 1.

At index 1, the property does not exist. The callback is skipped, so index 1 remains a hole in the result.

At index 2, the property exists and its value is 3. The callback computes 3 + 2 = 5, so result[2] = 5.

The final result has length 3, value 1 at index 0, a hole at index 1, and value 5 at index 2.

5. Explain why the result is correct

The main invariant is that after each visited index, result contains callback results only at indexes whose input properties existed when they were visited. Any skipped input index remains absent in the output.

Capturing the initial length means later appends do not increase the number of indexes examined. Checking i in array at visit time means a future property deleted by the callback is skipped when the loop eventually reaches that index. The mapping function itself does not mutate the input array.

6. Explain the JavaScript implementation

The implementation follows the same order as the walkthrough. It validates inputs, captures the initial length, creates the sparse result array, chooses T, checks each index with i in array, invokes the callback with call, writes mapped values only for existing indexes, and finally returns the new array.

7. Explain complexity and edge cases

Let n be the captured initial length. The loop examines at most n indexes, so the time complexity is O(n). The algorithm uses only a constant number of variables, so auxiliary space is O(1). The required returned array uses O(n) output space.

Important cases include an empty array, an array containing only holes, deletion of a future property by the callback, adding new indexes beyond the initial length, and any valid thisArg, including null or undefined.

Key Insight / Why This Solution Works

The key idea is to reproduce JavaScript's index-visitation behavior, not simply copy values. Capture the initial array length once and allocate new Array(len) so the result begins with the correct length and holes. Then visit numeric indexes in increasing order. The invariant is: for every index already processed, the result has a property there exactly when the input property existed at visit time, and that result property stores the callback's returned value. Using i in array also makes properties deleted before visitation get skipped.

Code
function arrayMap(array, callback, thisArg) {
  // Validate that the first argument satisfies the required array contract.
  if (!Array.isArray(array)) {
    throw new TypeError('First argument must be an array');
  }

  // Mapping requires a callable callback.
  if (typeof callback !== 'function') {
    throw new TypeError('Callback must be a function');
  }

  // Capture the initial length once so later appends do not extend iteration.
  const len = array.length;

  // Start with the required output length. Unwritten positions remain holes.
  const result = new Array(len);

  // Use the supplied thisArg as the callback receiver when it was provided.
  const T = arguments.length >= 3 ? thisArg : undefined;

  // Visit indexes in increasing order, using only the captured initial range.
  for (let i = 0; i < len; i++) {
    // Check existence at visit time. Holes and deleted properties are skipped.
    if (i in array) {
      // Read the value only after confirming that this index currently exists.
      const value = array[i];

      // Pass the current value, index, and original array with T as the receiver.
      const mapped = callback.call(T, value, i, array);

      // Write only visited indexes so skipped positions remain holes.
      result[i] = mapped;
    }
  }

  // Return the new array without directly mutating the input array.
  return result;
}

// Run the exact sparse-array example from the diagram.
const example = [1, , 3];
const mapped = arrayMap(example, (value, index) => value + index);

// Verify the final values, length, and preserved hole at index 1.
console.log(mapped); // [1, <1 empty item>, 5] in typical developer consoles
console.log(mapped.length); // 3
console.log(mapped[0]); // 1
console.log(1 in mapped); // false
console.log(mapped[2]); // 5
Time & Space Complexity

Let n be the array length captured before iteration starts. The loop checks indexes from 0 to n - 1, so the time complexity is O(n). The algorithm itself keeps only a few variables, so auxiliary space is O(1). Auxiliary space means extra working memory besides the required answer. The returned array has length n, so its required output space is O(n).

Where it is used

This pattern is useful when building JavaScript array utilities or polyfills that must match normal array-iteration behavior. It is also useful for understanding sparse arrays, callback receivers, and how mutations during iteration affect which indexes are visited.

Why Interviewers Ask This

This question checks whether you understand JavaScript arrays beyond simple value iteration. The interviewer is testing sparse-array behavior, property existence, callback arguments, thisArg binding, and mutations during traversal. They also want to see whether you capture the initial length correctly, preserve holes in a new array, validate inputs, and write clear JavaScript without relying on the built-in map. Accurate time and auxiliary-space analysis is another part of the evaluation.

Common interview mistakes

One common mistake is using for...of, which does not preserve the required sparse-index callback behavior because holes can be observed as undefined values instead of being skipped this way. Another mistake is assigning undefined to a missing output index. That creates a real property instead of keeping a hole. Candidates may also forget to capture the initial length, call the callback without the required thisArg, skip the TypeError validation, or check whether an index exists only once instead of checking at the moment it is visited.

Interview tip

Emphasize two lines while explaining the solution: capture array.length before the loop, and check i in array at every visit. Those two rules explain most of the mutation and sparse-array behavior.

Interviewer may ask next
What happens if the callback deletes an index that has not been visited yet?

The loop still reaches that numeric index because it uses the captured initial length. However, i in array is checked only when the index is visited. If the callback deleted that property earlier, the condition is false. The callback is not invoked for that index, and the matching position in the result remains a hole. The complexity remains O(n) time, O(1) auxiliary space, and O(n) output space.

What happens if the callback appends new elements to the input array?

Indexes added at or beyond the captured initial length are not visited. The loop limit is the saved len, so later growth cannot extend the iteration range. Changes to indexes inside the original range can still affect later visits because property existence and values are read when each index is reached. The complexity remains O(n) time for the captured length, O(1) auxiliary space, and O(n) output space.

6. Find all DOM elements whose computed style matches a requested value.CodingMediumAmazon

Question Details

Implement getElementsByStyle(property, value, root = document) in browser JavaScript. property is a non-empty CSS property name accepted by CSSStyleDeclaration.getPropertyValue, value is the exact normalized string to compare, and root is a Document or Element. Return a static array of descendant elements in document order; include root itself only when it is an Element and its computed value matches. Read each candidate through getComputedStyle(element).getPropertyValue(property), trim the returned value and requested value, and do not inspect text nodes. Traverse the light DOM only and do not enter shadow roots or iframes. Reject invalid roots or empty property names; do not mutate the DOM. Example: when two descendants compute to color: rgb(255, 255, 255), calling with that exact normalized value must return those two elements in document order.

Short Interview Answer (30-60 seconds)

I would validate the CSS property and root first, then trim the property name and requested value. If root is an Element, I check it first. Then I use a TreeWalker with NodeFilter.SHOW_ELEMENT to visit descendant elements in document order. For each element, I read getComputedStyle(element).getPropertyValue(property), trim it, and compare it with the requested value using exact string equality. Matching elements go into a normal array. The time is O(N), and the result uses O(K) space.

Detailed Explanation

See the Code while reading this explanation.

The function finds DOM elements whose final browser style has one exact requested value. It receives a CSS property, a value to compare, and a Document or Element that acts as the search root. If the root itself is an Element, it can also be returned. The search visits only Element nodes in normal document order. It does not inspect text nodes, enter shadow roots, or enter iframe documents. Each matching element is placed in a normal JavaScript array, so the returned result is a static snapshot.

Useful Questions to Ask the Interviewer
  1. Should the comparison use the exact string returned by getComputedStyle after trimming? Yes. The requested value and computed value are both trimmed before exact comparison.
  2. Should root itself be included? Only when root is an Element and its computed value matches.
  3. Should the traversal enter shadow DOM or iframe documents? No. It must stay in the light DOM.
Find all DOM elements whose computed style matches a requested value. diagram
How to Explain It in an Interview
1. Validate and normalize the inputs

First, I check that property is a string and is not empty after trimming. I also check that root is either a Document or an Element. If either rule fails, I throw a TypeError. I trim the property name into prop. I also convert the requested value to a string and trim it once into target.

2. Create the result and check root

I start with an empty result array. If root is an Element, I read its computed value with getComputedStyle(root).getPropertyValue(prop), trim that value, and compare it with target using exact string equality. If it matches, I add root before its descendants. This preserves the required document order.

3. Traverse descendant elements in document order

I create a TreeWalker rooted at root with NodeFilter.SHOW_ELEMENT. This means the walker returns only Element nodes, so text nodes are never inspected. TreeWalker visits descendants in document order. It stays in the supplied light-DOM tree. It does not automatically enter a host's shadow root or an iframe's separate document.

4. Walk through the diagram example

The root is div#root. Its computed color is rgb(0, 0, 0), so it does not match rgb(255, 255, 255). The walker then visits p#a. Its computed color is rgb(255, 255, 255), so it is added. Next, span#b also computes to rgb(255, 255, 255), so it is added. Then p#c computes to rgb(0, 0, 0), so it is skipped. div#d inherits black, so it also computes to rgb(0, 0, 0) and is skipped. Finally, p#e computes to rgb(255, 255, 255), so it is added. The final result is [p#a, span#b, p#e] in document order.

5. Explain why the result is correct

The invariant is that after each visited element, result contains exactly the matching elements processed so far, in document order. Every candidate is checked with the required computed-style call. A non-match is skipped. A match is appended once. Because root is checked first when needed and TreeWalker then visits descendants in document order, the final array contains exactly the required elements in the required order.

6. Explain the JavaScript implementation

The code uses a small isMatch helper for the repeated computed-style comparison. It checks an Element root before creating the traversal result for descendants. TreeWalker visits Element nodes only. The loop calls nextNode until there are no more descendants. Each match is pushed into result. The search function never changes any DOM node, attribute, style, or tree relationship.

7. Explain complexity and edge cases

Let N be the number of candidate elements in the root subtree, including root when root is an Element. The traversal processes each candidate once, so the diagram's time complexity is O(N). If K elements match, the returned array stores K element references, so the result space is O(K). Important cases are an invalid root, an empty property name, a property whose computed value is an empty string, an empty requested value, an Element root that matches, and the requirement to stay outside shadow roots and iframe documents.

Key Insight / Why This Solution Works

Use a TreeWalker configured with NodeFilter.SHOW_ELEMENT. First validate the property and root. Trim the property name and requested value once. If root is an Element, test it before traversing descendants. Then visit each descendant Element in document order. For every candidate, read getComputedStyle(element).getPropertyValue(prop), trim the returned string, and compare it exactly with target. The central invariant is that result always contains exactly the matching elements already processed, in document order. This directly matches the required output and avoids inspecting non-element nodes.

Code
function getElementsByStyle(property, value, root = document) {
  // Reject an invalid or empty CSS property name before any traversal starts.
  if (typeof property !== 'string' || property.trim() === '') {
    throw new TypeError('property must be a non-empty string');
  }

  // Accept only Document or Element roots, matching the required input contract.
  const isDocument = root && root.nodeType === Node.DOCUMENT_NODE;
  const isElement = root && root.nodeType === Node.ELEMENT_NODE;
  if (!isDocument && !isElement) {
    throw new TypeError('root must be a Document or Element');
  }

  // Normalize the property name and requested value once before processing nodes.
  const prop = property.trim();
  const target = String(value).trim();

  // A normal array gives the caller a static snapshot rather than a live collection.
  const result = [];

  // Read the exact computed value required by the question and compare trimmed strings.
  const isMatch = (element) => {
    const computedValue = getComputedStyle(element).getPropertyValue(prop).trim();
    return computedValue === target;
  };

  // The root participates only when it is an Element. Check it before descendants
  // so a matching root appears first in document order.
  if (isElement && isMatch(root)) {
    result.push(root);
  }

  // Visit descendant Element nodes only. TreeWalker follows document order.
  // Shadow roots and iframe documents are separate trees, so this walker does not enter them.
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);

  // Process every descendant Element once and append only exact computed-style matches.
  let current = walker.nextNode();
  while (current) {
    if (isMatch(current)) {
      result.push(current);
    }
    current = walker.nextNode();
  }

  // The search itself does not mutate the DOM.
  return result;
}

// Example setup matching the diagram.
// These temporary nodes are created only so this standalone example can be run directly.
const root = document.createElement('div');
root.id = 'root';
root.style.color = 'black';

const a = document.createElement('p');
a.id = 'a';
a.style.color = 'white';
a.textContent = 'A';

const b = document.createElement('span');
b.id = 'b';
b.style.color = '#fff';
b.textContent = 'B';

const c = document.createElement('p');
c.id = 'c';
c.style.color = 'black';
c.textContent = 'C';

const d = document.createElement('div');
d.id = 'd';

const e = document.createElement('p');
e.id = 'e';
e.style.color = 'white';
e.textContent = 'E';

// Build the same light-DOM structure shown in the diagram.
d.appendChild(e);
c.appendChild(d);
root.append(a, b, c);

// Attach the temporary example so the browser resolves normal computed styles.
document.body.appendChild(root);

// Expected document-order result: p#a, span#b, p#e.
const matches = getElementsByStyle('color', 'rgb(255, 255, 255)', root);

console.log(matches.map((element) => `${element.tagName.toLowerCase()}#${element.id}`));

// Remove only the temporary example DOM. getElementsByStyle itself never mutates the DOM.
root.remove();
Time & Space Complexity

Let N be the number of candidate elements in the subtree rooted at root, including root itself when root is an Element. The algorithm processes each candidate once, so the time complexity shown in the diagram is O(N). Let K be the number of matching elements. The returned array stores K element references, so its space is O(K). The other variables use only constant extra working space.

Where it is used

This pattern is useful in browser developer tools, visual regression helpers, accessibility checks, style-audit utilities, and debugging tools that must find elements by their final computed CSS value instead of only checking inline styles.

Why Interviewers Ask This

This question tests whether you understand computed styles instead of only inline styles. It also checks DOM traversal order, the difference between Document and Element roots, filtering to Element nodes, light-DOM boundaries, and non-mutating browser code. The interviewer can see whether you can turn a precise browser API contract into a correct implementation, keep the returned order stable, explain edge cases clearly, and give complexity that matches the code.

Common interview mistakes

One common mistake is reading element.style instead of getComputedStyle, which misses inherited values and styles produced by stylesheets. Another is assuming a descendant query automatically includes an Element root. Candidates may also forget to trim the requested and computed strings, inspect text nodes unnecessarily, or traverse into separate shadow or iframe trees. Another mistake is returning a live DOM collection instead of a normal array. It is also easy to claim the wrong complexity or accidentally modify the DOM inside the search function.

Interview tip

State the traversal order before coding: check an Element root first, then use TreeWalker with SHOW_ELEMENT for descendants. That makes the root rule, document-order rule, and text-node rule easy for the interviewer to verify.

Interviewer may ask next
How would the solution change if it also had to search inside open shadow roots?

The computed-style comparison can stay the same, but one TreeWalker is not enough because a shadow root is a separate tree. I would explicitly detect hosts with an open shadowRoot and traverse those shadow trees as additional roots. I would also define how matches across separate trees should be ordered. The total work would still be O(N) over all visited elements. Extra traversal memory could grow with the number of pending roots, in addition to the O(K) returned matches.

What changes if the caller wants only the first matching element?

I would keep the same validation and exact computed-style comparison. I would check an Element root first. Then I would traverse descendants in document order and return immediately when the first match is found. If no element matches, I would return null. The worst-case time remains O(N), but the function can stop earlier. It no longer needs an O(K) result array, so the result storage becomes O(1).

7. Build a self-updating relative-time component.CodingMediumAmazon

Question Details

Implement React component RelativeTime({ date, now = Date.now }). date is a valid Date or millisecond timestamp not more than ten years in the future or past; now() returns the current millisecond timestamp for deterministic tests. Display just now for an absolute difference under 10 seconds, less than a minute ago for 10–59 seconds in the past, and otherwise the largest whole unit among minutes, hours, days, weeks, months of 30 days, or years of 365 days, using in … for future times. Schedule the next update at the earliest instant the displayed label can change, rather than polling every second forever; cancel timers when props change or the component unmounts. Set an exact ISO timestamp in a <time dateTime> element. Example: with date=now()-90_000, render 1 minute ago; after 30 seconds, render 2 minutes ago.

Short Interview Answer (30-60 seconds)

I compute the signed difference between the target time and now, then choose the largest whole time unit from a fixed descending list. I keep the sign because past and future values move toward different label boundaries. After rendering the label, I schedule one timer for the earliest millisecond when that label can change. Long waits are chunked to the browser timer limit. I clear timers on prop changes or unmount. Each update takes O(1) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The component receives a target date and a function that tells us the current time. It shows a simple message such as just now, 1 minute ago, or in 2 hours. The main goal is to keep that message correct without waking up every second. After choosing the current message, we calculate the first moment when that exact message can become different. We create one timer for that moment. When the timer runs, we calculate everything again. This avoids unnecessary polling while keeping the text current.

Useful Questions to Ask the Interviewer
  1. For a future time between 10 and 59 seconds away, should we use the symmetric label in less than a minute as shown in the required behavior?
  2. Should very long waits be split because a browser timer cannot safely represent a delay of many months or years in one setTimeout call?
Build a self-updating relative-time component. diagram
How to Explain It in an Interview
1. Understand the input and required output

date is either a valid Date or a millisecond timestamp. It is at most ten years in the future or past. now() returns the current millisecond timestamp and can be replaced in tests.

I normalize date to one number called targetMs. Then I calculate diff = targetMs - now(). A positive diff means the target is in the future. A negative diff means it is in the past.

The component returns a <time> element. Its dateTime attribute contains the exact ISO timestamp. Its visible text contains the relative-time label.

2. Choose the label from the signed difference

I use Math.abs(diff) to measure how far away the target is, but I keep the sign for past or future wording.

If the absolute difference is under 10 seconds, the label is just now.

From 10 to 59 seconds, a past value is less than a minute ago. A future value is in less than a minute.

For larger values, I check years, months, weeks, days, hours, and minutes from largest to smallest. I divide by the unit size and use Math.floor. This gives the largest whole unit and its count.

3. Schedule the earliest possible label change

Past and future values move in opposite directions.

For a past timestamp, |diff| grows as time passes. The next label change happens when it reaches the next count or unit boundary.

For a future timestamp, |diff| shrinks. The next label change happens when it drops below the current count or unit boundary.

For example, if a future target is 2 hours 15 minutes away, the label is in 2 hours. About 15 minutes later, the remaining time becomes less than 2 hours, so that label can change.

For just now, a future target stays just now through the target instant. It changes only when the target becomes 10 seconds old.

4. Walk through the supplied example

The example uses date = now() - 90_000.

At the first render, diff is -90,000 ms. Its absolute value is 90 seconds. The largest whole unit is minutes. Math.floor(90 / 60) is 1, so the component renders 1 minute ago.

For a past minute label, the next count boundary is 2 minutes, or 120 seconds. The timestamp is currently 90 seconds old, so the timer waits 30 seconds.

After those 30 seconds, the timestamp is 120 seconds old. The count becomes 2, so the component renders 2 minutes ago.

If another 60 seconds passes, the age reaches 180 seconds and the label becomes 3 minutes ago.

5. Explain why the result is correct

The invariant is that every visible label is calculated from the latest signed difference targetMs - now().

The timer targets the first future millisecond when that exact label can be different. Past magnitudes grow. Future magnitudes shrink. When a timer fires, the component reads now() again, recalculates the label, and calculates a new boundary.

The effect cleanup clears the active timer. React runs that cleanup before the effect is replaced because its dependencies changed and when the component unmounts.

6. Explain complexity and important edge cases

Each update checks a fixed list of six units. The list does not grow with the input, so each update takes O(1) time.

The component keeps one label, one timer identifier, and a few numbers. Auxiliary space is O(1).

Important cases include values under 10 seconds, the 10-to-59-second range, transitions such as 59 seconds to 60 seconds, future values crossing downward boundaries, prop changes, clock changes between updates, and component unmounting.

Browser setTimeout delays are effectively limited to about 2^31 - 1 milliseconds, or about 24.8 days. If the next display change is farther away, the code schedules only that maximum delay and recalculates after the timer fires.

Key Insight / Why This Solution Works

The key insight is to use the same time boundaries for both formatting and scheduling. First compute the signed difference targetMs - now(). Use the absolute value to choose the size of the time unit, but keep the sign to determine past or future wording and the direction of the next boundary. A fixed array stores year, month, week, day, hour, and minute sizes from largest to smallest. The central invariant is that the visible label always comes from the latest signed difference, and the timer is scheduled for the first moment when that exact label can change. This avoids one-second polling.

Code
import { useEffect, useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';

// Exact unit sizes required by the relative-time rules.
const SECOND = 1_000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;

// Browser timers effectively cannot represent a larger single delay.
// Long waits are chunked and recalculated when this timer fires.
const MAX_TIMEOUT = 2_147_483_647;

// Largest units come first so the first match is the largest whole unit.
const UNITS = [
  ['year', YEAR],
  ['month', MONTH],
  ['week', WEEK],
  ['day', DAY],
  ['hour', HOUR],
  ['minute', MINUTE],
];

function describe(diff) {
  // Keep the sign for future/past wording and use the magnitude for ranges.
  const future = diff > 0;
  const abs = Math.abs(diff);

  // Both future and past timestamps use this label while under 10 seconds away.
  if (abs < 10 * SECOND) {
    return 'just now';
  }

  // The 10-59 second range uses special wording instead of a numeric unit.
  if (abs < MINUTE) {
    return future ? 'in less than a minute' : 'less than a minute ago';
  }

  // Choose the largest whole unit by scanning the fixed list from largest down.
  for (const [name, size] of UNITS) {
    if (abs >= size) {
      const count = Math.floor(abs / size);
      const unit = count === 1 ? name : `${name}s`;

      // The count is the same in both directions. Only the wording changes.
      return future ? `in ${count} ${unit}` : `${count} ${unit} ago`;
    }
  }

  // Defensive fallback. All valid ranges above already return a label.
  return 'just now';
}

function nextChangeDelay(diff) {
  // Past magnitudes grow while future magnitudes shrink.
  const future = diff > 0;
  const abs = Math.abs(diff);

  if (abs < 10 * SECOND) {
    // Past: wait until the timestamp becomes exactly 10 seconds old.
    // Future: stay "just now" through the target and until it is 10 seconds old.
    return future ? abs + 10 * SECOND : 10 * SECOND - abs;
  }

  if (abs < MINUTE) {
    // Past: "less than a minute ago" ends at 60 seconds old.
    // Future: "in less than a minute" ends below 10 seconds remaining.
    return future ? abs - 10 * SECOND + 1 : MINUTE - abs;
  }

  // Find the unit that produced the current visible label.
  for (const [, size] of UNITS) {
    if (abs >= size) {
      const count = Math.floor(abs / size);

      // Future values change just after dropping below the current count boundary.
      // Past values change when they reach the next count boundary.
      return future ? abs - count * size + 1 : (count + 1) * size - abs;
    }
  }

  // Defensive fallback for completeness.
  return SECOND;
}

export function RelativeTime({ date, now = Date.now }) {
  // Normalize Date and numeric inputs to one millisecond timestamp.
  const targetMs = useMemo(() => (date instanceof Date ? date.getTime() : date), [date]);

  // Calculate the first visible label immediately.
  const [label, setLabel] = useState(() => describe(targetMs - now()));

  useEffect(() => {
    let timerId;
    let cancelled = false;

    function update() {
      // Ignore a stale callback after this effect has been cleaned up.
      if (cancelled) return;

      // Read now() again because time or the injected test clock may have changed.
      const diff = targetMs - now();
      setLabel(describe(diff));

      // Schedule the earliest possible label change instead of polling each second.
      const delay = Math.max(1, nextChangeDelay(diff));

      // Chunk any wait longer than the browser's practical timeout range.
      timerId = setTimeout(update, Math.min(delay, MAX_TIMEOUT));
    }

    // Recalculate immediately when targetMs or the now function changes.
    update();

    return () => {
      // Prevent stale work and remove the active timer on change or unmount.
      cancelled = true;
      clearTimeout(timerId);
    };
  }, [targetMs, now]);

  // Keep the exact timestamp for machines while showing relative text to the user.
  return <time dateTime={new Date(targetMs).toISOString()}>{label}</time>;
}

// Direct invocation using the diagram's exact example: date = now() - 90_000.
const rootElement = document.getElementById('root');

if (rootElement) {
  const exampleNow = Date.now();
  const exampleDate = exampleNow - 90_000;

  // Initially this renders "1 minute ago".
  // The component's own boundary timer updates it to "2 minutes ago"
  // about 30 seconds later, without one-second polling.
  createRoot(rootElement).render(<RelativeTime date={exampleDate} />);
}
Time & Space Complexity

Each update does a constant amount of work. It checks at most six units: year, month, week, day, hour, and minute. Six is fixed, so the time is O(1) per update. The component stores only a fixed unit list, a few numbers, one label, and one active timer. Auxiliary space is O(1). If the calculated wait is longer than the browser timer limit, the component may wake up several times to chunk the wait, but each individual update still takes O(1) time and O(1) extra space.

Where it is used

This pattern is useful for relative timestamps in feeds, comments, notifications, chat messages, activity logs, dashboards, and status pages. It is especially useful when many labels stay unchanged for minutes or hours. Scheduling only the next real display boundary avoids repeatedly waking components when their visible text would not change.

Why Interviewers Ask This

This problem checks more than string formatting. It tests whether you can reason about exact time boundaries, signed values, React effects, cleanup, deterministic testing, and browser timer limitations. It also shows whether you can avoid wasteful polling while still updating at the first moment the visible result can change. A strong answer keeps the formatting logic, scheduling logic, React lifecycle behavior, and complexity explanation consistent with each other.

Common interview mistakes

A common mistake is polling every second even when the label cannot change for minutes or hours. Another is using only Math.abs(diff) and losing whether the timestamp is in the future or past. That makes future scheduling wrong because future magnitudes shrink instead of grow. Candidates may also forget the special 10-to-59-second wording, use the wrong side of a future boundary, forget effect cleanup, forget the exact ISO dateTime value, or pass a multi-month delay directly to setTimeout instead of chunking long waits.

Interview tip

Explain the direction before writing the timer formula: past values get farther away, while future values get closer. Then use the supplied 90-second example. 1 minute ago changes at 120 seconds old, so the next update is exactly 30 seconds later. This makes the scheduling rule easy for the interviewer to verify.

Interviewer may ask next
How would you test this component without waiting for real time to pass?

I would inject a deterministic now function and use fake timers. I would start with a fixed current timestamp and set date = now() - 90_000. The first label should be 1 minute ago. Then I would advance the fake clock and fake timer by 30,000 ms. The next label should be 2 minutes ago. I would also test 9,999 ms, 10,000 ms, 59,999 ms, 60,000 ms, future boundaries, cleanup after prop changes, unmount cleanup, and a calculated delay above the browser timer limit. Each update remains O(1) time and O(1) auxiliary space.

What changes if the component may receive timestamps many decades away instead of at most ten years?

The label-selection algorithm can remain the same if the same year, month, week, day, hour, and minute rules are still required. The main tradeoff is timer scheduling. One browser setTimeout cannot safely represent an extremely large delay, so I would continue capping each wait at 2_147_483_647 ms and recalculate after every capped wakeup. Correctness is preserved because each wakeup reads now() again before choosing the label and next boundary. Each wakeup is still O(1) time and O(1) auxiliary space, but very distant dates cause more periodic wakeups.

8. Calculate how much rainwater is trapped between elevation bars.CodingHardAmazon

Question Details

Implement trapRainWater(heights). heights is an array of 0 through 100,000 non-negative safe integers representing unit-width elevation bars; the array may be empty, and duplicate heights are allowed. Return the total trapped-water volume as a safe integer, assuming water cannot escape through bars but can leave from either end. Do not mutate or sort the input. Reject non-array input, negative heights, non-integers, or a result outside Number.MAX_SAFE_INTEGER with an appropriate error. Example: trapRainWater([0,1,0,2,1,0,1,3,2,1,2,1]) must return 6; fewer than three bars return 0. Use standard ECMAScript only and target O(n) time with O(1) auxiliary space.

Short Interview Answer (30-60 seconds)

I would use two pointers, one at each end, and keep the highest bar seen from the left and from the right. On each loop, I process the side with the smaller running maximum because that side already has a confirmed water boundary. I update that maximum, add the water trapped at that index, and move that pointer inward. Each index is processed at most once. This gives O(n) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is an array of bar heights. Each bar has width one. We need the total amount of water that can stay between the bars after rain. Water can leave from the two ends. We must not sort or change the array. The solution uses two pointers and two running maximum heights. At each step, it works on the side whose running maximum is smaller because that side already has enough information to decide its trapped water.

Useful Questions to Ask the Interviewer
  1. Should invalid heights and non-array input throw an error? Yes. The stated contract requires an appropriate error.
  2. Should every height be validated before returning 0 for an array with fewer than three bars? Yes. The diagram validates the complete input first.
  3. Should the input remain unchanged? Yes. The solution must not sort or mutate it.
Calculate how much rainwater is trapped between elevation bars. diagram
How to Explain It in an Interview
1. Understand the input and required output

heights must be an array. Every element must be a safe integer from 0 through 100000. The function returns one safe integer containing the total trapped-water volume. For [0,1,0,2,1,0,1,3,2,1,2,1], the answer is 6. A valid array with fewer than three bars returns 0.

2. Choose the two-pointer method

Place left at the first bar and right at the last bar. Keep leftMax, which is the highest bar processed from the left, and rightMax, which is the highest bar processed from the right. If leftMax <= rightMax, the left side has the smaller confirmed boundary, so the water at left can be finalized. Otherwise, the right side can be finalized.

3. Initialize the state

First validate the array and every height. If the valid array has fewer than three bars, return 0. Otherwise set left = 0, right = n - 1, leftMax = 0, rightMax = 0, and water = 0.

4. Walk through the example

The processing order shown by the two-pointer method begins as follows: 1. Index 0 from the left has height 0. leftMax stays 0. Add 0. 2. Index 1 from the left has height 1. leftMax becomes 1. Add 0. 3. Index 11 from the right has height 1. rightMax becomes 1. Add 0. 4. Index 2 from the left has height 0. Add 1 - 0 = 1. Total is 1. 5. Index 3 from the left has height 2. leftMax becomes 2. Add 0. 6. Index 10 from the right has height 2. rightMax becomes 2. Add 0. 7. Index 4 from the left has height 1. Add 2 - 1 = 1. Total is 2. 8. Index 5 from the left has height 0. Add 2 - 0 = 2. Total is 4. 9. Index 6 from the left has height 1. Add 2 - 1 = 1. Total is 5. 10. Index 7 from the left has height 3. leftMax becomes 3. Add 0. 11. Index 9 from the right has height 1. Add 2 - 1 = 1. Total is 6. 12. The pointers meet at index 8, whose height is 2. Processing that final position adds 2 - 2 = 0. The final total remains 6.

5. Explain why the result is correct

When leftMax <= rightMax, there is already a right-side boundary at least as high as leftMax. This means the left side's water level is determined by leftMax. After updating leftMax, the amount at that index is final. The same reasoning works from the right when rightMax is smaller. Each index is finalized once, so water is not counted twice.

6. Explain the JavaScript implementation

The code rejects non-array input and validates every height. It returns 0 for a valid array shorter than three bars. It then runs the two-pointer loop, updates one running maximum, calculates the non-negative amount trapped at that index, checks the safe-integer limit before adding, and moves one pointer inward. The loop includes the meeting index so a possible trapped amount there is not skipped.

7. Explain complexity and edge cases

Each pointer only moves toward the center. Every array position is processed at most once, so the running time is O(n). The function stores only a constant number of numeric variables, so auxiliary space is O(1). Empty and short valid arrays return 0. Monotonic or equal-height arrays trap 0. Invalid values throw an error.

Key Insight / Why This Solution Works

The key insight is that only the smaller confirmed boundary is needed to finalize one side. leftMax is the highest processed height from the left. rightMax is the highest processed height from the right. When leftMax <= rightMax, the right side already provides a boundary at least as high as leftMax, so the water at left is leftMax - heights[left] after updating leftMax. Otherwise, the symmetric calculation is safe on the right. The central invariant is that every index moved past a pointer already has its final trapped-water value. The implementation processes the meeting index once before stopping, which is necessary for inputs such as [3,0,3].

Code
function trapRainWater(heights) {
  // The contract requires an array. Reject every other input type.
  if (!Array.isArray(heights)) {
    throw new TypeError('heights must be an array');
  }

  const n = heights.length;

  // Validate every height before handling the short-array case.
  for (let i = 0; i < n; i++) {
    const height = heights[i];

    // Non-integers and unsafe integers are invalid input values.
    if (!Number.isSafeInteger(height)) {
      throw new TypeError(`heights[${i}] must be a safe integer`);
    }

    // The question limits every height to the inclusive range 0..100000.
    if (height < 0 || height > 100000) {
      throw new RangeError(`heights[${i}] must be between 0 and 100000`);
    }
  }

  // A valid array needs at least three bars to trap water.
  if (n < 3) {
    return 0;
  }

  // Start one pointer at each end and track the best boundary seen on each side.
  let left = 0;
  let right = n - 1;
  let leftMax = 0;
  let rightMax = 0;
  let water = 0;

  const maxSafe = Number.MAX_SAFE_INTEGER;

  // Process each position exactly once, including the final meeting position.
  while (left <= right) {
    if (leftMax <= rightMax) {
      // The left boundary is the limiting confirmed boundary for this step.
      leftMax = Math.max(leftMax, heights[left]);
      const add = leftMax - heights[left];

      // Check before addition so the result never becomes an unsafe integer.
      if (water > maxSafe - add) {
        throw new RangeError('Result exceeds Number.MAX_SAFE_INTEGER');
      }

      water += add;
      left++;
    } else {
      // The right boundary is the limiting confirmed boundary for this step.
      rightMax = Math.max(rightMax, heights[right]);
      const add = rightMax - heights[right];

      // Apply the same safe-integer guard before changing the total.
      if (water > maxSafe - add) {
        throw new RangeError('Result exceeds Number.MAX_SAFE_INTEGER');
      }

      water += add;
      right--;
    }
  }

  // All positions are finalized, so return the total trapped-water volume.
  return water;
}

// Run the exact example used in the diagram.
console.log(trapRainWater([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])); // 6
Time & Space Complexity

Let n be the number of bars. Each loop moves either left one position right or right one position left. Every index is therefore processed at most once. The time complexity is O(n). The algorithm uses only the two pointers, two running maximum values, the current addition, and the accumulated total. These do not grow with n, so auxiliary space is O(1). The input is not copied, sorted, or changed.

Where it is used

This two-pointer pattern is useful when information from two boundaries lets us permanently finish work at one end. Similar reasoning appears in array boundary problems, terrain-volume calculations, and problems where processing one side becomes safe once its limiting boundary is known.

Why Interviewers Ask This

This problem tests whether a candidate can recognize a two-pointer pattern and maintain a useful boundary invariant instead of creating extra prefix and suffix arrays. It also tests exact pointer movement, state updates, edge cases, and complexity reasoning. The JavaScript contract adds safe-integer validation and result checking. A strong answer connects the proof directly to the code and notices subtle stopping-condition errors that can make an otherwise correct two-pointer idea fail.

Common interview mistakes

A common mistake is moving the side with the larger running maximum instead of the smaller one. Another is calculating water before updating that side's maximum. A subtle mistake is stopping when left === right without processing the meeting position. For example, that would fail on [3,0,3]. Candidates may also return 0 for a short array before validating its values, check overflow only after unsafe arithmetic, or sort the input even though mutation and sorting are forbidden.

Interview tip

Explain the invariant before writing the loop: the side with the smaller running maximum has enough information to finalize its current position. Then point out that the meeting position must also be processed once.

Interviewer may ask next
Why must the meeting index be processed instead of stopping as soon as left equals right?

The meeting position may still contain trapped water. For example, [3,0,3] has 3 units at the middle index. If the loop stops when left === right, that middle index is skipped and the result is wrong. Using left <= right processes the meeting position exactly once. The time complexity remains O(n) and auxiliary space remains O(1).

What changes if we must return the trapped water at every index as well as the total?

Keep the same two-pointer decision rule, but create an output array of length n. When an index is finalized, store its trapped amount in that array and also add it to the total. The correctness argument does not change because each processed position still gets its final value exactly once. Running time remains O(n). The returned per-index array needs O(n) output space, while the algorithm's other auxiliary memory remains O(1).

9. How would you enforce object-level permissions in a browser storage explorer?SecurityMediumAmazon

Question Details

A frontend lists object keys and offers download, rename, and delete actions based on permissions returned by an API. Map the untrusted browser, authenticated principal, protected objects, and risks from changing an object key, prefix, version, or action in a request. Explain server-side object-level authorization, short-lived download authorization, conditional mutations tied to a selected version, prevention of prefix-confusion or encoded-key bugs, and least-privilege action metadata. Cover bulk operations with partial denial, stale permission displays, audit records, error redaction, and logout. Include tests for another user's key, a key containing slash or encoded separators, a renamed object, and a request repeated after authorization changes. Hiding an action may improve usability but must not grant or deny access.

Short Interview Answer (30-60 seconds)

I would treat the browser as untrusted and make the server authorize every object and action. Hidden buttons never provide security. Downloads get short-lived authorization, mutations use version conditions, object identities are interpreted consistently, bulk items are checked separately, and captured requests are reauthorized against current permissions.

Detailed Explanation

The browser can show stored items and action buttons, but a person can change anything the browser sends. So I would never let the screen decide what someone may download, rename, or delete. Every request must be checked again by a trusted service using the signed-in person, the exact stored item, and the requested operation. Changing a name, folder-like path, saved revision, or requested action must not bypass that check. Temporary download access should expire quickly. Changes should fail safely when an item has changed. Lists, errors, history, and sign-out behavior should also avoid exposing private information.

Useful Questions to Ask the Interviewer
  1. Are permissions defined directly on each object, inherited from a prefix, or both?
  2. Does each object have an immutable internal ID, or is its key and version the only identity?
  3. Are rename and delete expected to be conditional on the version the user selected?
  4. For bulk actions, should allowed objects succeed while denied objects return individual results?
  5. How quickly must authorization changes affect a browser page that is already open?
How would you enforce object-level permissions in a browser storage explorer? diagram
How to Explain It in an Interview

I would begin with the trust boundary. The browser is untrusted. Authentication proves who the principal is. Authorization decides whether that principal may perform one specific action on one specific protected object. The trusted server must make that authorization decision on every request. A permission flag returned to JavaScript may control whether a button is shown, but changing that flag must never change what the server permits.

When the explorer loads objects, the server can return least-privilege action metadata such as whether the current principal may download, rename, or delete each item. The frontend uses this metadata only for usability. A user can edit JavaScript state, modify the request, or call the endpoint without the interface, so the server must independently authorize the principal, object, action, and current policy again when the operation arrives.

I would prefer an opaque, stable object identifier for authorization when the storage design provides one. If the API uses object keys directly, all layers must agree on exactly what bytes or characters identify the object. I would not casually URL-decode, normalize, trim, or rewrite storage keys because many object stores legitimately allow slashes, percent characters, repeated separators, Unicode, or other unusual characters in a key. Instead, the API should define one unambiguous transport representation, parse it once according to that contract, reject malformed or ambiguous encodings, and use the resulting exact identity for both authorization and the storage operation.

This prevents prefix-confusion and encoded-key bugs. For example, the server must not authorize the literal key private%2Ffile.txt and then have another layer decode it into private/file.txt, or perform a prefix check on one representation while storage resolves another. If a key contains a slash or an encoded separator, the authorization layer and storage layer must identify exactly the same object. Using opaque object IDs at the API boundary can make this easier.

For downloads, I would use short-lived authorization scoped to the exact object and download action. This could be a server-mediated response or a narrowly scoped signed capability that expires quickly. If versions matter, it should also identify the exact version. The browser must never receive server secrets, long-lived storage credentials, or a credential that grants broader access than the requested download.

Rename and delete need protection against stale state. The browser should send the selected object's identity and its expected version, generation, ETag, or equivalent revision marker. The server first authenticates the principal and authorizes the requested mutation. It then performs a conditional storage operation that succeeds only if the expected version is still current. If another change happened first, the server returns a conflict instead of modifying a newer state that the user did not select.

Rename requires authorization for both the source and destination. The server should verify permission to mutate the source object and permission to place or create the object at the destination key or prefix. Permission on the old location must not automatically imply permission on the new location. After the rename, later requests must authorize the object's current identity and location rather than trusting stale metadata from before the rename.

For bulk operations, I would authorize every object separately. An authorized first object must never authorize the rest of the batch. If the product allows partial success, the server can process allowed objects and return a per-item result such as success, denied, conflict, or unavailable. Denied results should reveal no more information than the principal is entitled to know. Each mutation should still use its own expected version.

Permission displays can become stale. For example, a Delete button may still be visible after an administrator revokes access. That is a usability issue, not a security failure, as long as clicking it causes a new server-side authorization check and the operation is denied. The frontend should refresh or invalidate permission metadata after mutations, authorization failures, session changes, and policy-change notifications when the system provides them, but UI freshness never replaces server enforcement.

Because object keys may be attacker-controlled text, I would also render names with safe DOM APIs such as textContent or normal framework escaping instead of inserting them with innerHTML. If the application intentionally supports HTML content, it requires appropriate sanitization for that HTML context. A restrictive Content Security Policy and Trusted Types can provide additional defense against DOM-based XSS, but they do not replace object-level authorization.

If authentication uses cookies, state-changing endpoints also need appropriate CSRF protection, such as correctly configured SameSite cookies plus an anti-CSRF mechanism where required by the application's architecture. CORS and the browser's same-origin policy are not substitutes for authorization because a request that reaches the server still needs an object-level access decision.

Errors should be redacted. An unauthorized request should not reveal whether another user's private object exists, who owns it, internal permission rules, or sensitive metadata unless the product intentionally allows that disclosure. The browser can receive a safe denied or unavailable response while protected server logs retain enough detail for investigation.

Audit records should include the authenticated principal, requested action, stable object identity, relevant version, authorization decision, timestamp, and a correlation or request identifier. Mutations should also record whether they succeeded, were denied, or failed because the expected version was stale. Logs must not contain passwords, session cookies, bearer tokens, signing secrets, or unnecessary object contents.

On logout, the frontend should clear sensitive in-memory state and cached permission information belonging to the previous session. The server should terminate server-side sessions and revoke or rotate refresh credentials when the authentication design supports that. A previously issued short-lived download capability may remain usable until expiration unless the system supports revocation, which is why its scope and lifetime should be deliberately small.

I would verify the design with adversarial tests. First, capture a valid request and replace the object key or object ID with another user's object; the server must deny it. Second, test a legitimate key containing a slash and keys containing encoded separators or ambiguous encodings; the authorization and storage layers must identify the same exact object or reject the request. Third, rename an object and replay a request that uses its previous key, version, or stale permission metadata; it must not accidentally authorize the renamed object. Fourth, capture an authorized request, change the principal's authorization, and replay the request; the server must evaluate current policy and deny it after access has been revoked.

The main tradeoff is usability versus security enforcement. Sending action metadata lets the frontend present a cleaner interface and avoid actions that are expected to fail. That metadata can become stale or be changed by the user, so it is never authoritative. Security comes from current server-side authorization on every sensitive operation.

Technical Approach
  1. Treat every browser-supplied value as untrusted, including object ID or key, prefix, version, requested action, and displayed permission flags.
  2. Authenticate the principal on the trusted server.
  3. Resolve one exact object identity using an opaque ID when possible, or a precisely defined key representation without inconsistent decoding or normalization.
  4. Authorize the exact principal-object-action combination against current policy.
  5. Return least-privilege action metadata to the frontend only for usability.
  6. For downloads, issue only short-lived, object-scoped and action-scoped authorization.
  7. For rename and delete, require an expected version and use a conditional mutation so stale requests fail safely.
  8. For rename, authorize both the source mutation and destination placement.
  9. For bulk actions, authorize and process each object independently and return safe per-item outcomes.
  10. Redact authorization errors and write protected audit records without secrets.
  11. Refresh frontend permission metadata when useful, but always reauthorize on the server.
  12. Clear session-related client state on logout and terminate or revoke server-side authentication state where supported.
  13. Verify the controls with another user's object, slash and encoded-separator keys, renamed objects, stale versions, and replay after authorization changes.
Practical Insights

For one object, the browser work is small. The main cost is the server's authentication, policy lookup, object lookup, and storage operation. A bulk request grows roughly with the number of objects because every object needs its own authorization decision and usually its own version check. Audit records add storage and operational cost. Short-lived download capabilities can reduce application-server bandwidth when storage serves the data directly, but they add signing, expiration, and key-management responsibilities. The biggest maintenance cost is keeping object identity parsing, authorization rules, storage lookup, conditional mutations, error behavior, and tests consistent across every endpoint.

Why Interviewers Ask This

This question tests whether the candidate understands that frontend permission controls are only a usability feature and that real object-level access control belongs on the trusted server. It also evaluates judgment around tampered object identifiers, encoded-key ambiguity, stale permissions, short-lived downloads, version-safe rename and delete operations, bulk partial denial, auditability, safe error handling, logout, and adversarial verification.

Common interview mistakes

Common mistakes include treating a hidden or disabled button as authorization; trusting browser-supplied permission flags; authorizing only a prefix instead of the exact object and action; decoding or normalizing a key differently between authorization and storage access; assuming every encoded separator should simply be decoded or rejected without defining the API's key representation; putting long-lived storage credentials or secrets in frontend code; issuing download authorization that lasts too long or covers too many objects; renaming or deleting without an expected-version condition; checking only the source permission during rename and ignoring the destination; authorizing a whole bulk request because one item is allowed; revealing private object existence through detailed errors; trusting stale permission metadata after policy changes; using CORS as if it were authorization; rendering attacker-controlled object names through innerHTML; logging tokens or secrets; and assuming logout automatically revokes every temporary capability already issued.

Interview tip

Start with the core rule: the browser is untrusted and the server authorizes every object and action. Then cover exact object identity, short-lived downloads, version-safe mutations, source-and-destination checks for rename, per-item bulk authorization, stale UI permissions, safe errors, audit records, logout, and the four adversarial tests required by the question.

Interviewer may ask next
What should happen if a user selects 100 objects for deletion but has permission to delete only 60 of them?

The server should authorize all 100 objects independently. If partial success is part of the API contract, it can delete the 60 allowed objects and return safe per-item results for the others, such as denied or conflict. Each allowed deletion should still use the expected version so a stale object fails rather than deleting a newer version. A denial must not expose unnecessary information about an object the principal cannot access, and sensitive decisions and successful mutations should be recorded in protected audit logs.

How would you prevent a permission check from being bypassed with encoded or unusual object keys?

I would define one unambiguous API representation for object identity and use the exact parsed identity for both authorization and storage access. I would avoid independent decoding or normalization in different layers. Legitimate storage keys may contain slashes, percent characters, repeated separators, or Unicode, so the server should not blindly rewrite them. Malformed or ambiguous transport encodings should be rejected. Tests should confirm that literal slashes, encoded separators, unusual Unicode, and repeated encodings either identify exactly the intended object in every layer or are safely rejected. An opaque stable object ID is preferable when the storage architecture supports it.

10. How would you keep client-side restaurant prices and customization rules from becoming an authorization boundary?SecurityEasyAmazon

Question Details

A restaurant-ordering frontend displays prices and lets users choose option groups before submitting a cart. Identify the untrusted browser, the protected order total and inventory rules, and the attacker who can modify JavaScript state or send a handcrafted request. Explain what the client may calculate for immediate feedback, what the server must recompute and authorize, how menu and price versions are bound to submission, and how the UI handles a changed-price or invalid-option response without silently placing a different order. Include quantity limits, unavailable items, discounts, idempotent submission, error messages that do not reveal internal rules, and tests that alter hidden fields and request bodies. Do not treat disabled controls or minified code as enforcement.

Short Interview Answer (30-60 seconds)

I would treat browser prices and customization state as display-only hints. The server must reload trusted menu data, authorize every item and option, enforce quantities and discounts, recompute the total, validate menu versions, and reject changed orders until the user reviews and confirms them.

Detailed Explanation

The main idea is simple: a customer can change anything stored in the browser, so the restaurant must never trust the price or choices sent from the page. The page can still show a quick total and help the customer choose valid options. Before accepting the order, the restaurant's system must check the real menu, current price, allowed choices, quantity, availability, and discounts again. If something important changed, it should clearly tell the customer and ask for approval instead of quietly placing a different order.

Useful Questions to Ask the Interviewer
  1. Should submission fail whenever the overall menu version changes, or only when a change affects an item, option, price, discount, or rule used by this cart?
  2. Are discounts determined entirely by server-side promotion rules, or can the client submit a promotion code or identifier for the server to validate?
  3. Should unavailable items block the whole cart, or should the API return item-level problems so the customer can repair the cart?
  4. What behavior is expected for duplicate submissions caused by retries, double-clicks, or uncertain network responses?
How would you keep client-side restaurant prices and customization rules from becoming an authorization boundary? diagram
How to Explain It in an Interview

I would start with the trust boundary. The browser is untrusted because the user controls it. A user can change JavaScript state, edit hidden fields, enable disabled controls, call application functions manually, alter a network request, or skip the UI and send a handcrafted request. Minified or bundled JavaScript does not change this. The protected values are the chargeable order total, current item availability, quantity limits, allowed customization combinations, discount eligibility, and any rule that determines what the restaurant will fulfill or charge.

Authentication and authorization are different. Authentication establishes who the user is. Authorization decides whether a requested operation is allowed. Even an authenticated customer can modify a request, so authentication does not make browser-supplied prices, quantities, discounts, or option IDs trustworthy. The trusted server must enforce authorization.

The frontend can still calculate an estimated total and validate selections for immediate feedback. For example, it can show the price impact of adding an option, prevent normal users from choosing too many options, display an unavailable state, and warn when a required option is missing. These checks improve usability and reduce unnecessary requests, but they are not security controls. A disabled button, hidden input, client-side validation rule, or minified script can always be bypassed.

When the user submits the cart, I would send stable identifiers instead of treating displayed values as authority. A request can contain item IDs, selected option IDs, quantities, a promotion code or promotion identifier when applicable, and a menu or price version representing the data the customer reviewed. The client may also send its displayed total for diagnostics or mismatch detection, but the server must never use that client total as the amount to charge.

The submitted version must be bound to that checkout attempt. The server should validate the cart against authoritative menu data and determine whether the relevant menu, price, option, availability, or promotion rules have changed since the version the customer reviewed. A simple implementation can require an exact current version match. A more precise implementation can allow unrelated menu changes while still rejecting any change that affects the submitted cart. The important security rule is that the client cannot choose which authoritative rules apply.

For every submitted item, the server should verify that the item exists, belongs to the intended restaurant or ordering context, is currently orderable, and satisfies authoritative quantity limits. A quantity such as zero, a negative number, a fractional value when only whole units are allowed, or an excessive value must be rejected according to the server's rules rather than accepted because the frontend normally prevents it.

For customizations, the server should verify that every selected option actually belongs to an allowed option group for the selected item. It should enforce required groups, minimum and maximum selection counts, duplicate-selection rules, mutually incompatible choices, and any other authoritative combination rule. An attacker must not be able to copy an inexpensive option ID from one item into another item or submit an option that the UI did not display.

Availability must also be checked on the server at submission time. An item or option that was visible when the page loaded may become unavailable before checkout. The server should reject or return a conflict for that item rather than trusting the older browser state.

Discounts are another authorization decision. The browser may submit a promotion code or identifier, but it must not decide the discount amount. The server should evaluate the current promotion rules, including applicability, expiry, eligibility, usage limits, and the items to which the discount applies, and calculate the discount from trusted data.

After all validation succeeds, the server recomputes the complete total from authoritative item prices, option prices, quantities, discounts, and other applicable server-owned pricing rules. The client-calculated total is only an estimate for the user interface. The recomputed server total is authoritative.

If a relevant price, option rule, discount, or availability state changed, the server should not silently transform the cart and place a different order. It should return a structured conflict response describing only the customer-visible changes needed to repair or review the cart. The UI can then replace its stale display with authoritative server-provided values, highlight the affected items, and explain messages such as 'The price of this item changed' or 'This option is no longer available.' If the resulting order is materially different from what the customer approved, the user should explicitly review and submit it again.

The server should fail closed. An invalid option combination, excessive quantity, unavailable item, invalid discount, stale relevant version, or inconsistent cart should prevent order creation until the request is valid. Error responses should be useful to the customer without revealing internal implementation details, database structure, stack traces, secret rule identifiers, fraud thresholds, or other sensitive logic. Logs should record enough information for investigation, such as request IDs, validation categories, version mismatches, and high-level failure reasons, while avoiding secrets, payment credentials, authentication tokens, and unnecessary personal information.

Order creation should also be idempotent. Idempotency means that retrying the same logical checkout operation does not create multiple orders. The client can include a unique idempotency key for a checkout attempt and reuse that same key when retrying because of a timeout, uncertain response, or double-click. The trusted server must enforce the guarantee by associating the key with the logical request and its result. Repeating the same accepted operation should return the existing outcome rather than creating another order. If the customer changes the cart after a price or validation conflict and confirms a new version, that is a new logical checkout attempt and should use a new idempotency key.

I would verify these controls by testing outside the normal UI path. Tests should modify hidden fields, change JavaScript state, alter displayed prices, submit a fake total, send negative or excessive quantities, use malformed quantity values, select option IDs from another item, omit required options, exceed option-group limits, create incompatible combinations, reuse invalid or expired promotions, request unavailable items, submit an old menu or price version, and modify the request body directly. Each case should either be rejected or safely recalculated from authoritative server data.

I would also test race and retry behavior. For example, make an item unavailable after the customer loads the cart but before submission, change a relevant price between review and checkout, and send the same valid submission more than once with the same idempotency key. The tests should confirm that stale orders are not silently placed, duplicate orders are not created, safe customer-facing errors are returned, and logs contain useful diagnostics without secrets.

The main tradeoff is strict freshness versus user experience. Rejecting every cart whenever any menu version changes is simple and safe, but it can interrupt checkout because of an unrelated menu update. A more precise server can determine whether the submitted items and relevant rules actually changed. That requires additional versioning and validation logic, but it reduces unnecessary conflicts without moving any authorization decision into the browser.

Technical Approach
  1. Treat browser state, hidden fields, calculated totals, disabled controls, and request bodies as untrusted.
  2. Let the frontend calculate estimates and perform selection checks only for user experience.
  3. Submit stable item IDs, option IDs, quantities, discount references, and the menu or price version the user reviewed.
  4. Bind that version to the checkout attempt.
  5. On the trusted server, load authoritative restaurant menu, pricing, availability, customization, and promotion data.
  6. Validate item identity and restaurant context, current availability, quantity format and limits, required option groups, option membership, selection counts, incompatible combinations, and discount eligibility.
  7. Detect whether relevant authoritative data changed since the submitted version.
  8. Recompute the full total using only trusted server data.
  9. If relevant data changed or validation fails, return a structured safe error or conflict without creating the order.
  10. Let the UI display authoritative changes and require explicit confirmation before submitting a materially changed cart.
  11. Protect order creation with a server-enforced idempotency key.
  12. Log useful validation outcomes without secrets.
  13. Test by bypassing the UI and manipulating JavaScript state, hidden fields, versions, and request bodies.
Practical Insights

The browser work is small because it only calculates estimates and displays validation feedback. The important cost is on the server, which must load trusted data and validate the submitted cart. If there are n submitted items and selected options, validation is usually roughly proportional to n when authoritative records can be looked up efficiently. The server also needs temporary memory for the request and trusted data used during validation. Version tracking and idempotency require some persistent server-side storage and cleanup. The larger maintenance cost is keeping pricing, availability, customization, quantity, promotion, and version rules centralized so every client is checked against the same authoritative rules.

Why Interviewers Ask This

This question tests whether the candidate understands that browser-controlled state cannot enforce business-critical pricing or ordering rules. It evaluates trust-boundary reasoning, the difference between authentication and authorization, authoritative server-side validation, stale menu handling, discount and inventory enforcement, idempotent submission, safe failure behavior, and security testing against manipulated JavaScript state or handcrafted requests.

Common interview mistakes

Common mistakes include trusting a JavaScript-calculated total, assuming disabled controls enforce rules, trusting hidden fields, accepting an option ID without verifying that it belongs to the selected item, validating quantities only in the UI, accepting a client-supplied discount amount, trusting stale availability, silently replacing unavailable options, silently charging a changed price, and treating authentication as authorization. Other mistakes include relying on minified code as protection, creating the order before all authoritative checks finish, leaking internal business or fraud rules through error messages, logging secrets or tokens, retrying checkout without server-side idempotency, and testing only through the normal UI instead of sending manipulated request bodies.

Interview tip

Lead with one sentence: the browser is untrusted and the server is authoritative. Then walk through a single checkout from client-side estimate to server validation, authoritative price recomputation, version conflict handling, explicit user reconfirmation, and idempotent order creation. Explicitly say that disabled controls, hidden fields, and minified JavaScript improve neither authorization nor trust.

Interviewer may ask next
What should happen if the price changes between the time the user reviews the cart and the time they submit it?

The server should recompute the cart with the current authoritative price and detect that the relevant submitted menu or price version is stale. It should not silently charge the new amount. Instead, it should return a structured conflict with the customer-visible changed data, let the frontend update and highlight the affected cart entries, and require the customer to review and explicitly submit the updated order again.

How would you prevent a double-click or network retry from creating two identical restaurant orders?

Use server-enforced idempotency. A checkout attempt carries an idempotency key that is reused for retries of the same logical request. The server associates that key with the request and its resulting order or response, so a duplicate receives the existing outcome instead of creating another order. If the cart changes after a validation or price conflict and the customer confirms the new cart, that is a new logical operation and should use a new idempotency key.

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.