31 Netflix Java Developer Interview Questions & Answers

netflix icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. How would you validate a string of parentheses?CodingEasyNetflix

Question Details

Validate a string containing brackets and determine whether the nesting and order are correct.

Short Interview Answer (30-60 seconds)

I would use a stack to keep unmatched opening brackets. I process the string from left to right. When I see '(', '[', or '{', I push it onto the stack. For a closing bracket, I first check that the stack is not empty and that its top has the matching opening bracket. If not, I return false immediately. At the end, the stack must be empty. This runs in O(n) expected time and uses O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a string made only of parentheses, square brackets, and curly brackets. We need to return true only when every opening bracket has the correct closing bracket and the nesting order is valid. A stack fits this problem because the most recently opened bracket must be the first one closed. We push opening brackets onto the stack. For every closing bracket, we compare it with the current stack top. A mismatch makes the string invalid immediately. An empty stack at the end means every bracket was matched.

Useful Questions to Ask the Interviewer
  1. Should an empty string be considered valid?
  2. Can I assume the input contains only '(', ')', '[', ']', '{', and '}'?
How would you validate a string of parentheses? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string of bracket characters. We return a boolean value. We return true only when every opening bracket is closed by the same bracket type and in the correct nesting order. For example, "{[()]}" is valid, so the expected result is true.

2. Choose the stack and matching table

I use a stack to store opening brackets that have not been matched yet. The stack top is important because it represents the most recent unmatched opening bracket. I also use the matching table ) -> (, ] -> [, and } -> {. The central rule is that a closing bracket must match the current stack top.

3. Initialize the state

The stack starts empty: []. Traversal starts at index 0. The invariant is that the stack contains exactly the unmatched opening brackets seen so far, in nesting order. The top is the only opening bracket that can legally match the next closing bracket.

4. Walk through the example

For s = "{[()]}", index 0 contains '{'. The stack is empty, so we push '{'. The stack becomes ['{'] and processing continues.

At index 1, the character is '['. It is an opening bracket, so we push it. The stack changes from ['{'] to ['{','['].

At index 2, the character is '('. We push it. The stack changes from ['{','['] to ['{','[','('].

At index 3, the character is ')'. Before the check, the stack is ['{','[','(']. The matching table says ')' needs '('. The stack top is '(', so the pair is valid. We pop '('. The stack becomes ['{','['] and processing continues.

At index 4, the character is ']'. The stack is ['{','[']. The matching table says ']' needs '['. The top is '[', so we pop it. The stack becomes ['{'].

At index 5, the character is '}'. The stack is ['{']. The required opening bracket is '{'. It matches the top, so we pop it. The stack becomes [].

After all six characters are processed, stack.isEmpty() is true. We return true.

5. Explain why the result is correct

The stack always contains only unmatched opening brackets. They stay in the exact order in which they must later be closed. A closing bracket is valid only when it matches the current top. Popping a matching pair keeps the invariant true. If the stack is empty at the end, every opening bracket was matched in the correct order.

6. Explain the Java implementation

The Java code uses Deque<Character> with ArrayDeque as the stack. A Map<Character, Character> stores the required opening bracket for each closing bracket. Opening brackets are pushed with stack.push(). For a closing bracket, the code first checks stack.isEmpty(). If the stack is empty, it returns false immediately. Otherwise, it pops the top opening bracket and compares it with the required opening bracket from the map. A mismatch also returns false immediately. After the loop, it returns stack.isEmpty().

7. Explain complexity and edge cases

We process each character at most once and can stop early when the answer is already false. Stack push and pop are constant-time operations. The matching table contains only the three fixed closing-bracket mappings, so each lookup is constant time for this solution. The overall time is O(n), matching the diagram's expected O(n) bound. In the worst case, the stack can hold n opening brackets, so auxiliary space is O(n). Important cases are an empty string, a string starting with a closing bracket, mismatched nesting such as "([)]", and leftover opening brackets such as "(((".

Key Insight / Why This Solution Works

The key insight is that valid brackets must close in last-in, first-out order. That is exactly how a stack works. Every opening bracket is pushed onto the stack. When a closing bracket appears, it must match the opening bracket at the top. If the stack is empty or the types do not match, the string is invalid immediately. The invariant is: the stack contains exactly the unmatched opening brackets seen so far, in nesting order. If the stack is empty after the full string is processed, every pair was matched correctly.

Code
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;

public class Main {

    public static boolean isValid(String s) {
        // Store opening brackets that have not been matched yet.
        Deque<Character> stack = new ArrayDeque<>();

        // Map each closing bracket to the opening bracket it must match.
        Map<Character, Character> matching = Map.of(')', '(', ']', '[', '}', '{');

        // Process the input from left to right at most once.
        for (char ch : s.toCharArray()) {
            // Opening brackets wait on the stack for their closing bracket.
            if (ch == '(' || ch == '[' || ch == '{') {
                stack.push(ch);
            } else {
                // A closing bracket is invalid if no opening bracket is waiting.
                if (stack.isEmpty()) {
                    return false;
                }

                // Remove the most recent unmatched opening bracket.
                char opening = stack.pop();

                // Its type must match the current closing bracket.
                if (opening != matching.get(ch)) {
                    return false;
                }
            }
        }

        // The string is valid only when no opening brackets remain unmatched.
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        // Run the same verified example shown in the diagram.
        String s = "{[()]}";
        System.out.println(isValid(s)); // true
    }
}
Time & Space Complexity

Let n be the number of characters in the string. We process each character at most once and may stop early on a mismatch, so the overall time is O(n), consistent with the diagram's expected O(n) bound. Stack push and pop are O(1). The matching table has only three fixed entries, so its lookup is constant time here. Auxiliary space means extra memory used by the algorithm. In the worst case, every character can be an opening bracket, so the stack can grow to n entries. The auxiliary space is O(n).

Where it is used

This stack pattern is useful in parsers, compilers, code editors, template validators, and configuration-file checks. It is especially useful when nested items must close in the reverse order in which they were opened, such as brackets, nested expressions, and other last-in, first-out structures.

Why Interviewers Ask This

This problem checks whether you recognize a last-in, first-out pattern and choose a stack naturally. It also tests whether you can maintain a clear invariant while processing a string, handle invalid states early, and distinguish bracket type from bracket count. In Java, the interviewer can also see whether you know how to use Deque and ArrayDeque correctly. Finally, it tests whether you can explain O(n) time, O(n) auxiliary space, and important edge cases.

Common interview mistakes

A common mistake is checking only the number of opening and closing brackets. Equal counts do not prove that the nesting order is correct. Another mistake is popping without first checking whether the stack is empty. Candidates also sometimes forget to compare the closing bracket with the exact bracket type at the stack top. Another mistake is returning true without checking whether opening brackets remain in the stack. Finally, the push and pop order must stay last-in, first-out.

Interview tip

State the stack invariant before coding: the stack contains the unmatched opening brackets, and its top is the only bracket that may match the next closing bracket. That one sentence makes the push, pop, mismatch, and final empty-stack checks easy to justify.

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

The same stack logic still works. Process each incoming character as it arrives. Push opening brackets. For a closing bracket, check the stack and return or report invalid immediately if it is empty or the top type does not match. At the end of the stream, the stack must be empty. The total time is O(n) for n received characters, and the auxiliary space is O(n) in the worst case. The main tradeoff is that the input cannot be declared fully valid until the stream ends.

Can the auxiliary space be reduced below O(n)?

Not for the general single-pass stack solution when arbitrary nesting depth must be validated exactly. An input can contain n unmatched opening brackets before any closing brackets appear. Their types and order must be remembered so later closing brackets can be checked correctly. That requires O(n) auxiliary space in the worst case. The time remains O(n).

2. How would you implement Queue using Stacks?CodingEasyNetflix

Question Details

Implement a queue using stacks and explain how enqueue, dequeue, and empty work.

Short Interview Answer (30-60 seconds)

I would use two stacks: inStack for new elements and outStack for elements ready to leave the queue. enqueue pushes onto inStack. For dequeue, if outStack is empty, I move every element from inStack to outStack. This reverses the order and puts the oldest element on top. Then I pop from outStack. empty checks both stacks. enqueue is O(1), dequeue is O(1) amortized with O(n) worst case during a transfer, and auxiliary space is O(n).

Detailed Explanation

See the Code while reading this explanation.

The goal is to make a queue where the first value added is the first value removed. We use two stacks to do this. New values go into inStack. When we need the oldest value and outStack is empty, we move all waiting values into outStack. This reverses their order, so the oldest value becomes the next one removed. We keep using outStack until it is empty again.

Useful Questions to Ask the Interviewer
  1. Should dequeue throw an exception when the queue is empty?
  2. Should the queue support duplicate, zero, and negative integer values?
How would you implement Queue using Stacks? diagram
How to Explain It in an Interview
1. Choose the two-stack design

I keep two stacks named inStack and outStack. inStack stores newly added values. outStack stores values that are ready to leave in queue order. The important invariant is that whenever outStack is not empty, its top element is the current queue front.

2. Initialize the state

Both stacks start empty. Before any operation, inStack = [] and outStack = []. enqueue always pushes directly onto inStack. We do not move values during enqueue.

3. Walk through the exact example

The operations are enqueue(10), enqueue(20), enqueue(30), dequeue(), enqueue(40), dequeue(), empty().

After enqueue(10), inStack is [10] and outStack is []. After enqueue(20), inStack is [20, 10] and outStack is []. After enqueue(30), inStack is [30, 20, 10] and outStack is [].

The first dequeue sees that outStack is empty. We transfer all values from inStack to outStack. We pop 30, then 20, then 10 from inStack and push them onto outStack. This creates outStack = [10, 20, 30], with the top shown first. Now 10 is on top, so dequeue returns 10. The state becomes inStack = [] and outStack = [20, 30].

Next, enqueue(40) pushes 40 onto inStack. The state is inStack = [40] and outStack = [20, 30].

The second dequeue does not transfer anything because outStack already contains values. It pops 20 directly. The state becomes inStack = [40] and outStack = [30]. The returned value is 20.

Finally, empty() checks both stacks. They are not both empty, so it returns false. The returned results are [10, 20, false]. The remaining logical queue order is front -> [30, 40] -> back.

4. Explain why the result is correct

Moving all values from inStack to outStack reverses their order. The oldest queued value becomes the top of outStack. As long as outStack has values, its top is the next queue front. Newer values can enter inStack, but they cannot leave before the older values already stored in outStack.

5. Explain the Java implementation

The Java code uses Deque<Integer> with ArrayDeque for both stacks. enqueue calls push on inStack. dequeue first calls moveIfNeeded(). That helper transfers values only when outStack is empty. Then dequeue checks whether the queue is empty and otherwise pops the top of outStack. empty returns true only when both stacks are empty.

6. Explain complexity and edge cases

enqueue is O(1). One dequeue can take O(n) when it performs a transfer, but each element moves from inStack to outStack at most once. Because of that, dequeue is O(1) amortized across many operations. empty is O(1). The two stacks together use O(n) auxiliary space. Duplicate values, negative values, and zero work normally because queue behavior depends on order, not value uniqueness. The implementation throws NoSuchElementException if dequeue is called when the queue is empty.

Key Insight / Why This Solution Works

Use two stacks with different jobs. inStack receives every new value. outStack provides the queue front. When outStack is empty and a dequeue is requested, move every value from inStack to outStack. Moving values between stacks reverses their order, so the oldest queued element becomes the top of outStack. The central invariant is that whenever outStack is non-empty, its top is the current queue front. This avoids transferring values on every dequeue and lets each element move from inStack to outStack at most once.

Code
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;

public class Main {

    static class MyQueue {

        // New elements are pushed here first.
        private final Deque<Integer> inStack = new ArrayDeque<>();

        // When non-empty, the top is the current queue front.
        private final Deque<Integer> outStack = new ArrayDeque<>();

        public void enqueue(int x) {
            // Add the newest value to the input stack.
            inStack.push(x);
        }

        public int dequeue() {
            // Transfer values only if no front value is ready in outStack.
            moveIfNeeded();

            // If no value exists after the transfer attempt, the queue is empty.
            if (outStack.isEmpty()) {
                throw new NoSuchElementException("Queue is empty");
            }

            // The top of outStack is the oldest queued value.
            return outStack.pop();
        }

        public boolean empty() {
            // The queue is empty only when both stacks contain no elements.
            return inStack.isEmpty() && outStack.isEmpty();
        }

        private void moveIfNeeded() {
            // Keep the current outStack order until it has been fully consumed.
            if (outStack.isEmpty()) {
                // Reverse all waiting values so the oldest one becomes the top.
                while (!inStack.isEmpty()) {
                    outStack.push(inStack.pop());
                }
            }
        }
    }

    public static void main(String[] args) {
        MyQueue queue = new MyQueue();

        // Run the exact operation sequence shown in the diagram.
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);

        // outStack is empty, so values transfer and the oldest value, 10, leaves.
        System.out.println(queue.dequeue());

        // New values still enter inStack even while outStack has older values.
        queue.enqueue(40);

        // outStack already has the next front, so this returns 20 without transfer.
        System.out.println(queue.dequeue());

        // Values 30 and 40 remain, so the queue is not empty.
        System.out.println(queue.empty());
    }
}
Time & Space Complexity

enqueue takes O(1) time because it performs one push onto inStack. dequeue is O(1) amortized. One particular dequeue can take O(n) if outStack is empty and all waiting elements must move from inStack to outStack, but each element is transferred at most once before it is removed. empty takes O(1) because it only checks whether two stacks are empty. The auxiliary space is O(n) because all queue elements are stored across inStack and outStack.

Where it is used

This two-stack pattern is useful when queue behavior must be built using stack operations. It also teaches a useful design idea: delay a larger piece of work until it is needed, then reuse the result. Similar ideas appear in buffering and in systems that separate newly received items from items that are ready to process.

Why Interviewers Ask This

This problem checks whether you understand how one data structure can be built from another. The interviewer is looking for correct stack ordering, a clear invariant, and recognition that transferring values reverses their order to produce FIFO behavior. It also tests whether you avoid unnecessary transfers, handle an empty queue safely, write correct Java with Deque and ArrayDeque, and explain amortized complexity instead of incorrectly claiming that every dequeue is always O(1).

Common interview mistakes

A common mistake is transferring inStack into outStack on every dequeue. That does unnecessary work and loses the intended amortized performance. Another mistake is transferring new values while outStack still contains older values, which can break FIFO order. Candidates also sometimes check only one stack in empty(), even though values may exist in either stack. Another mistake is popping outStack without transferring first when it is empty. Finally, saying every dequeue is strictly O(1) is inaccurate because one dequeue can take O(n) during a transfer.

Interview tip

State the invariant early: whenever outStack is not empty, its top is the queue front. Then explain that you transfer from inStack only when outStack becomes empty. This makes both FIFO correctness and the O(1) amortized dequeue cost easy to explain.

Interviewer may ask next
Can you implement the same queue if dequeue must never perform an O(n) transfer?

Not with this exact lazy-transfer design while also keeping enqueue O(1). In the shown design, one dequeue may perform an O(n) transfer, although dequeue is O(1) amortized. A simple alternative can move that work to enqueue by rearranging elements when they are added, but then enqueue can take O(n). The main tradeoff is which operation pays the linear cost. Auxiliary space remains O(n).

What happens if the queue contains duplicate, negative, or zero values?

Nothing special is required. The algorithm depends only on insertion order, not on value uniqueness or sign. Each value is pushed and popped independently, so duplicates, negative values, and zero keep their FIFO order. The complexity stays O(1) for enqueue, O(1) amortized for dequeue with O(n) worst case during a transfer, and O(n) auxiliary space.

3. How would you reverse an integer?CodingEasyNetflix

Question Details

Reverse the digits of an integer while handling overflow correctly.

Short Interview Answer (30-60 seconds)

I would reverse the integer one digit at a time. I repeatedly take the last digit with x % 10, remove it with x / 10, and append it to a reversed value. Before appending, I check whether multiplying by 10 and adding the digit would overflow a 32-bit signed integer. If it would, I return 0. Otherwise, I continue until x becomes 0. This takes O(d) time and O(1) auxiliary space, where d is the number of digits.

Detailed Explanation

See the Code while reading this explanation.

The goal is to read the digits of the number from right to left and rebuild them in that order. For example, -1203 becomes -3021. We remove one last digit at a time and add it to a new number. Before adding each digit, we check that the new number will still fit inside the allowed 32-bit signed integer range. This method fits well because it works directly with the digits and needs no extra collection. It also avoids extra storage.

Useful Questions to Ask the Interviewer
  1. Should I return 0 if the reversed value goes outside the 32-bit signed integer range?
  2. Should negative numbers keep their negative sign in the reversed result?
How would you reverse an integer? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is one 32-bit signed integer. We must return the same digits in reverse order. If the reversed number would be smaller than -2,147,483,648 or larger than 2,147,483,647, we return 0. In the diagram, the example is x = -1203, and the expected result is -3021.

2. Choose the digit-by-digit algorithm

We do not need an array, stack, or string. We can work directly with the integer. The expression x % 10 gives the last digit. In Java, the remainder keeps the sign of the dividend, so -1203 % 10 gives -3. Then x / 10 removes that last digit because Java integer division truncates toward zero.

The main invariant is that reversed always stores the correct reverse of the digits already removed from x.

3. Initialize the state

We start with reversed = 0. The original x is -1203. We begin with the least-significant digit, which is the digit on the far right.

Before appending any new digit, we check whether reversed * 10 + digit would overflow. For the positive side, Integer.MAX_VALUE is 2,147,483,647. For the negative side, Integer.MIN_VALUE is -2,147,483,648. The important boundary value before multiplying by 10 is 214748364 or -214748364. At the positive boundary, the next digit must be at most 7. At the negative boundary, the next digit must be at least -8.

4. Walk through the example

Step 1: x is -1203 and reversed is 0. We calculate digit = -1203 % 10, so digit is -3. We reduce x to -120. The overflow check is safe. We append -3, so reversed becomes -3.

Step 2: x is -120. The next digit is 0. We reduce x to -12. The check is safe. We append 0, so reversed becomes -30.

Step 3: x is -12. The next digit is -2. We reduce x to -1. The check is safe. We append -2, so reversed becomes -302.

Step 4: x is -1. The next digit is -1. We reduce x to 0. The check is safe. We append -1, so reversed becomes -3021.

Now x == 0, so the loop stops and we return -3021.

5. Explain why the result is correct

Each loop removes exactly one trailing digit from x and appends that digit to reversed. Because reversed already contains the reversed form of all previously removed digits, adding the next removed digit keeps that property true. When x becomes 0, every digit has been processed. Therefore reversed contains the complete reversed integer. The overflow checks happen before the multiplication and addition, so an invalid 32-bit result is never created.

6. Explain the Java implementation

The method keeps one integer variable called reversed. Inside the loop, it gets the last digit with %, removes that digit with /= 10, checks the positive and negative overflow boundaries, and then appends the digit. Java division truncates toward zero, and % keeps the sign of x, so the same code works for both positive and negative inputs without separate sign handling.

7. Explain complexity and edge cases

If the input has d digits, the loop runs at most d times, so the time complexity is O(d). The algorithm uses only a few integer variables, so the auxiliary space is O(1). Important cases are negative inputs such as -123 -> -321, trailing zeros such as 120 -> 21, zero itself, and values such as 1534236469 whose reversed form would overflow and therefore return 0.

Key Insight / Why This Solution Works

The key idea is to build the answer from the least-significant digit to the most-significant digit. On each iteration, digit = x % 10 gives the next digit we need, and x /= 10 removes it from the remaining input. The invariant is that reversed always contains the correct reverse of the digits already removed from x. Before computing reversed * 10 + digit, we compare reversed and digit against the 32-bit boundaries. This prevents overflow instead of detecting it after it has already happened. No extra data structure is needed.

Code
public class Main {

    public static int reverse(int x) {
        // Store the reversed digits built so far.
        int reversed = 0;

        // Process one trailing digit at a time until no digits remain.
        while (x != 0) {
            // Extract the current last digit. Java keeps the sign of x here.
            int digit = x % 10;

            // Remove the digit we just extracted. Java division truncates toward zero.
            x /= 10;

            // Check the positive boundary before multiplying by 10 and appending.
            // If reversed is already too large, or it is exactly at the boundary
            // and the next digit is greater than 7, the result would overflow.
            if (
                reversed > Integer.MAX_VALUE / 10 ||
                (reversed == Integer.MAX_VALUE / 10 && digit > 7)
            ) {
                return 0;
            }

            // Check the negative boundary before multiplying by 10 and appending.
            // At Integer.MIN_VALUE / 10, the next digit cannot be less than -8.
            if (
                reversed < Integer.MIN_VALUE / 10 ||
                (reversed == Integer.MIN_VALUE / 10 && digit < -8)
            ) {
                return 0;
            }

            // Append the extracted digit after confirming that the operation is safe.
            reversed = reversed * 10 + digit;
        }

        // Every digit has been processed, so this is the final reversed value.
        return reversed;
    }

    public static void main(String[] args) {
        // Run the exact example shown in the diagram.
        int input = -1203;
        int result = reverse(input);

        // Expected output: -3021
        System.out.println(result);
    }
}
Time & Space Complexity

Let d be the number of digits in the integer. We process each digit at most once, so the time complexity is O(d). For a 32-bit integer, d is bounded, but O(d) clearly describes the work by number of digits. The algorithm only stores x, reversed, and the current digit. It does not create an array, string, stack, or other growing structure. Therefore the auxiliary space complexity is O(1).

Where it is used

This digit-processing pattern is useful when software needs to inspect or transform decimal digits without first converting the number to a string. Similar logic can be used for palindrome-number checks, digit sums, extracting individual digits, and other small numeric transformations where constant extra memory is useful.

Why Interviewers Ask This

This problem tests whether you can manipulate integer digits directly, maintain a simple loop invariant, and handle overflow safely in Java. The interviewer can see whether you understand division and remainder behavior for negative values, whether you place a safety check before a dangerous arithmetic operation, and whether your code handles edge cases such as zero and trailing zeros. It also tests whether you can explain O(d) time and O(1) auxiliary space accurately.

Common interview mistakes

A common mistake is checking for overflow only after calculating reversed * 10 + digit. At that point, the int may already have overflowed. Another mistake is using only the positive boundary and forgetting that Integer.MIN_VALUE ends in 8 while Integer.MAX_VALUE ends in 7. Candidates may also handle negative numbers separately even though Java division and remainder already make the same loop work. Another mistake is forgetting that trailing zeros disappear, so 120 becomes 21. Finally, converting to a string changes the approach and no longer matches this constant-extra-space digit algorithm.

Interview tip

Explain the overflow check before writing the append line. Say that you first prove reversed * 10 + digit is safe, and only then perform the multiplication and addition. This shows that you understand the main risk in the problem.

Interviewer may ask next
Could you solve this by converting the integer to a string first?

Yes. I could convert the digits to a string representation, reverse the digit characters while handling the sign, and then verify that the result fits in a 32-bit signed integer. That would still take O(d) time, but it would use O(d) extra space for the string or character data. The digit-by-digit solution in the diagram uses O(1) auxiliary space, so it avoids that extra memory.

Why do the overflow checks use 7 for the positive boundary and -8 for the negative boundary?

Integer.MAX_VALUE is 2,147,483,647, so if reversed is already 214748364, the final digit can be at most 7. Integer.MIN_VALUE is -2,147,483,648, so if reversed is -214748364, the final digit can be as low as -8. Any larger positive digit or smaller negative digit would move the result outside the 32-bit signed integer range. The time remains O(d) and the auxiliary space remains O(1).

4. How would you find the longest common prefix?CodingEasyNetflix

Question Details

Return the longest common prefix across a list of strings and explain the comparison strategy.

Short Interview Answer (30-60 seconds)

I would use vertical scanning. I take the first string as the reference and compare the same character position across every other string. If any string is too short or has a different character, I immediately return the part of the first string before that index. If every position matches, I return the whole first string. This works because all earlier positions are already confirmed. The time is O(n × m), with O(1) auxiliary space excluding the returned string.

Detailed Explanation

See the Code while reading this explanation.

We need to find the starting characters that every string shares. For example, with ["flower", "flow", "flight"], all three strings start with "f" and then "l". At index 2, "flower" and "flow" have "o", but "flight" has "i". So the answer is "fl". The diagram uses the first string as a reference. We compare one character position at a time across all other strings and stop as soon as a mismatch or shorter string appears.

Useful Questions to Ask the Interviewer
  1. Should I return an empty string when the input array is empty?
  2. Is the comparison case-sensitive?
  3. Can the input contain an empty string?
How would you find the longest common prefix? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of strings. We must return the longest prefix that appears at the beginning of every string. A prefix must start at index 0. We are not looking for characters that appear later in the strings.

For the example ["flower", "flow", "flight"], the required output is "fl".

2. Choose vertical scanning

I use the first string, "flower", as the reference string. I check one character position at a time. At each index i, I compare first.charAt(i) with the character at the same index in every other string.

The important invariant is simple: before processing index i, every character before i already matches in every string.

If another string ends before index i can be read, the common prefix cannot continue. If another string has a different character at index i, the common prefix also cannot continue. In either case, I return first.substring(0, i).

3. Initialize the state

The reference string is first = "flower". The outer loop starts at i = 0. The other strings are "flow" and "flight".

There is no extra collection or table. The algorithm only needs the current indices, the current character, and references to the input strings.

4. Walk through the example

At Step 1, i = 0. The current character is 'f'. flow[0] is 'f', and flight[0] is also 'f'. All strings match, so processing continues. The confirmed prefix is "f".

At Step 2, i = 1. The current character is 'l'. flow[1] is 'l', and flight[1] is also 'l'. All strings match again. The confirmed prefix is now "fl".

At Step 3, i = 2. The current character is 'o'. flow[2] is 'o', but flight[2] is 'i'. This is the first mismatch. The method immediately returns first.substring(0, 2), which is "fl". No later indices are processed.

5. Explain why the result is correct

Before each index i, every earlier character has already matched across every string. Therefore first.substring(0, i) is a valid common prefix.

When we find the first mismatch or the first string that is too short, no common prefix can extend beyond that position. Returning the part before that position is therefore correct. For the example, the first failure is at index 2, so "fl" is the longest common prefix.

6. Explain the Java implementation

The code first handles a null or empty array by returning "". It stores strs[0] as first. The outer loop visits each character position in first. The inner loop checks the same position in every remaining string.

The condition checks the string length before calling charAt(i). This prevents reading past the end of a shorter string. If a shorter string or mismatch is found, the code returns immediately. If every character in first matches across all strings, the method returns first.

7. Explain complexity and edge cases

Let n be the number of strings and m be the number of character positions examined up to the stopping point. For each position, the algorithm may compare against all n strings, so the time complexity is O(n × m). The algorithm uses O(1) auxiliary space, excluding the returned string.

Important edge cases are an empty array, one string, an empty string inside the array, a mismatch at index 0, and a shorter string that ends after some earlier characters already matched.

Key Insight / Why This Solution Works

The key idea is vertical scanning. Use the first string as the reference and compare one character position across all strings before moving to the next position. The invariant is that before processing index i, first.substring(0, i) already matches the beginning of every string. If another string is too short or has a different character at i, no longer common prefix is possible, so returning first.substring(0, i) is correct. If every position in the first string matches, then the entire first string is the longest common prefix.

Code
public class Main {

    public static String longestCommonPrefix(String[] strs) {
        // Handle null or empty input before trying to read the first string.
        if (strs == null || strs.length == 0) {
            return "";
        }

        // Use the first string as the reference for all character comparisons.
        String first = strs[0];

        // Move from left to right through each character in the reference string.
        for (int i = 0; i < first.length(); i++) {
            // This is the character every other string must match at index i.
            char current = first.charAt(i);

            // Compare the same character position in every remaining string.
            for (int j = 1; j < strs.length; j++) {
                // Check the length first so charAt(i) is never called out of bounds.
                // A shorter string or different character ends the common prefix.
                if (i >= strs[j].length() || strs[j].charAt(i) != current) {
                    // All positions before i have already matched in every string.
                    return first.substring(0, i);
                }
            }
        }

        // If every reference character matched, the whole first string is common.
        return first;
    }

    public static void main(String[] args) {
        // Run the exact example used in the approved diagram.
        String[] strs = { "flower", "flow", "flight" };

        // The first mismatch is at index 2, so the expected result is "fl".
        String result = longestCommonPrefix(strs);
        System.out.println(result);
    }
}
Time & Space Complexity

Let n be the number of strings. Let m be the number of character positions examined up to the stopping point. At each position, we may compare that character with every string. This gives O(n × m) time. The algorithm can stop early at the first mismatch or shorter string. It does not create a map, set, stack, queue, or other growing data structure, so its auxiliary space is O(1), excluding the returned string.

Where it is used

This pattern is useful when several strings must be compared from the beginning. Similar logic can be used in text processing, grouping paths by a shared beginning, comparing hierarchical names, and finding common prefixes in identifiers or configuration values.

Why Interviewers Ask This

This problem checks whether you can turn a simple string requirement into a clear loop structure. The interviewer can see whether you understand aligned character comparison, maintain a useful invariant, handle shorter strings safely, and stop as soon as the answer is known. It also tests basic Java string indexing, correct substring boundaries, edge-case reasoning, and whether you can explain the O(n × m) time and O(1) auxiliary space accurately.

Common interview mistakes

A common mistake is comparing whole strings instead of comparing characters at the same index. Another mistake is calling charAt(i) before checking whether a shorter string has index i, which can cause an exception. Candidates may also continue processing after the first mismatch even though the answer is already known. Another mistake is including the mismatching character in the returned substring. Finally, the complexity should match the nested comparisons: O(n × m), not O(n).

Interview tip

State the invariant before coding: every character before index i already matches across all strings. Then explain that the length check must happen before charAt(i), and that the algorithm returns immediately at the first mismatch because no longer common prefix can exist.

Interviewer may ask next
What happens if one of the strings is empty?

The same algorithm still works. If the first string is empty, the outer loop runs zero times and the method returns the empty first string. If a later string is empty, then at i = 0 the condition i >= strs[j].length() is true. The method returns first.substring(0, 0), which is "". This is correct because no non-empty prefix can be shared with an empty string. The algorithm stops immediately in that case, and the auxiliary space remains O(1).

What happens if the input contains only one string?

No algorithm change is needed. That string becomes the reference. The inner loop has no other strings to compare, so each outer-loop position completes without a mismatch. After the outer loop finishes, the method returns the whole first string. This is correct because a string is its own longest common prefix. For a string of length m, this implementation takes O(m) time and O(1) auxiliary space excluding the returned string.

5. How would you implement an LRU cache?CodingMediumNetflix

Question Details

Design an LRU cache with O(1) get and put operations and explain the supporting data structures.

Short Interview Answer (30-60 seconds)

I would combine a HashMap with a doubly linked list. The map stores each key to its list node, so I can find an entry in O(1) average time. The list keeps entries from most recently used to least recently used. On every successful get or update, I move that node to the front. When capacity is exceeded, I remove the node before the tail. Each get and put takes O(1) expected time, with O(capacity) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The cache can hold only a limited number of items. A get should return the stored value, or -1 when the key is missing. A put should add a new item or replace the value of an existing item. Whenever an item is used, it becomes the most recently used one. If adding a new item makes the cache too large, the least recently used item must be removed. We need one structure for quick key lookup and another structure for keeping the usage order.

Useful Questions to Ask the Interviewer
  1. Should get return -1 when the key is missing?
  2. Should updating an existing key also make it the most recently used key?
  3. Can I assume the capacity is at least 1?
How would you implement an LRU cache? diagram
How to Explain It in an Interview
1. Choose the two supporting data structures

I use a HashMap<Integer, Node> and a doubly linked list. The map stores key → node. This lets me find a cache entry in O(1) average time. The list stores recency order. The real node immediately after the dummy head is the most recently used entry. The real node immediately before the dummy tail is the least recently used entry.

The central invariant is that every live cache key appears exactly once in the map and exactly once in the list. The list is always ordered from most recently used to least recently used.

2. Initialize the cache

The map starts empty. The list contains only a dummy head and dummy tail. I connect head.next to tail and tail.prev to head. These dummy nodes avoid special cases when adding or removing nodes at either end of the list.

3. Handle get

For get(key), I first look up the key in the map. If the key is missing, I return -1 and leave the cache unchanged. If the key exists, I remove that node from its current list position and insert it immediately after the head. This makes it the most recently used entry. Then I return its value.

4. Handle put

For put(key, value), I first check whether the key already exists. If it does, I update the node's value and move that node to the front. If the key is new, I create a node, add it to the map, and insert it after the head. If the number of live entries is now greater than capacity, I remove tail.prev because that node is the least recently used entry. I also remove that key from the map.

5. Walk through the verified example

The capacity is 2. The cache starts empty.

1. put(1, 10): insert key 1. State becomes [1:10]. 2. put(2, 20): insert key 2 at the front. State becomes [2:20, 1:10]. 3. get(1): key 1 is found. Move it to the front and return 10. State becomes [1:10, 2:20]. 4. put(3, 30): insert key 3. Capacity is exceeded, so evict key 2. State becomes [3:30, 1:10]. 5. get(2): key 2 is no longer in the map, so return -1. State stays [3:30, 1:10]. 6. put(4, 40): insert key 4. Capacity is exceeded, so evict key 1. State becomes [4:40, 3:30]. 7. get(1): key 1 is missing, so return -1. State stays [4:40, 3:30]. 8. get(3): key 3 is found. Move it to the front and return 30. State becomes [3:30, 4:40]. 9. get(4): key 4 is found. Move it to the front and return 40. Final state becomes [4:40, 3:30].

The complete operation-by-operation output is [null, null, 10, null, -1, null, -1, 30, 40]. The values returned by get operations are [10, -1, -1, 30, 40].

6. Explain why it is correct, the Java implementation, and the cost

Every successful get and every put makes the affected key the most recently used entry by moving its node directly after the head. Because the list always stays in recency order, tail.prev is always the correct least recently used entry to evict. The map points directly to every live node, and an evicted key is removed from both structures.

In Java, helper methods remove a node and add a node after the head. Each helper changes only a fixed number of pointers. HashMap lookup, insertion, and removal are O(1) on average. Therefore each get and put takes O(1) expected time. The map and list hold at most the configured capacity, so auxiliary space is O(capacity).

Key Insight / Why This Solution Works

The key insight is that the two required jobs need two cooperating data structures. A HashMap gives quick access by key, but it does not directly maintain recency order. A doubly linked list maintains recency order and lets us remove or move a known node in O(1) time. The map stores key → node. The list is ordered from MRU near the dummy head to LRU near the dummy tail. The invariant is that every live key appears exactly once in both structures, and tail.prev is always the least recently used entry. A successful get or put moves the affected node to the front. If capacity is exceeded, removing tail.prev evicts exactly the correct entry.

Code
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {

    static class LRUCache {

        // Each node stores one cache entry and its position in the recency list.
        private static class Node {

            int key;
            int value;
            Node prev;
            Node next;

            Node(int key, int value) {
                this.key = key;
                this.value = value;
            }
        }

        private final int capacity;

        // Maps each live key directly to its linked-list node for average O(1) lookup.
        private final Map<Integer, Node> cache = new HashMap<>();

        // Dummy nodes remove special cases at the front and back of the list.
        private final Node head = new Node(0, 0);
        private final Node tail = new Node(0, 0);

        public LRUCache(int capacity) {
            this.capacity = capacity;

            // The cache starts with no real entries: head <-> tail.
            head.next = tail;
            tail.prev = head;
        }

        public int get(int key) {
            // Look up the node directly instead of scanning the linked list.
            Node node = cache.get(key);

            if (node == null) {
                // A miss returns -1 and does not change recency order.
                return -1;
            }

            // A successful access makes this key the most recently used entry.
            moveToFront(node);
            return node.value;
        }

        public void put(int key, int value) {
            Node node = cache.get(key);

            if (node != null) {
                // Update the existing value and refresh this key's recency.
                node.value = value;
                moveToFront(node);
                return;
            }

            // Add a new key to both structures and place it at the MRU position.
            Node fresh = new Node(key, value);
            cache.put(key, fresh);
            addAfterHead(fresh);

            if (cache.size() > capacity) {
                // The real node before the dummy tail is always the LRU entry.
                Node lru = tail.prev;

                // Remove the evicted entry from both the list and the map.
                remove(lru);
                cache.remove(lru.key);
            }
        }

        private void moveToFront(Node node) {
            // Detach the node from its old recency position.
            remove(node);

            // Insert it directly after head, which is the MRU position.
            addAfterHead(node);
        }

        private void addAfterHead(Node node) {
            // Save the old first node through node.next before changing head.next.
            node.next = head.next;
            node.prev = head;

            // Connect the old first node back to the inserted node.
            head.next.prev = node;

            // Connect head forward to the inserted node.
            head.next = node;
        }

        private void remove(Node node) {
            // Bypass this node from both directions without scanning the list.
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
    }

    public static void main(String[] args) {
        LRUCache cache = new LRUCache(2);
        List<String> output = new ArrayList<>();

        // Run the exact operation sequence from the diagram.
        cache.put(1, 10);
        output.add("null");

        cache.put(2, 20);
        output.add("null");

        output.add(String.valueOf(cache.get(1)));

        cache.put(3, 30);
        output.add("null");

        output.add(String.valueOf(cache.get(2)));

        cache.put(4, 40);
        output.add("null");

        output.add(String.valueOf(cache.get(1)));
        output.add(String.valueOf(cache.get(3)));
        output.add(String.valueOf(cache.get(4)));

        // Expected output: [null, null, 10, null, -1, null, -1, 30, 40]
        System.out.println(output);
    }
}
Time & Space Complexity

Each get and put takes O(1) expected time. Java HashMap lookup, insertion, and removal are O(1) on average, with normal hashing and collision caveats. Once we already have a node reference, the doubly linked list can remove that node or insert it after the head in O(1) time because only a fixed number of pointers change. The cache stores at most capacity live entries in the map and list, so auxiliary space is O(capacity).

Where it is used

This pattern is useful when software has limited cache space and wants to keep recently used data available. Examples include application data caches, API response caches, database page caches, image caches, and other systems where older unused entries should be removed before recently accessed entries.

Why Interviewers Ask This

This problem tests whether you can combine two data structures to satisfy a strict performance goal. The interviewer wants to see that a HashMap alone does not maintain recency order and a linked list alone does not provide fast key lookup. They also evaluate whether you can maintain a clear invariant, update doubly linked list pointers safely, evict the correct node, keep the map and list consistent, write correct Java, and explain expected O(1) hash-based performance accurately.

Common interview mistakes
  1. Forgetting to move a node to the front after a successful get. That makes the recency order wrong.
  2. Updating an existing value without refreshing its recency. A put on an existing key must also make that key most recently used.
  3. Evicting tail instead of tail.prev. The tail is only a dummy node. tail.prev is the real least recently used entry.
  4. Removing an evicted node from the list but forgetting to remove its key from the HashMap. The map and list would no longer represent the same live entries.
  5. Updating prev and next pointers incorrectly and breaking the doubly linked list.
  6. Claiming guaranteed O(1) HashMap behavior. Java HashMap operations are normally O(1) on average, so the cache operations are described as O(1) expected time.
Interview tip

State the invariant before writing code: the map stores key → node, the list is ordered MRU to LRU, and tail.prev is always the entry to evict. Then write remove and addAfterHead as small helpers. Once those helpers are correct, get and put become short and much easier to explain.

Interviewer may ask next
What happens when the cache capacity is 1?

The same algorithm works without any structural change. The list can contain only one live node between the dummy head and tail. If a different new key is inserted, the size becomes greater than 1, so tail.prev identifies the previous entry and that entry is evicted. Each get and put remains O(1) expected time. Auxiliary space is O(capacity), which is O(1) when the capacity is exactly 1.

What is the worst-case behavior of the HashMap part of this design?

The design describes get and put as O(1) expected time because Java HashMap lookup, insertion, and removal are O(1) on average. Heavy hash collisions can make an individual map operation slower than the average case, so O(1) should not be presented as an unconditional worst-case guarantee. The doubly linked list operations still change only a fixed number of pointers and remain O(1). The design uses O(capacity) extra memory for the map and list nodes.

6. How would you solve Meeting Rooms II?CodingMediumNetflix

Question Details

Determine the minimum number of meeting rooms required for overlapping intervals.

Short Interview Answer (30-60 seconds)

I would sort the meetings by start time and use a min heap to store one end time for each allocated room. The smallest value is the room that becomes available first. For each meeting, if its start time is at least that smallest end time, I reuse that room by removing the old end time. Then I add the current meeting's end time. The final heap size is the minimum rooms needed. Time is O(n log n), with O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given meeting time intervals. Each interval has a start time and an end time. We need the smallest number of rooms that can hold all meetings without putting overlapping meetings in the same room. A room can be reused when one meeting ends exactly when another starts. I sort the meetings by start time and keep one end time for each allocated room in a min heap. This makes it easy to find the room that becomes available first.

Useful Questions to Ask the Interviewer
  1. Can a room be reused when one meeting starts exactly when another meeting ends?
  2. Can the input be empty?
  3. Do you only want the minimum number of rooms, or do you also want the actual room assignments?
How would you solve Meeting Rooms II? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of meeting intervals. Each interval is [start, end]. The output is one integer: the minimum number of rooms required. The diagram uses [[0,10],[5,15],[10,20],[20,30]]. The expected output is 2. Meetings [0,10] and [5,15] overlap, so at least two rooms are needed.

2. Choose the algorithm and data structure

First, sort the meetings by start time. Then use a min heap. In Java, PriorityQueue is a min heap by default. Each heap value is the current end time assigned to one allocated room. The smallest heap value is the earliest end time among those rooms. If the next meeting starts at or after that value, that room can be reused. Otherwise, another room must be allocated.

3. Initialize the state

After sorting, the intervals are [[0,10],[5,15],[10,20],[20,30]]. Start with an empty min heap. Put the first meeting's end time, 10, into the heap. The heap becomes [10], so one room is allocated. The invariant is that the min heap lets us find the allocated room with the earliest end time.

4. Walk through the example

Step 0: Process [0,10]. Heap before: []. This is initialization, so add 10. Heap after: [10]. Rooms allocated: 1.

Step 1: Process [5,15]. Heap before: [10]. Compare start 5 with earliest end 10. Since 5 < 10, the existing room is still busy. Add 15. Heap after: [10,15]. Rooms allocated: 2.

Step 2: Process [10,20]. Heap before: [10,15]. Compare start 10 with earliest end 10. Since 10 >= 10, the room ending at 10 can be reused. Remove 10 and add 20. Heap after: [15,20]. Rooms allocated: 2.

Step 3: Process [20,30]. Heap before: [15,20]. Compare start 20 with earliest end 15. Since 20 >= 15, that room can be reused. Remove 15 and add 30. Heap after: [20,30]. Rooms allocated: 2.

All meetings are now processed. The heap size is 2, so the final answer is 2.

5. Explain why the result is correct

The heap always exposes the earliest end time among the allocated rooms. If the next meeting starts before that time, then every allocated room is still unavailable for that meeting, so another room is necessary. If the meeting starts at or after that time, the earliest finishing room can safely be reused. This keeps the number of allocated rooms as small as possible. In the example, one valid assignment is Room A: [0,10] -> [10,20] -> [20,30], and Room B: [5,15].

6. Explain the Java implementation

The code first handles null or empty input by returning 0. It sorts intervals by start time. It creates a PriorityQueue<Integer> that stores room end times. The first meeting end is inserted. For each remaining meeting, the code compares its start with minHeap.peek(). If start >= minHeap.peek(), it removes that earliest end because the room can be reused. It then inserts the current meeting's end time. After all meetings are processed, minHeap.size() is returned.

7. Explain complexity and edge cases

Sorting costs O(n log n). Each heap insertion or removal costs O(log n). Across all meetings, the total time is O(n log n). The heap can contain up to n end times, so auxiliary space is O(n). Important cases are empty input, touching intervals that can reuse the same room, identical overlapping intervals that may require several rooms, and one long meeting that overlaps several short meetings.

Key Insight / Why This Solution Works

The key idea is to process meetings in start-time order while tracking the end times of allocated rooms. A min heap stores one end time for each allocated room. Its smallest value is the earliest room end time, so it tells us which room can be reused first. If the next meeting starts at or after that end time, we remove it and reuse that room. Otherwise, we allocate another room by adding the new end time without removing anything. The central invariant is that the min heap exposes the earliest end time among the allocated rooms.

Code
import java.util.Arrays;
import java.util.PriorityQueue;

public class Main {

    public static int minMeetingRooms(int[][] intervals) {
        // Empty or missing input needs no meeting rooms.
        if (intervals == null || intervals.length == 0) {
            return 0;
        }

        // Process meetings in ascending start-time order.
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        // Each heap value is an end time associated with one allocated room.
        // Java PriorityQueue is a min heap, so peek() returns the earliest end time.
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();

        // The first meeting allocates the first room.
        minHeap.offer(intervals[0][1]);

        // Process every remaining meeting in sorted start-time order.
        for (int i = 1; i < intervals.length; i++) {
            int start = intervals[i][0];
            int end = intervals[i][1];

            // If the next meeting starts when or after the earliest room becomes free,
            // remove that old end time because the same room can be reused.
            if (start >= minHeap.peek()) {
                minHeap.poll();
            }

            // Record the current meeting's end time for its assigned room.
            // If no end time was removed, this increases the allocated room count.
            minHeap.offer(end);
        }

        // One heap entry remains for each room that had to be allocated.
        return minHeap.size();
    }

    public static void main(String[] args) {
        // Use the exact verified example from the diagram.
        int[][] intervals = { { 0, 10 }, { 5, 15 }, { 10, 20 }, { 20, 30 } };

        // Expected output: 2
        System.out.println(minMeetingRooms(intervals));
    }
}
Time & Space Complexity

Let n be the number of meetings. Sorting the intervals by start time costs O(n log n). For each remaining meeting, we insert one end time into the min heap, and we may remove one end time. Each heap insertion or removal costs O(log n). Therefore, the total time is O(n log n). The heap can grow to contain up to n end times in the worst case, so the auxiliary space is O(n).

Where it is used

This pattern is useful when several time-based tasks must share a limited resource. Examples include meeting-room allocation, classroom scheduling, machine scheduling, worker assignment, and similar systems where the earliest available resource should be reused before creating another one.

Why Interviewers Ask This

This problem checks whether you recognize an interval-scheduling pattern and choose a suitable data structure. The interviewer can evaluate your understanding of sorting, min heaps, overlap rules, and resource reuse. It also tests whether you can maintain a clear invariant, handle the start == end case correctly, write clean Java with PriorityQueue, and explain why the implementation takes O(n log n) time and O(n) auxiliary space.

Common interview mistakes

A common mistake is treating start == earliestEnd as an overlap. The correct reuse condition is start >= earliestEnd because a room can be reused when one meeting ends exactly when another starts. Another mistake is using a max heap instead of a min heap. We need the earliest end time, so Java PriorityQueue should keep its default min-heap ordering. Candidates can also forget to sort by start time, forget to remove the earliest end before reusing its room, or claim O(n) time while ignoring the O(n log n) sorting and heap work.

Interview tip

State the heap invariant early: the heap stores one end time for each allocated room, and its smallest value is the earliest room end time. Then use start >= earliestEnd to explain exactly when a room can be reused.

Interviewer may ask next
How would you return the actual room assignment instead of only the number of rooms?

Keep the same start-time sorting and min-heap idea, but store both the room ID and its current end time in each heap entry. When the earliest end time is <= the next start, remove that entry and reuse its room ID. Otherwise, create a new room ID. Then insert the current meeting with its assigned room ID and new end time. Correctness is preserved because the earliest finishing room is still reused first. Time remains O(n log n), and the heap plus assignments use O(n) space.

What happens if many meetings have exactly the same start and end times?

They overlap with each other, so they cannot share a room. After sorting, the first interval adds one end time. For every following identical interval, its start is still less than the earliest end time, so nothing is removed and another end time is added. If there are k identical overlapping meetings, the algorithm allocates k rooms. The overall complexity remains O(n log n) time and O(n) auxiliary space.

7. How would you solve Trapping Rain Water?CodingHardNetflix

Question Details

Compute how much water is trapped after rainfall and explain the boundary tracking approach.

Short Interview Answer (30-60 seconds)

I use two pointers, one at each end of the height array. I keep leftMax and rightMax as the tallest walls seen from each side. At every step, I process the lower side because the opposite side already gives a high enough boundary. I either update that side's maximum or add the difference as trapped water, then move that pointer inward. Each index is processed at most once. The time complexity is O(n), and the auxiliary space is O(1).

Detailed Explanation

See the Code while reading this explanation.

We are given a row of bars with different heights. Rain can stay above a shorter bar when taller bars form boundaries around it. We need to find the total amount of water that remains after the rain. I start from both ends and move toward the middle. I remember the tallest bar seen from each side. By always handling the lower current side, I can decide its water immediately without storing extra information for every position. This gives the required total in one pass with constant extra memory.

Useful Questions to Ask the Interviewer
  1. Can I assume the height values are non-negative?
  2. Should I return only the total trapped water, not the amount stored at each index?
  3. Is constant auxiliary space preferred if we can still keep O(n) time?
How would you solve Trapping Rain Water? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an integer array called height. Each element is the height of one bar, and each bar has width 1. The output is one integer: the total number of water units trapped between the bars. For the example height = [0,1,0,2,1,0,1,3,2,1,2,1], the expected result is 6.

2. Choose the two-pointer boundary approach

I keep left at the start and right at the end. I also keep leftMax and rightMax. leftMax is the tallest bar already seen from the left. rightMax is the tallest bar already seen from the right. If height[left] <= height[right], the current right bar already provides a boundary at least as high as height[left]. That means the final water at the left index depends only on leftMax. Otherwise, the same reasoning applies from the right using rightMax.

3. Initialize the state

For the diagram example, left starts at index 0 with height 0. right starts at index 11 with height 1. leftMax = 0, rightMax = 0, and water = 0. The loop continues while left < right.

4. Walk through the example

Step 1: left is index 0 with height 0, and right is index 11 with height 1. Since 0 <= 1, process the left side. leftMax remains 0. Move left to 1. Total water is 0.

Step 2: left is index 1 with height 1. Since 1 <= 1, process the left side. Update leftMax from 0 to 1. Move left to 2. Total water is still 0.

Step 3: left is index 2 with height

  1. leftMax is 1, so this index traps 1 - 0 = 1 unit. Total becomes
  2. Move left to 3.

Step 4: left is index 3 with height 2, and right is index 11 with height 1. Since the right side is lower, process it. Update rightMax from 0 to 1. Move right to 10. Total stays 1.

Step 5: left is index 3 with height 2, and right is index 10 with height 2. The code uses <=, so it processes the left side. Update leftMax from 1 to 2. Move left to 4. Total stays 1.

Step 6: left is index 4 with height

  1. leftMax is 2, so add 2 - 1 = 1 unit. Total becomes
  2. Move left to 5.

Step 7: left is index 5 with height 0. Add 2 - 0 = 2 units. Total becomes 4. Move left to 6.

Step 8: left is index 6 with height 1. Add 2 - 1 = 1 unit. Total becomes 5. Move left to 7.

Step 9: left is index 7 with height 3, and right is index 10 with height 2. Process the right side. Update rightMax from 1 to 2. Move right to 9. Total stays 5.

Step 10: right is index 9 with height 1. rightMax is 2, so add 2 - 1 = 1 unit. Total becomes 6. Move right to 8.

Step 11: right is index 8 with height 2. rightMax is already 2, so it remains 2. Move right to 7. Now left == right, so the loop stops. The water contributions are index 2 -> 1, index 4 -> 1, index 5 -> 2, index 6 -> 1, and index 9 -> 1. Their sum is 1 + 1 + 2 + 1 + 1 = 6.

5. Explain why the result is correct

The invariant is that leftMax is the tallest boundary seen so far from the left, and rightMax is the tallest boundary seen so far from the right. When height[left] <= height[right], the right side already guarantees a boundary at least as high as the current left bar. Therefore, the final water at left can be decided using leftMax. When the right side is lower, the same argument lets us decide the final water at right using rightMax. Once an index is processed, its trapped-water amount never needs to change.

6. Explain the Java implementation

The Java method first returns 0 for a null array or an array with fewer than three bars. It initializes left, right, leftMax, rightMax, and water. Inside the loop, it compares the two endpoint heights. It processes only the lower side, updates that side's running maximum or adds the boundary gap to water, and then moves only that pointer inward. When the two pointers meet, it returns water. The example prints 6.

7. Explain complexity and edge cases

Each pointer moves in only one direction, toward the middle. Therefore, each position is processed at most once, giving O(n) time. The algorithm stores only a fixed number of integer variables, so auxiliary space is O(1). Fewer than three bars trap 0 water. Strictly increasing bars, strictly decreasing bars, and equal-height bars also trap 0. Zeros and deep valleys are handled by the same boundary calculation.

Key Insight / Why This Solution Works

The key insight is that we can decide trapped water from the lower current side without knowing every future boundary. leftMax stores the tallest bar seen so far from the left, and rightMax stores the tallest bar seen so far from the right. The invariant is that if height[left] <= height[right], the current right side already provides a boundary at least as high as height[left], so the final water at left depends only on leftMax. Otherwise, the final water at right depends only on rightMax. After processing that index, we move only its pointer inward. This gives the same boundary information using two running maxima instead of extra per-index storage.

Code
public class Main {

    public static int trap(int[] height) {
        // Fewer than three bars cannot form a valley with boundaries on both sides.
        if (height == null || height.length < 3) {
            return 0;
        }

        // Start one pointer at each end of the array.
        int left = 0;
        int right = height.length - 1;

        // Track the tallest boundary seen so far from each direction.
        int leftMax = 0;
        int rightMax = 0;

        // Store the total trapped water found so far.
        int water = 0;

        // Process indices until the two pointers meet.
        while (left < right) {
            // The lower current side can be finalized because the opposite
            // current bar already provides a boundary at least that high.
            if (height[left] <= height[right]) {
                // If this is a new left boundary, update leftMax.
                if (height[left] >= leftMax) {
                    leftMax = height[left];
                } else {
                    // Otherwise, the gap below leftMax is trapped water.
                    water += leftMax - height[left];
                }

                // This left index is complete, so move inward.
                left++;
            } else {
                // If this is a new right boundary, update rightMax.
                if (height[right] >= rightMax) {
                    rightMax = height[right];
                } else {
                    // Otherwise, the gap below rightMax is trapped water.
                    water += rightMax - height[right];
                }

                // This right index is complete, so move inward.
                right--;
            }
        }

        // All necessary indices have been processed.
        return water;
    }

    public static void main(String[] args) {
        // Exact example used in the approved diagram.
        int[] height = { 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 };

        // Expected output: 6.
        System.out.println(trap(height));
    }
}
Time & Space Complexity

The time complexity is O(n). The left pointer only moves to the right, and the right pointer only moves to the left. No pointer moves backward, so each position is processed at most once. The auxiliary space is O(1). Auxiliary space means extra memory used by the algorithm. We keep only left, right, leftMax, rightMax, and water, so the amount of extra memory does not grow with the input size.

Where it is used

This two-pointer boundary pattern is useful for array problems where information from both ends controls the answer and one side can be finalized safely before the other. It is especially useful when we want linear processing with constant extra memory instead of storing boundary information for every position.

Why Interviewers Ask This

This problem tests whether you can recognize a two-pointer boundary pattern and explain why it is correct. The interviewer wants to see whether you can maintain leftMax and rightMax, decide which side is safe to process, move the correct pointer, and calculate water without double counting. It also tests whether your Java code matches your reasoning and whether you can explain the O(n) time and O(1) auxiliary space accurately while handling important edge cases.

Common interview mistakes

One common mistake is processing the taller side instead of the lower side. That removes the reason the current water amount can be finalized safely. Another mistake is adding water before first checking whether the current bar creates a new leftMax or rightMax. Candidates may also move both pointers in one iteration instead of moving only the side that was processed. Another mistake is using extra prefix or suffix arrays but still claiming O(1) auxiliary space. Finally, this exact implementation processes the left side when height[left] <= height[right], so the walkthrough and code must use the same equality rule.

Interview tip

State the invariant before writing the loop: if the left bar is lower or equal, the right side already guarantees a sufficient boundary, so the left index can be finalized using leftMax. Then write the right-side branch as the mirror of that reasoning.

Interviewer may ask next
Could you solve the same problem using extra arrays instead of two pointers?

Yes. Build a prefixMax array where prefixMax[i] is the tallest bar from index 0 through i, and a suffixMax array where suffixMax[i] is the tallest bar from i through the end. Then water at index i is max(0, min(prefixMax[i], suffixMax[i]) - height[i]). Correctness comes from using the tallest available boundary on each side of every index. The time complexity remains O(n), but auxiliary space becomes O(n). The tradeoff is simpler per-index calculation but more memory.

What happens with fewer than three bars or with monotonic heights?

The trapped-water total is 0. Fewer than three bars cannot have an interior position with boundaries on both sides. A strictly increasing array has no closed valley because every earlier bar lacks a sufficiently tall boundary on its left, and a strictly decreasing array has the symmetric problem on its right. Equal-height bars also trap 0. The code handles fewer than three bars directly, while the normal two-pointer processing naturally returns 0 for the monotonic and equal-height cases.

8. How would you implement Decode Ways?CodingHardNetflix

Question Details

Count the number of valid decodings for a digit string and explain how you handle invalid prefixes.

Short Interview Answer (30-60 seconds)

I would use dynamic programming. I let dp[i] store the number of valid decodings for the first i characters. I start with dp[0] = 1 and dp[1] = 1, then move left to right. At each position, I add dp[i - 1] if the last digit is 1 to 9, and dp[i - 2] if the last two digits are 10 to 26. Invalid prefixes add nothing. The time is O(n), with O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given a string that contains digits. Each number from 1 to 26 can represent one letter. We need to count how many different valid ways the whole string can be read as letters. Zero needs special care because it cannot represent a letter by itself. The main idea is to build the answer from left to right. For each position, we count ways that end with one valid digit or two valid digits. This avoids listing every possible decoding.

Useful Questions to Ask the Interviewer
  1. Can I assume the input contains only digit characters?
  2. Should a null or empty input return 0?
  3. Is returning only the number of decodings enough, without listing the decoded strings?
How would you implement Decode Ways? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a digit string s. The output is the number of valid ways to decode the complete string using 1 through 26 as letters A through Z. A zero cannot be decoded alone. It is valid only when it is part of 10 or 20.

For the diagram example, s = "11106". The answer is 2. The two valid decodings are 1|1|10|6, which gives AAJF, and 11|10|6, which gives KJF. The pair 06 is invalid.

2. Choose dynamic programming

I use a one-dimensional dynamic programming array named dp. Dynamic programming means I save answers for smaller prefixes and reuse them when solving a larger prefix.

The state is: dp[i] is the number of valid decodings for the first i characters, or the prefix s[0..i-1].

The central invariant is that after processing position i, dp[i] contains the correct number of valid decodings for the first i characters.

A valid decoding of a prefix can end in one of two ways. It can end with one valid digit from 1 to 9, or with one valid two-digit number from 10 to 26. These are exactly the two contributions used by the recurrence.

3. Initialize the state

If s is null, empty, or starts with '0', I return 0. A string that starts with zero has no valid decoding.

For a valid non-empty string, I create dp with n + 1 entries. I set dp[0] = 1. This represents one valid way to decode an empty prefix and gives the recurrence a correct starting value. I set dp[1] = 1 because the first character is already known to be nonzero.

For s = "11106", n = 5, so the initial array is [1, 1, 0, 0, 0, 0]. Processing begins at i = 2.

4. Walk through the example

At i = 2, the current character is '1'. The one-digit value 1 is valid, so I add dp[1] = 1. The two-digit value 11 is also valid, so I add dp[0] = 1. Therefore dp[2] = 2. The array becomes [1, 1, 2, 0, 0, 0].

At i = 3, the current character is '1'. The one-digit value 1 is valid, so I add dp[2] = 2. The two-digit value 11 is valid, so I add dp[1] = 1. Therefore dp[3] = 3. The array becomes [1, 1, 2, 3, 0, 0].

At i = 4, the current character is '0'. Zero cannot stand alone, so the one-digit path contributes nothing. The two-digit value is 10, which is valid, so I add dp[2] = 2. Therefore dp[4] = 2. The array becomes [1, 1, 2, 3, 2, 0].

At i = 5, the current character is '6'. The one-digit value 6 is valid, so I add dp[4] = 2. The final two characters are "06". The code computes their numeric value as 6, which is outside the valid two-digit range 10 through 26, so that path contributes nothing. Therefore dp[5] = 2. The final array is [1, 1, 2, 3, 2, 2].

The result is read from dp[5], so the method returns 2.

5. Explain why the result is correct

Every valid decoding of the first i characters must end with either one valid digit or one valid two-digit code. If the last digit is from 1 to 9, every valid decoding counted by dp[i - 1] can be extended with that digit. If the last two digits form a value from 10 to 26, every decoding counted by dp[i - 2] can be extended with that pair. Invalid endings add zero. These cases cover all valid endings without counting an invalid one.

6. Explain the Java implementation

The Java code first rejects null, empty, and leading-zero inputs. It creates an int array of size n + 1 and sets the two base cases. It then processes i from 2 through n. Character arithmetic converts the current character to oneDigit and combines the previous and current characters into twoDigit without creating substring objects. The code adds the correct previous dp value only when that ending is valid. Finally, it returns dp[n].

7. Explain complexity and edge cases

The loop processes each position once, so the time complexity is O(n). The dp array has n + 1 entries, so the auxiliary space is O(n).

Important cases are a string starting with '0', which returns 0, valid zero pairs such as 10 and 20, invalid pairs such as 06, and prefixes such as 30 where zero cannot be used alone. A null or empty string also returns 0 defensively.

Key Insight / Why This Solution Works

The key insight is that a valid decoding of a prefix can end in only two possible ways: one valid digit from 1 to 9, or one valid two-digit value from 10 to 26. I define dp[i] as the number of valid decodings for the first i characters. If the one-digit ending is valid, dp[i - 1] contributes to dp[i]. If the two-digit ending is valid, dp[i - 2] contributes. Invalid endings contribute nothing. The invariant is that each computed dp[i] is the correct count for that prefix. Reusing these prefix counts avoids enumerating every complete decoding.

Code
public class Main {

    public static int numDecodings(String s) {
        // A null, empty, or leading-zero string has no valid decoding.
        if (s == null || s.isEmpty() || s.charAt(0) == '0') {
            return 0;
        }

        int n = s.length();

        // dp[i] stores the number of valid decodings for the first i characters.
        int[] dp = new int[n + 1];

        // The empty prefix contributes one way when a valid two-digit code starts the string.
        dp[0] = 1;

        // The first character is nonzero because the leading-zero case was rejected.
        dp[1] = 1;

        // Build the answer from left to right using already solved prefixes.
        for (int i = 2; i <= n; i++) {
            // Convert the current character into its one-digit numeric value.
            int oneDigit = s.charAt(i - 1) - '0';

            // Combine the previous and current characters into a two-digit numeric value.
            int twoDigit = (s.charAt(i - 2) - '0') * 10 + oneDigit;

            // A value from 1 through 9 can stand alone, so extend every
            // decoding of the prefix that ends one character earlier.
            if (oneDigit >= 1 && oneDigit <= 9) {
                dp[i] += dp[i - 1];
            }

            // A value from 10 through 26 is a valid two-digit letter code,
            // so extend every decoding of the prefix two characters earlier.
            if (twoDigit >= 10 && twoDigit <= 26) {
                dp[i] += dp[i - 2];
            }

            // If neither ending is valid, this prefix receives no contribution
            // and dp[i] remains 0.
        }

        // The final state stores the number of decodings for the whole string.
        return dp[n];
    }

    public static void main(String[] args) {
        // Run the exact example shown in the approved diagram.
        String s = "11106";
        int result = numDecodings(s);

        // The approved example has exactly two valid decodings.
        System.out.println(result);
    }
}
Time & Space Complexity

Let n be the number of characters in the string. We move from left to right and process each position once, so the time complexity is O(n). We create a dynamic programming array with n + 1 integer entries, so the auxiliary space is O(n). Auxiliary space means extra memory used by the algorithm. The code does not create substrings for each check. It uses character arithmetic to calculate the one-digit and two-digit values.

Where it is used

This dynamic programming pattern is useful when the answer for a larger prefix depends on answers for a small number of earlier prefixes. Similar ideas appear in parsing encoded data, counting valid message interpretations, counting ways to build sequences, and other problems where each position has a few legal ways to extend an earlier valid state.

Why Interviewers Ask This

This problem tests whether you can recognize a counting dynamic programming pattern and define a useful state. It also tests whether you can derive the recurrence from valid one-digit and two-digit endings instead of guessing it. Zero handling reveals whether you understand the input rules carefully. The interviewer can also evaluate your base cases, dependency order, Java string handling, correctness reasoning, and whether your O(n) time and O(n) auxiliary-space analysis matches the implementation.

Common interview mistakes

A common mistake is treating '0' as a valid letter by itself. Another is accepting a two-digit value outside 10 through 26. Candidates also sometimes forget that 10 and 20 are valid even though their final digit is zero. Another mistake is adding dp[i - 1] when the current digit is zero. It is also easy to initialize dp[0] incorrectly. dp[0] must be 1 so a valid two-digit code at the beginning can contribute correctly.

Interview tip

State the dp meaning before writing the recurrence: "dp[i] is the number of ways to decode the first i characters." Then check the one-digit ending and two-digit ending separately. This makes the handling of zero much easier to explain and code correctly.

Interviewer may ask next
Can you reduce the auxiliary space from O(n)?

Yes. Each new state uses only dp[i - 1] and dp[i - 2], so the full array is not required if we only need the final count. We can keep two variables for those previous counts and update them from left to right. The validity checks for oneDigit and twoDigit stay the same, so correctness is preserved. The time remains O(n), while auxiliary space becomes O(1). The tradeoff is that we no longer keep every intermediate dp value for inspection.

How would you handle an invalid prefix such as "30" or an invalid pair such as "06"?

The same recurrence handles both cases. For "30", the final digit 0 is not a valid one-digit code and 30 is outside the valid two-digit range 10 through 26, so that state gets no contribution and remains 0. For a pair such as "06" inside a larger string, the pair itself contributes nothing because its numeric value is not between 10 and 26. A following 6 may still contribute through the one-digit rule if the prefix before that 6 has valid decodings. The time remains O(n) and the auxiliary space remains O(n).

9. How would you plan eviction and cleanup for a production cache?System DesignMediumNetflix

Question Details

Design the eviction and cleanup strategy for a production cache. Cover expiry, memory growth, policy choice, background cleanup, correctness, and how you would validate the design under load.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to keep cache reads fast without letting memory grow forever. The main challenge is balancing expiry, eviction, cleanup, and correct writes. I would explain three flows: the cache hit path, the cache miss and load path, and the write path. Each JVM replica owns its own bounded in-memory cache. Expired entries are never served, background cleanup removes old data, and the Source of Truth stays the official data source. The trade-off is more cache-management logic for better speed and safer memory use.

Detailed Explanation

The goal is to keep useful data in memory so reads stay fast, while making sure old entries disappear and memory does not keep growing. The difficult part is that a production cache must handle several things at once. Entries expire, popular keys receive much more traffic, misses can create sudden load on the backing store, and every JVM replica has its own separate heap. The diagram handles this by separating the fast read path, the miss and load path, the write path, and background cleanup. It also measures and tests the cache under load so we can check that memory and latency stay safe.

Useful Questions to Ask the Interviewer
  1. How long should cached entries normally remain valid?
  2. What memory limit should each JVM replica use?
  3. Are some keys much hotter than others?
  4. How quickly should a write become visible through the cache?
  5. What latency and memory targets should we verify under load?
How would you plan eviction and cleanup for a production cache? diagram
How to Explain It in an Interview
1. Explain the JVM cache boundary

I would start by saying that each Java service replica owns its own in-memory cache. Threads or virtual threads inside one JVM share that heap. Separate JVM replicas do not share cache state.

The Request Handler uses the Cache Facade, which groups cache policies, metrics, and serialization. The cache itself is a concurrent key/value store, so multiple request threads can use it safely. The diagram shows striped locking or lock-free techniques as possible ways to support concurrent access.

Each entry keeps expireAt, weight, and version. expireAt controls expiry. weight helps enforce the memory cap. version helps stop an older value from replacing a newer one.

2. Explain the cache hit path

For the normal read path, the Request Handler asks the Cache Facade for the key. The Cache Facade performs the cache lookup.

If the entry exists and has not expired, the cache returns the value. The Request Handler can then return the response to the client. Lazy expiration checks expireAt during reads or writes, so an expired entry is removed before it can be used.

3. Explain the cache miss and load path

If the key is missing or expired, the request goes through the Single-Flight Loader. Single-flight means only one load for the same key is allowed at a time inside that JVM. Other requests can wait for that result instead of sending duplicate loads.

The loader reads the value from the Source of Truth or Backing Data Store. The returned value is stored in the cache with a new expireAt. The cache only applies the update when its version is newer, which helps prevent a stale load from overwriting fresh data.

4. Explain eviction and background cleanup

The cache has a hard maximum weighted size, so memory growth is bounded. When space is needed, the eviction policy uses Window TinyLFU or an LRU-LFU hybrid to favor entries that are more useful.

Cleanup also runs outside the main request path. The Background Cleanup Scheduler performs periodic expiry sweeps, sample-based cleanup, and more aggressive cleanup when heap pressure rises. After an eviction, the eviction listener releases related resources such as handles or buffers.

5. Explain writes, observability, and load testing

For writes, the application updates the Source of Truth first. After that succeeds, it invalidates or refreshes the cache entry by key. The cache is best-effort, while the Source of Truth remains the official data source.

I would measure hit ratio, eviction rate, expired-entry lag, heap usage, GC pauses, load latency, and errors. Alerts should watch memory growth, cleanup lag, eviction spikes, hit-rate drops, high latency, and OOM risk. Under load, I would test hot-key traffic, burst load, memory pressure, cache stampedes, and TTL expiry storms. I would expect stable heap use, no OOM, an acceptable hit rate, and p95 latency within the target.

Engineering Considerations / Design Trade-offs

The benefit is that the cache stays fast while memory remains bounded. Hard expiry stops old entries from being served. The weighted memory cap prevents one JVM cache from growing without control. Window TinyLFU or the LRU-LFU hybrid helps keep more useful entries when space is tight. Background cleanup removes expired data without putting all cleanup work on request threads. The downside is more moving parts. We must manage expiry, versions, eviction, cleanup, and resource release carefully. Each JVM replica also owns a separate cache, so replicas can contain different entries. Single-flight reduces duplicate loads for one key, but waiting requests depend on that one in-flight load.

Why Interviewers Ask This

Interviewers ask this to see whether you can treat a cache as a real production system, not just a map in memory. They want to see how you bound memory, handle expiry, prevent repeated backing-store loads, keep writes correct, and move cleanup away from the fast request path. They also want to know whether you can measure the design under pressure and explain its trade-offs clearly.

Interviewer may ask next
What would you change if many requests suddenly miss on the same hot key at the same time?

I would keep the same design and rely on the Single-Flight Loader for that key. Only one request inside that JVM should load the value from the Source of Truth. Other requests for the same key can wait for that result instead of starting identical loads.

This protects the backing store during a cache stampede, which means many requests miss at nearly the same time. When the load finishes, the loader stores the value with its expireAt and version. Waiting requests can then use the same loaded result.

I would validate this with the cache stampede and hot-key tests shown in the diagram. I would watch load latency, errors, heap usage, and hit ratio during the test.

Correctness stays the same because the Source of Truth still provides the data. The version check still prevents an older result from replacing a newer one. The downside is that requests waiting behind the single in-flight load can see higher latency when that load is slow.

What would you change if the JVM starts approaching its memory limit during a traffic spike?

I would keep the hard maximum weighted size and make the existing heap-pressure cleanup path more aggressive. The Background Cleanup Scheduler already has a heap-pressure trigger, so it can remove expired entries faster when heap usage becomes risky.

The eviction policy would continue choosing which live entries to remove when the cache reaches its memory cap. Periodic expiry sweeps and sample-based cleanup would still run. The eviction listener would also release any related resources after entries leave the cache.

I would watch heap usage, GC pauses, eviction rate, cleanup lag, latency, and errors. The load test should include burst traffic and memory pressure so we can confirm that the heap becomes stable instead of moving toward an out-of-memory failure.

Correctness does not change. An evicted entry simply becomes a later cache miss and can be loaded again from the Source of Truth. The downside is a lower hit rate and more backing-store reads while memory pressure remains high.

10. How would you design a crash-resilient file system?System DesignEasyNetflix

Question Details

Design a file system that can recover file contents and metadata correctly after a crash. Cover create, read, write, delete, rename, durability guarantees, crash recovery, observability, and the main bottlenecks in a production deployment.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to keep file contents and metadata correct even if the system crashes during an update. The main challenge is deciding when a write is truly safe to acknowledge. I would explain the design through the write path, the read path, and crash recovery. Writes use a journal, durable data blocks, and atomic metadata updates. Reads verify stored checksums. Background workers create checkpoints and reclaim deleted blocks. The main trade-off is that durable fsync calls add write latency.

Detailed Explanation

The system must keep file contents and metadata correct even if a machine stops during an operation. A crash must not leave half-written metadata, broken directory entries, or blocks that no longer match their metadata. The hard part is deciding when a change is safe enough to report as successful. The diagram handles this with a journal, durable block writes, atomic metadata changes, checksums, checkpoints, and ordered recovery. I would explain the normal write and read paths first, then delete and rename, and finally crash recovery and production limits.

Useful Questions to Ask the Interviewer
  1. Does a successful write need to survive an immediate machine crash?
  2. Do rename and delete need to be atomic from the client's point of view?
  3. Should recovery favor faster startup or less checkpoint work during normal operation?
How would you design a crash-resilient file system? diagram
How to Explain It in an Interview
1. Explain the entry path and durability rule

I would say every request starts at the Client / SDK and enters the API Gateway. The gateway handles TLS, AuthN/AuthZ, validation, and rate limits. It sends the request to the File System Service, which runs as Java 25 JVM replicas.

Inside a replica, the Request handler uses virtual threads for concurrent blocking work. The Namespace + transaction coordinator controls inode and directory changes. The Metadata cache keeps frequently used inode, directory, block-map, and free-space information close to the service.

The key rule is simple. The service acknowledges success only after the required journal, metadata, and data changes are durable.

2. Explain create and write

For CREATE / WRITE, the service allocates the inode and blocks, then writes new data blocks. Using write-new-blocks, or copy-on-write, avoids changing old blocks in place.

Next, the service appends the transaction to the Journal / WAL with checksums. It fsyncs the journal and data so storage confirms the writes are durable. The metadata update is committed atomically. Only then does the client receive success.

3. Explain read, delete, and rename

For READ, the service looks up metadata, fetches blocks from the Data block store, verifies each block checksum, and returns the content.

For DELETE, it journals removal of the directory entry and tombstones the inode. After the metadata transaction commits, the client receives success. Background workers reclaim blocks later when it is safe.

For RENAME, the service journals and atomically updates the source and destination directory entries. The data blocks stay unchanged.

4. Explain checkpoints and crash recovery

Background workers periodically write a Checkpoint / snapshot of consistent metadata. This reduces how much journal history must be replayed after a crash.

On restart, recovery loads the latest checkpoint and replays journal records in sequence order. It validates checksums and sequence numbers, applies committed transactions, and discards partial or corrupt records. It rebuilds free-space state if needed. The service resumes traffic only after the namespace is consistent.

5. Explain observability and bottlenecks

The gateway, File System Service, and durable storage layer send metrics, logs, traces, and alerts to Observability. These signals help find slow fsync calls, storage errors, and recovery problems.

The main limits are journal fsync latency and metadata lock contention on hot directories. Small files can create extra metadata work. Long checkpoint intervals increase recovery time. Copy-on-write and deferred cleanup also use extra space. These are the main costs of stronger crash safety.

Engineering Considerations / Design Trade-offs

The benefit is strong crash safety. A successful write has already reached the durable journal and required storage before the client gets an acknowledgment. Atomic metadata transactions also keep rename and delete from stopping halfway. The downside is extra work on writes. fsync can be slow because the system waits for storage. Hot directories may also create lock contention around metadata. Small files cause more metadata work compared with their data size. Checkpoints make restart faster, but frequent checkpoints add background work. Waiting too long between checkpoints makes recovery slower. Copy-on-write and delayed cleanup also use more storage until old blocks are reclaimed.

Why Interviewers Ask This

The interviewer wants to see whether you can protect data when failures happen at bad times. They are checking whether you understand write ordering, durable commits, atomic metadata changes, checksums, and recovery. They also want to see if you can separate normal request work from background cleanup and checkpointing. A strong answer should explain both correctness and the real cost of getting that correctness.

Interviewer may ask next
How would the design change if recovery after a crash had to finish much faster?

I would keep the same architecture, but I would create checkpoints more often. The Checkpoint / snapshot stores a consistent view of the metadata. On restart, the recovery worker loads that checkpoint and only replays journal records written after it. A newer checkpoint therefore means fewer journal records to process.

I would not remove the Journal / WAL because it is still required for safe commits between checkpoints. I would also keep checksum and sequence-number validation during replay. Those checks prevent a partial or damaged record from being treated as a valid committed change.

The main change affects the background checkpoint worker and the amount of journal replay needed during recovery. Correctness stays the same because the journal still records committed changes after the snapshot.

The downside is more background I/O. Frequent checkpoints can compete with normal reads and writes, so I would choose an interval that balances normal performance against recovery time.

What would you do if one directory becomes extremely busy and causes metadata lock contention?

I would keep the same File System Service and Namespace + transaction coordinator, but I would make the protected metadata update as short as possible. Create, delete, and rename still need safe atomic changes, so I would not weaken those correctness rules.

The Metadata cache can reduce repeated metadata reads, but it cannot replace safe coordination for writes. I would avoid doing unrelated work while the hot directory state is locked. Data-block work and other preparation should stay outside that small critical section when the existing flow allows it.

Observability can show lock wait time, operation latency, and which directories are causing pressure. That helps confirm the bottleneck before changing behavior.

The downside is that one very hot directory can still limit write concurrency. Strong atomic directory updates require coordination, so correctness places a practical limit on how many conflicting changes can happen at once.

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.