Apple Java Developer Interview Questions & Answers

apple icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

11. How would you solve Merge Intervals?CodingMediumApple

Question Details

Merge overlapping intervals and explain your approach, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I would first sort the intervals by their start value. Then I keep a merged list starting with the first interval. For each remaining interval, I compare its start with the end of the last merged interval. If they overlap, I extend the last end to the larger end. Otherwise, I append a new interval. Sorting makes possible overlaps adjacent. The total time is O(n log n), and the merged output uses O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a list of closed intervals. Each interval has a start and an end. The goal is to combine ranges that overlap and return ranges that no longer overlap. I first sort the intervals by their start values. This puts possible overlaps next to each other. I then build a merged result from left to right. I only need to compare the current interval with the last interval already in the result. If they overlap, I extend that result interval. Otherwise, I add a new one.

Useful Questions to Ask the Interviewer
  1. Should touching closed intervals such as [1,4] and [4,5] be merged?
  2. Is it acceptable to sort the input intervals by their start value?
How would you solve Merge Intervals? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of closed intervals. For the example, the input is [[1,3],[2,6],[8,10],[15,18]]. The required output is a new set of non-overlapping intervals that covers exactly the same ranges. The expected result is [[1,6],[8,10],[15,18]]. Because the intervals are closed, touching intervals also overlap under the condition current.start <= lastMerged.end.

2. Sort and initialize the merged state

Sort the intervals by their start value. The example is already in sorted order: [[1,3],[2,6],[8,10],[15,18]]. If the input is empty, return an empty array. Otherwise, put a copy of the first interval, [1,3], into the merged list. Start processing at index 1. The invariant is that the merged list contains the correctly merged, non-overlapping intervals for the part of the input already processed.

3. Walk through the example

At index 1, the current interval is [2,6]. The merged list before the step is [[1,3]]. Check 2 <= 3. This is true, so the intervals overlap. Update the end to max(3,6) = 6. The merged list becomes [[1,6]].

At index 2, the current interval is [8,10]. The state before the step is [[1,6]]. Check 8 <= 6. This is false, so there is no overlap. Append [8,10]. The state becomes [[1,6],[8,10]].

At index 3, the current interval is [15,18]. The state before the step is [[1,6],[8,10]]. Compare it with the last merged interval [8,10]. Check 15 <= 10. This is false. Append [15,18]. The final state is [[1,6],[8,10],[15,18]].

4. Explain why only the last merged interval matters

Sorting places possible overlaps next to each other. After earlier intervals have already been merged, the current interval cannot need to merge with an older result interval without also overlapping the last merged interval. So each current interval only needs one comparison with the last merged interval. We either extend that interval or append a new non-overlapping interval.

5. Explain why the result is correct

The invariant is that the processed prefix is represented by correctly merged, non-overlapping intervals. If current.start <= lastMerged.end, merging preserves the same covered range by extending the end to the maximum endpoint. If current.start > lastMerged.end, the current interval cannot overlap the previous merged ranges, so appending it preserves non-overlap. Therefore, the final result covers exactly the same ranges as the original input.

6. Explain the Java implementation and complexity

The Java code handles an empty input first. It sorts with Integer.compare on the start values. It stores merged intervals in an ArrayList<int[]>. During the loop, it reads the current interval and the last merged interval. On overlap, it updates the last end with Math.max. Otherwise, it adds a new interval. Finally, it converts the list to int[][]. Sorting costs O(n log n), the sweep costs O(n), and the merged output uses O(n) auxiliary space.

Key Insight / Why This Solution Works

The key idea is to sort intervals by their start value before merging. After sorting, intervals that may overlap are next to each other. Keep a list called merged and compare each current interval only with the last interval in that list. If current[0] <= lastMerged[1], the ranges overlap, so set lastMerged[1] to Math.max(lastMerged[1], current[1]). Otherwise, append the current interval as a new merged block. The central invariant is that after each processed interval, merged contains the correctly merged, non-overlapping representation of the processed prefix.

Code
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {

    public static int[][] merge(int[][] intervals) {
        // Handle the empty-input edge case before reading the first interval.
        if (intervals == null || intervals.length == 0) {
            return new int[0][];
        }

        // Sort by start value so intervals that can overlap are adjacent.
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        // This list stores the correctly merged processed prefix.
        List<int[]> merged = new ArrayList<>();

        // Seed the result with a copy of the first sorted interval.
        merged.add(new int[] { intervals[0][0], intervals[0][1] });

        // Process every remaining interval from left to right.
        for (int i = 1; i < intervals.length; i++) {
            int[] current = intervals[i];
            int[] lastMerged = merged.get(merged.size() - 1);

            // Closed intervals overlap when the current start is at or before
            // the end of the last merged interval.
            if (current[0] <= lastMerged[1]) {
                // Keep the earlier start and extend the right boundary when needed.
                lastMerged[1] = Math.max(lastMerged[1], current[1]);
            } else {
                // No overlap exists, so start a new merged interval.
                merged.add(new int[] { current[0], current[1] });
            }
        }

        // Convert the merged list to the required int[][] result.
        return merged.toArray(new int[merged.size()][]);
    }

    public static void main(String[] args) {
        // Use the exact example shown in the approved diagram.
        int[][] intervals = { { 1, 3 }, { 2, 6 }, { 8, 10 }, { 15, 18 } };

        // Run the merge algorithm on the diagram's example.
        int[][] result = merge(intervals);

        // Expected output: [[1, 6], [8, 10], [15, 18]]
        System.out.println(Arrays.deepToString(result));
    }
}
Time & Space Complexity

Let n be the number of intervals. Sorting the intervals by start value takes O(n log n) time. After sorting, the algorithm makes one left-to-right sweep, which takes O(n) time. Sorting therefore dominates, so the total time is O(n log n). The merged result can contain up to n intervals, so the illustrated solution uses O(n) auxiliary space for the merged output.

Where it is used

This pattern is useful whenever overlapping time or numeric ranges must be combined. Examples include combining booking windows, calendar ranges, reserved time periods, covered numeric ranges, or other intervals before later processing.

Why Interviewers Ask This

This problem tests whether you recognize the sort-and-sweep interval pattern. The interviewer can see whether you choose the correct sorting key, maintain a useful invariant, apply the overlap condition correctly, and update interval boundaries without losing coverage. It also checks whether you handle cases such as touching or contained intervals, write clear Java collection code, and include the sorting cost when explaining time complexity.

Common interview mistakes

A common mistake is trying to merge before sorting by start value. Then possible overlaps may not be adjacent. Another mistake is using current[0] < lastMerged[1] instead of current[0] <= lastMerged[1]. For closed intervals, [1,4] and [4,5] touch and must merge. Candidates may also compare the current interval with every earlier interval instead of only the last merged interval. Another mistake is mishandling fully contained intervals such as [1,10] and [2,3]. Finally, the complexity explanation must include the O(n log n) sorting cost.

Interview tip

State the invariant before coding: after processing each interval, the merged list is already correct and non-overlapping for the processed prefix. Then the merge-or-append decision is easy to explain.

Interviewer may ask next
What changes if the intervals are already sorted by start value?

The merge logic stays the same, but we can skip Arrays.sort. We still seed the merged list with the first interval and compare each remaining interval with the last merged interval. The invariant and overlap rule current.start <= lastMerged.end stay the same. The time complexity becomes O(n) because only the sweep remains. The merged output still uses O(n) space in the worst case. The tradeoff is that this faster bound depends on the input already being correctly sorted.

Can the algorithm reduce extra memory by merging intervals in place?

Yes. After sorting, we can keep a write position in the same interval array. Each new interval is either merged into the interval at the write position or placed at the next write position. The same sorted-order invariant and overlap rule preserve correctness. Sorting still makes the time O(n log n), followed by an O(n) sweep. The merge work itself can use O(1) extra state, although returning an exact-sized int[][] may still require a result array. The tradeoff is that the input array is modified.

12. How would you solve Word Ladder?CodingHardApple

Question Details

Find the shortest transformation sequence between two words and explain your approach, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I would use breadth-first search because every valid one-letter change has the same cost. I put the allowed words in a HashSet and process the queue one level at a time. For each word, I try every lowercase letter at every position. When a generated word is in the set, I remove it before enqueueing it, which marks it visited. The first time I dequeue endWord, that level is the shortest length. The expected time is O(NL²), with O(NL) worst-case auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given a starting word, an ending word, and a list of allowed words. We need the length of the shortest valid chain from the start to the end. Each step changes exactly one letter, and every new word must be allowed. In the diagram, the start is "hit" and the end is "cog". One shortest chain is hit → hot → dot → dog → cog, so the returned length is 5. Breadth-first search fits because it explores shorter transformation chains before longer ones.

Useful Questions to Ask the Interviewer
  1. Should I return the number of words in the shortest transformation sequence rather than the actual path?
  2. Can I assume the words contain lowercase English letters, so each position has 26 possible letters to try?
  3. Should I return 0 when endWord cannot be reached?
How would you solve Word Ladder? diagram
How to Explain It in an Interview
1. Understand the input and required output

The inputs are beginWord, endWord, and wordList. The output is the number of words in the shortest valid transformation sequence. Each move changes exactly one character. Every newly reached word must exist in wordList. For the shown example, beginWord is "hit", endWord is "cog", and wordList is ["hot", "dot", "dog", "lot", "log", "cog"]. The answer is 5.

2. Choose BFS and a HashSet

I use breadth-first search, or BFS. BFS processes states in increasing distance from the start. Here, distance means the number of transformations used. That is why the first time endWord is dequeued, the current level gives the shortest sequence length.

I copy wordList into a HashSet named dict. The set tells me whether a generated word is valid and still unseen. When I discover a valid word, I remove it from dict before enqueueing it. This marks the word visited at its earliest BFS level and prevents duplicate queue entries.

The central invariant is that the queue is processed one complete level at a time. Every word reached at an earlier level requires no more transformations than any word first reached at a later level.

3. Initialize the state

If beginWord already equals endWord, the shown implementation returns 1. Otherwise, I build dict from wordList. If endWord is not present, I return 0.

I create an ArrayDeque, enqueue beginWord, and set level to 1 because the sequence already contains the starting word. In the example, the initial queue is [hit], level is 1, and dict contains hot, dot, dog, lot, log, and cog.

4. Walk through the example

At level 1, I dequeue "hit". I try changing every position to each lowercase letter. The only valid unseen neighbor is "hot". I remove "hot" from dict and enqueue it. The queue becomes [hot].

At level 2, I dequeue "hot". The valid unseen neighbors are "dot" and "lot". I remove both from dict and enqueue them. The queue becomes [dot, lot].

At level 3, I process "dot" and then "lot". From "dot" I discover "dog". From "lot" I discover "log". Both are removed from dict before they are enqueued. The queue becomes [dog, log].

At level 4, I process "dog" and then "log". From "dog" I discover "cog", remove it from dict, and enqueue it. When "log" later generates "cog", that word has already been removed, so it is not enqueued again. The queue becomes [cog].

At level 5, I dequeue "cog". It equals endWord, so I return 5 immediately. One valid shortest sequence is hit → hot → dot → dog → cog. A sequence through lot → log also has length 5.

5. Explain why the result is correct

Every valid one-letter transformation has equal cost. BFS therefore explores all states at a smaller transformation distance before states at a larger distance. Removing a word from dict when it is first discovered prevents it from being rediscovered at the same or a later level. Therefore, when endWord is first dequeued, no shorter valid sequence can remain unexplored.

6. Explain the Java implementation

Before processing a BFS level, the code saves queue.size(). Only those words belong to the current level. New words added during that loop belong to the next level.

For each dequeued word, the code first checks whether it equals endWord. Otherwise, it converts the word to a char array. For each character position, it saves the original character, tries letters 'a' through 'z', skips the unchanged character, creates a candidate String, and calls dict.remove(next). If remove returns true, the candidate was valid and unseen, so it is enqueued. The original character is restored before moving to the next position. After the complete level is processed, level increases by one.

7. Explain complexity and edge cases

Let N be the number of words and L be the word length. For each reachable word, the code tries 26 letters at each of L positions. Creating and hashing each generated length-L String can cost O(L). The expected time is therefore O(26 × N × L²), commonly written as O(NL²). HashSet lookup and removal are O(1) on average after hashing.

The queue may hold O(N) generated Strings of length L, so worst-case auxiliary space is O(NL), plus O(N) HashSet references. Relevant edge cases are endWord missing from wordList, beginWord already equal to endWord, duplicate word-list entries collapsing in the HashSet, and no valid path, which returns 0.

Key Insight / Why This Solution Works

Treat each valid word as a state in an implicit unweighted graph. Two states are connected when one letter can be changed to produce the other word. The algorithm does not build every edge in advance. Instead, when a word is dequeued, it generates candidate neighbors by changing one character at a time and checking the HashSet. BFS is appropriate because every transformation has equal cost. The key invariant is that states are processed level by level in nondecreasing transformation distance. Removing a valid word from dict when it is first discovered marks it visited at its earliest level. Therefore, the first dequeue of endWord gives the shortest transformation length.

Code
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;

public class Main {

    public static int ladderLength(String beginWord, String endWord, List<String> wordList) {
        // The sequence already contains the target when start and end are equal.
        if (beginWord.equals(endWord)) {
            return 1;
        }

        // dict contains valid words that have not yet been discovered by BFS.
        Set<String> dict = new HashSet<>(wordList);

        // The illustrated contract requires the final word to be in wordList.
        if (!dict.contains(endWord)) {
            return 0;
        }

        // Start BFS with the beginning word at sequence length 1.
        Queue<String> queue = new ArrayDeque<>();
        queue.offer(beginWord);
        int level = 1;

        while (!queue.isEmpty()) {
            // Freeze the current queue size so newly discovered words stay
            // in the next BFS level instead of being processed immediately.
            int size = queue.size();

            for (int s = 0; s < size; s++) {
                String word = queue.poll();

                // BFS processes levels in increasing distance order, so the
                // first dequeue of endWord gives the shortest sequence length.
                if (word.equals(endWord)) {
                    return level;
                }

                // A char array lets us change one position at a time.
                char[] chars = word.toCharArray();

                for (int i = 0; i < chars.length; i++) {
                    // Save the original character so this position can be restored.
                    char original = chars[i];

                    for (char c = 'a'; c <= 'z'; c++) {
                        // A valid transformation must actually change one letter.
                        if (c == original) {
                            continue;
                        }

                        chars[i] = c;
                        String next = new String(chars);

                        // remove() succeeds only for a valid unseen word.
                        // Removing before enqueueing marks the word visited now.
                        if (dict.remove(next)) {
                            queue.offer(next);
                        }
                    }

                    // Restore this position before trying changes at the next index.
                    chars[i] = original;
                }
            }

            // All words at this transformation distance are finished.
            level++;
        }

        // The search ended without reaching endWord.
        return 0;
    }

    public static void main(String[] args) {
        // Run the exact example shown in the approved diagram.
        String beginWord = "hit";
        String endWord = "cog";
        List<String> wordList = List.of("hot", "dot", "dog", "lot", "log", "cog");

        // Expected output: 5
        System.out.println(ladderLength(beginWord, endWord, wordList));
    }
}
Time & Space Complexity

Let N be the number of words and L be the length of each word. For each reachable word, the code examines L positions and tries 26 lowercase letters at every position. Creating a candidate String and hashing that length-L String can each take O(L), so the illustrated Java implementation takes O(26 × N × L²) expected time. Because 26 is constant, this is commonly written as O(NL²). HashSet lookup and removal are O(1) on average after hashing. The queue can hold O(N) generated Strings of length L, giving O(NL) worst-case auxiliary space, plus O(N) HashSet references.

Where it is used

This BFS pattern is useful when software must find a shortest sequence of equal-cost state changes without building the whole graph first. Examples include word transformations, puzzle-state searches, configuration transitions, and shortest paths in other unweighted state spaces where valid neighbors can be generated when needed.

Why Interviewers Ask This

This problem tests whether you recognize a shortest-path problem in an implicit unweighted graph and choose BFS. It also checks whether you can generate neighbors without building the full graph, use a HashSet for membership and visited state, preserve BFS level boundaries, and avoid duplicate work. The interviewer can also evaluate your Java string-mutation logic, edge-case reasoning, early-return reasoning, and whether your complexity analysis includes String creation and hashing costs.

Common interview mistakes

One common mistake is marking a word visited only when it is dequeued. That can enqueue the same word more than once. Remove it from the HashSet when it is first discovered. Another mistake is processing newly enqueued words in the same BFS level instead of saving the current queue size first. Candidates may also forget to restore the original character after trying replacements at one position. Another mistake is claiming O(NL) time while ignoring the O(L) cost of creating and hashing each generated String. Finally, do not continue processing after endWord is dequeued and the shortest level is known.

Interview tip

Before coding, state the two rules that make the solution correct: process exactly one queue level at a time, and remove a valid word from the HashSet when it is first discovered. Then connect those rules directly to why the first dequeue of endWord gives the shortest length.

Interviewer may ask next
How would you return one actual shortest transformation sequence instead of only its length?

Keep the same BFS, but record a parent when each word is first discovered. For example, store child → parent immediately before enqueueing the child. When endWord is reached, follow the parent links from endWord back to beginWord and reverse that list. BFS correctness is unchanged because each parent is recorded at the child's earliest level. The expected search time remains O(NL²) for this implementation. The parent map adds O(N) references, while queued and stored word data keeps worst-case auxiliary space at O(NL).

How could you reduce the amount of search work for a very large word list?

A common extension is bidirectional BFS. Start one frontier at beginWord and another at endWord, and expand the smaller frontier each round until the searches meet. Correctness is preserved because both sides still advance through equal-cost BFS levels. The worst-case asymptotic work can still be large, but the number of explored states is often much smaller in practice. The main tradeoff is extra bookkeeping because two frontiers and visited structures must be maintained.

13. How would you solve Merge K Sorted Lists?CodingHardApple

Question Details

Merge k sorted linked lists and explain your approach, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I would use a min heap with at most one current node from each list. First, I put every non-null list head into the heap. Then I repeatedly remove the smallest node, append that same node to the merged list, and add its next node to the heap if it exists. This keeps the result sorted because the heap always exposes a smallest remaining candidate. With N total nodes and k lists, the time is O(N log k) and the auxiliary space is O(k).

Detailed Explanation

See the Code while reading this explanation.

We are given k lists of connected nodes. Each list is already ordered from smaller values to larger values. We need to connect all of those existing nodes into one ordered list. At each step, we only need to decide which list has the smallest next value. I keep one current node from each non-empty list in a structure that can quickly give me the smallest one. After I take that node, I make its next node available. This repeats until no nodes remain.

Useful Questions to Ask the Interviewer
  1. Can some entries in the input array be null?
  2. Should I reuse the existing ListNode objects instead of creating new nodes for every value?
  3. Can duplicate values appear in different lists?
How would you solve Merge K Sorted Lists? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of k sorted linked lists. The output is one linked list containing all nodes in ascending order. For the diagram example, the lists are L1 = 1 → 4 → 5, L2 = 1 → 3 → 4, and L3 = 2 → 6. There are 8 nodes in total. The final result is 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6. We reuse the existing nodes instead of copying their values into new result nodes.

2. Choose the algorithm and data structure

I use a Java PriorityQueue as a min heap. Each heap item is a ListNode reference. The comparator orders nodes by node.val. The heap keeps at most one current candidate from each active list. The important invariant is that before each removal, the heap contains the smallest unmerged node from every non-empty list. Because of that, a minimum heap item is a correct next node for the merged output.

A simple alternative would be to scan all k current list heads every time we need the next node. That can require O(k) work for each of N nodes. The min heap reduces each selection to O(log k).

3. Initialize the state

First, insert the head of every non-null list into the min heap. For the example, the initial heap candidates are (1,L1), (1,L2), and (2,L3). Then create a dummy node. The dummy node gives us a fixed starting point for the result. A tail pointer starts at dummy and always points to the last node in the merged list. The initial merged state is dummy → empty.

4. Walk through the example

Step 1: the heap contains (1,L1), (1,L2), and (2,L3). In the diagram's shown trace, remove (1,L1). Append it after tail. The result is 1. Its successor is 4, so add (4,L1). The candidates are now (1,L2), (2,L3), and (4,L1).

Step 2: remove (1,L2). Append it. The result becomes 1 → 1. Add its successor (3,L2). The candidates become (2,L3), (3,L2), and (4,L1).

Step 3: remove (2,L3). The result becomes 1 → 1 → 2. Add its successor (6,L3). The candidates become (3,L2), (4,L1), and (6,L3).

Step 4: remove (3,L2). The result becomes 1 → 1 → 2 → 3. Add its successor (4,L2). The candidates become (4,L1), (4,L2), and (6,L3).

Step 5: in the diagram's shown trace, remove (4,L1). The result becomes 1 → 1 → 2 → 3 → 4. Add its successor (5,L1). The candidates become (4,L2), (5,L1), and (6,L3).

Step 6: remove (4,L2). The result becomes 1 → 1 → 2 → 3 → 4 → 4. This node has no successor, so nothing is added. The candidates are (5,L1) and (6,L3).

Step 7: remove (5,L1). The result becomes 1 → 1 → 2 → 3 → 4 → 4 → 5. It has no successor, so the heap now contains only (6,L3).

Step 8: remove (6,L3). The result becomes 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6. It has no successor. The heap becomes empty, so processing stops and we return dummy.next.

The diagram shows one valid order for nodes with equal values. Java PriorityQueue does not guarantee which equal-valued node is removed first when the comparator returns 0. Either tie order is valid here because the required merged values remain 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6.

5. Explain why the result is correct

Before every heap removal, each active input list contributes its smallest unmerged node. No node farther inside one of those sorted lists can be smaller than that list's current candidate. Therefore, a minimum heap candidate is globally smallest among the remaining nodes. Appending it keeps the merged prefix sorted. After removing a node, only its successor can become the next candidate from that same list. Adding that successor restores the invariant for the next iteration.

6. Explain the Java implementation

Java PriorityQueue is used as the min heap. A comparator orders ListNode references by node.val. We seed the heap with every non-null list head. The dummy node and tail pointer build the result without special handling for the first real node. Inside the loop, poll removes a smallest current node. We link that same node after tail and move tail forward. If that node has a successor, we offer the successor into the heap. When the heap becomes empty, dummy.next is the first real node of the completed merged list.

7. Explain complexity and edge cases

Let N be the total number of nodes across all lists and k be the number of lists. Every node enters the heap once and leaves the heap once. The heap contains at most k nodes, so each push or pop costs O(log k). The total time is O(N log k). The heap uses O(k) auxiliary space. The other working pointers use O(1) extra space.

If lists is null or has length 0, return null. Null lists are skipped during heap initialization. If all lists are empty, the heap starts empty and dummy.next is null. Duplicate values are valid and remain in sorted order. If k is 1, the same algorithm returns that single sorted list.

Key Insight / Why This Solution Works

The key idea is to compare only the current smallest unmerged node from each list. Because every input list is already sorted, we do not need to look farther inside a list until its current node has been removed. A min heap stores these current candidates and exposes a smallest one efficiently. The central invariant is that the heap contains the smallest unmerged node from every active list. After a node is appended, only that node's successor needs to enter the heap. This keeps the heap size at most k and produces the merged values in ascending order.

Code
import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {

    // A node in a singly linked list.
    static class ListNode {

        int val;
        ListNode next;

        ListNode(int val) {
            this.val = val;
        }
    }

    public ListNode mergeKLists(ListNode[] lists) {
        // Handle a missing or empty array of lists.
        if (lists == null || lists.length == 0) {
            return null;
        }

        // Store current ListNode references in a min heap ordered by node value.
        PriorityQueue<ListNode> minHeap = new PriorityQueue<>(
            Comparator.comparingInt(node -> node.val)
        );

        // Seed the heap with the first available node from every non-null list.
        for (ListNode head : lists) {
            if (head != null) {
                minHeap.offer(head);
            }
        }

        // Dummy gives the merged list a fixed starting point.
        // tail always points to the last node currently in the merged list.
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

        // Continue until there are no remaining candidate nodes.
        while (!minHeap.isEmpty()) {
            // Remove one of the smallest current candidates.
            ListNode smallest = minHeap.poll();

            // Reuse that exact node in the merged list and advance tail.
            tail.next = smallest;
            tail = tail.next;

            // Only this node's successor can become the next candidate from its list.
            if (smallest.next != null) {
                minHeap.offer(smallest.next);
            }
        }

        // Skip the dummy node and return the first real node of the merged list.
        return dummy.next;
    }

    public static void main(String[] args) {
        Main solution = new Main();

        // Build L1: 1 -> 4 -> 5.
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(4);
        l1.next.next = new ListNode(5);

        // Build L2: 1 -> 3 -> 4.
        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(3);
        l2.next.next = new ListNode(4);

        // Build L3: 2 -> 6.
        ListNode l3 = new ListNode(2);
        l3.next = new ListNode(6);

        // Run the exact input example shown in the diagram.
        ListNode merged = solution.mergeKLists(new ListNode[] { l1, l2, l3 });

        // Print the merged values in linked-list order.
        printList(merged);
    }

    static void printList(ListNode head) {
        ListNode current = head;

        // Visit each result node once and print arrows between nodes.
        while (current != null) {
            System.out.print(current.val);
            if (current.next != null) {
                System.out.print(" -> ");
            }
            current = current.next;
        }
        System.out.println();
    }
}
Time & Space Complexity

Let N be the total number of nodes in all lists, and let k be the number of lists. Every node is added to the min heap once and removed once. The heap contains at most k nodes, so each add or remove operation costs O(log k). The total time is O(N log k). The auxiliary space is O(k) because the heap stores at most one current node from each list. The dummy node and pointer variables use only O(1) additional working space.

Where it is used

This pattern is useful when several already-sorted sequences must be combined while repeatedly choosing the smallest current item. Examples include merging sorted data streams, combining sorted result batches, and merging several sorted runs during external sorting.

Why Interviewers Ask This

This problem tests whether you can use the fact that each input list is already sorted instead of treating all nodes as unrelated. The interviewer is looking for recognition of the min-heap pattern, correct handling of linked-list references, maintenance of a clear invariant, and accurate reasoning about O(N log k) time and O(k) auxiliary space. It also checks whether you handle null lists, duplicate values, pointer updates, and Java PriorityQueue behavior correctly.

Common interview mistakes

A common mistake is putting every node from every list into the heap. That works, but it grows the heap to O(N) instead of keeping at most O(k) candidates. Another mistake is forgetting to insert the popped node's successor, which causes the remaining nodes from that input list to be skipped. Candidates can also update linked-list pointers in an order that loses access to the remaining nodes. Another common mistake is claiming the heap uses O(N) auxiliary space or giving O(N log N) as the complexity of this exact implementation. Finally, do not assume Java PriorityQueue has a stable order for equal-valued nodes.

Interview tip

State the heap invariant before coding: the heap holds the smallest unmerged node from each active list. Then make every loop iteration restore that invariant by polling one node, appending it, and pushing only its successor.

Interviewer may ask next
Can you reduce the auxiliary space used by the heap?

Yes. Instead of the heap, the k lists can be merged in pairs in rounds. Each node is still processed across O(log k) merge levels, so the total time is O(N log k). If each two-list merge is iterative and rewires the existing nodes, the merge work itself needs O(1) auxiliary pointer space. The tradeoff is that this changes the control flow from the direct heap-based solution shown in the diagram.

How would this work if the sorted lists arrived as streams?

The same min-heap idea can be used when each active stream can provide its next item on demand. Put one current item from each stream into the heap. Remove a smallest item, output it, then request and insert the next item only from that same stream. For N produced items and k active streams, the heap work is O(N log k) and the heap uses O(k) space. The main tradeoff is that processing may need to wait for a stream to provide its next item.

14. How would you design a replication system for a database?System DesignHardApple

Question Details

Explain replication topology, failover, consistency, and recovery tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to keep database data available even when one node fails. The main challenge is balancing safe writes, fast reads, and quick recovery. I would explain the design through the write path, the read path, and failover and recovery. One Primary DB Node accepts writes, a Synchronous Standby protects recent data, and async replicas support read scaling and disaster recovery. The trade-off is that waiting for the standby improves durability but adds write latency.

Detailed Explanation

The goal is to keep database data safe and available when machines fail. The hard part is keeping writes correct while still scaling reads. We also need a safe way to replace a failed writer and rebuild damaged nodes. The diagram solves this with one main writer, a closely synchronized standby, separate read and disaster-recovery replicas, and a manager that controls failover. I would explain the design through normal writes, normal reads, failover, and recovery.

Useful Questions to Ask the Interviewer
  1. How much data loss can we accept during a failure?
  2. How important is write latency compared with durability?
  3. Can some reads return slightly old data?
  4. How quickly should the system recover from a Primary failure?
How would you design a replication system for a database? diagram
How to Explain It in an Interview
1. Explain the replication topology

I would use a single-writer design because it keeps write ownership simple. The Primary DB Node is the only node that accepts writes during normal operation. It also serves strong reads when callers need the latest committed data.

The Primary sends its WAL stream to the Synchronous Standby. WAL is the ordered record of database changes. The standby sends an ACK before the Primary completes the commit, so it is the preferred failover target.

The Synchronous Standby forwards changes asynchronously to the Async Read Replica. The Primary separately sends cross-region asynchronous replication to the Async DR Replica. These replicas may be slightly behind because they do not block the main commit.

2. Explain the write path

For a write, Clients / Java 25 Services send the request to the Connection Proxy / Read-Write Router. These Java services are stateless JVM replicas, so separate service instances do not share session state.

The router sends the write to the Primary DB Node. The Primary writes WAL and sends the WAL stream to the Synchronous Standby. The standby returns an ACK, the commit completes, and the commit result returns to the client.

The benefit is better protection for recent committed data. The downside is extra write latency because the Primary waits for the standby.

3. Explain the read path

For reads that need the newest committed value, the router sends strong reads to the Primary DB Node. This avoids reading from a replica that may still be behind.

For stale-tolerant reads, the router can use the Async Read Replica. This improves read scaling because the Primary handles less read traffic. The trade-off is replication lag, which means the replica may show older data for a short time.

The Async DR Replica is mainly for disaster recovery. It may also lag because its replication is asynchronous and cross-region.

4. Explain failover

The Cluster Manager / Leader Election Quorum receives heartbeats from the database nodes. A heartbeat is a small health signal that shows a node is alive.

If the Primary fails, the manager detects missed heartbeats. It first fences the old Primary so that it cannot keep accepting writes. This prevents split-brain, where two nodes both act as the writer.

The manager then promotes the Synchronous Standby to the new Primary. Finally, it updates the writer endpoint used by the router and clients. Writes may pause briefly during this switch.

5. Explain recovery and trade-offs

The Backup Store keeps snapshots and WAL history. A failed or new node restores a snapshot, replays newer WAL, catches up to the current position, and rejoins the cluster as a replica.

A very stale node may need a full resync. The design gives simple write ownership, safer failover, and scalable reads. The main trade-off is that synchronous protection increases commit latency, while asynchronous replicas improve scale but may return older data.

Engineering Considerations / Design Trade-offs

The benefit is that one Primary DB Node keeps writes simple and avoids write conflicts. The Synchronous Standby lowers the risk of losing recent committed data. The downside is that the Primary waits for its ACK, so writes take a little longer. The Async Read Replica makes reads easier to scale, but it may return old data for a short time. Automatic failover improves availability, but the old Primary must be fenced first. Snapshots and WAL replay make recovery faster, but a very stale node may still need a full resync.

Why Interviewers Ask This

Interviewers ask this to see whether you can balance correctness, availability, and speed. They want to know if you understand why one node owns writes, why replicas can lag, and how failover avoids two writers. They also want to see whether you can explain recovery clearly and make sensible trade-offs instead of claiming perfect availability, instant failover, or zero data loss.

Interviewer may ask next
What would you change if the business could not accept the extra write latency from waiting for the Synchronous Standby?

I would keep the same basic design, but I would change when the Primary DB Node considers a write committed. Instead of waiting for the Synchronous Standby ACK, the Primary could commit locally and replicate the WAL afterward.

This would reduce write latency because the client would not wait for another database node. The Clients / Java 25 Services, Connection Proxy / Read-Write Router, and replica layout could stay the same.

The important change is durability. If the Primary fails before its newest WAL reaches another node, some recently acknowledged writes could be lost. The Cluster Manager / Leader Election Quorum could still promote another node, but that node might not contain those latest changes.

I would only choose this behavior if the business accepts that risk. The main downside is a larger possible data-loss window during a sudden Primary failure.

What would you do if users complain that the Async Read Replica sometimes returns old data right after they update something?

I would keep the same replication system, but I would change how those sensitive reads are routed. Right after a write, the Connection Proxy / Read-Write Router should send that user's critical reads to the Primary DB Node instead of the Async Read Replica.

The Primary has the latest committed state, so this avoids showing an older value caused by replication lag. Other stale-tolerant reads can still go to the Async Read Replica and keep the read-scaling benefit.

The diagram also allows an operational choice to use lag-aware replicas. If a replica is known to be too far behind, the router should use the Primary for that critical read.

This keeps important reads correct without removing the replica. The downside is that more read traffic returns to the Primary, so some read-scaling benefit is lost.

15. How would you design a data center?System DesignMediumApple

Question Details

Explain the main components, request flow, availability, scaling, and operational tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to keep Java applications running even when network, server, or facility parts fail. The main challenge is balancing availability, performance, security, and cost. I would explain three flows: how requests enter, how stateless JVM replicas use cache and SQL, and how background and operational systems support them. Redundant networking, power, cooling, and monitoring remove many single failure points. The main trade-off is that stronger redundancy and consistency improve resilience but add latency, hardware cost, and power use.

Detailed Explanation

The goal is to run Java services inside a data center and keep them available when individual parts fail. Requests must reach healthy application instances quickly, while the platform also protects traffic, stores data, runs background work, and watches for problems. I would explain the request path, Java compute, shared services, operations, and facility support.

Useful Questions to Ask the Interviewer
  1. How much downtime is acceptable?
  2. Do we need disaster recovery outside this data center?
  3. What read and write load should we expect?
  4. How much recent data could we lose during a major failure?
  5. What security and network-segmentation rules are required?
How would you design a data center? diagram
How to Explain It in an Interview
1. Bring traffic in safely

I would start with the request path. Users reach two Internet providers, then redundant Edge Routers using BGP. Traffic passes through DDoS Protection / Firewall / WAF, then active-active L4/L7 Load Balancers.

The API Gateway / Ingress handles authentication, authorization, request validation, and rate limiting. Public, production, and management networks are separated. TLS protects traffic, with mTLS between services.

2. Run stateless Java services across racks

The ingress sends traffic into the Data Center Core. A redundant spine-leaf network connects multiple Compute Rack / Pool groups.

Each rack runs several stateless Java Service Replicas. Every replica is a separate JVM with a Java 21/25 service process, private heap, worker threads or virtual threads, connection pools, and a Local Health Check. Threads inside one JVM share its heap. Separate JVM replicas do not share heap state.

Health checks keep failed replicas out of the request path. Because the services are stateless, capacity grows by adding JVM replicas, racks, and leaf switches.

3. Use cache first, then SQL

A Java service first performs a Distributed Cache lookup. On a hit, it can use the cached data. On a miss, or for a write, it uses the SQL Database Primary.

Reads may go to Read Replicas. The Primary copies data to them asynchronously, so they can be a little behind. The service returns the response through the ingress and load-balancing path to the client.

4. Move slow work to the background

A service can publish events to the Message Queue / Event Log. Background Consumer JVMs read those events and update the database or Object / Backup Storage.

Database backups and snapshots also go to Object / Backup Storage. Service Discovery / Config / Secrets provides registration, configuration, and secrets. Logs, metrics, and traces go to the Observability Stack. The isolated Management Network uses a Bastion Host for Admin / SRE Access. CI/CD & Provisioning builds, tests, scans, and deploys to compute nodes.

5. Design for facility failures and cost

A+B power uses separate utility feeds, switchgear, UPS systems, PDUs, and generators. N+1 cooling uses chillers, CRAC/CRAH units, and hot-aisle / cold-aisle containment. Physical security includes badge access, perimeter fencing, CCTV, security operations, and environmental monitoring.

The design scales by adding racks, replicas, cache nodes, consumers, and Read Replicas. Stronger synchronous replication can reduce data loss, but adds latency. Asynchronous replication improves availability and disaster recovery, but may lose the newest writes during failover. More spare capacity improves resilience, but increases hardware, operating, and power costs.

Engineering Considerations / Design Trade-offs

The benefit is that the design can keep serving traffic when individual links, servers, or facility systems fail. Dual Internet paths, redundant networking, multiple racks, A+B power, and N+1 cooling remove many single failure points. Stateless Java services also scale horizontally by adding more JVM replicas. The downside is cost. Extra servers, switches, generators, cooling equipment, and spare capacity use more money and power. Replication adds another trade-off. Synchronous replication can reduce the chance of losing recent writes, but it adds latency. Asynchronous replication improves availability and disaster recovery, but a replica may be behind and the newest writes can be lost during failover.

Why Interviewers Ask This

Interviewers ask this question to see whether you can connect application design with the data center underneath it. They want to know if you can trace a request, place Java replicas correctly, use cache and databases safely, and separate background work from the main path. They also test whether you can remove single failure points, plan for scaling, protect the system, and explain the cost of stronger availability choices.

Interviewer may ask next
How would you change this design if the business could not accept losing the latest database writes during a data center failure?

I would keep the same basic design, but I would make critical database writes wait for another durable copy before returning success. In the current diagram, the SQL Primary copies data to Read Replicas asynchronously. That keeps writes fast, but a replica can be slightly behind.

For critical data, the SQL layer would use synchronous replication for the required copy. The Java service would still send writes to the SQL Primary. The Primary would confirm the write only after the required replica has also stored it. The Distributed Cache would remain only a speed layer and would not decide whether the write is safe.

The Message Queue / Event Log, Background Consumer JVMs, Observability Stack, and facility design can stay unchanged. Backups would still go to Object / Backup Storage.

The downside is higher write latency. A slow or unavailable replica can also reduce write availability because the Primary may need to wait instead of accepting the write immediately.

What would you do if one compute rack failed but user traffic kept increasing?

I would keep the same request path and remove the failed rack from service. Local Health Checks let the load-balancing path identify unhealthy Java Service Replicas, so new requests should go only to healthy replicas in the remaining Compute Rack / Pool groups.

Because the Java services are stateless, I can add more JVM replicas to healthy racks. If that is not enough, I can add another rack and connect it through redundant Leaf Switches to the Spine Switches. CI/CD & Provisioning can deploy the same application version to those new compute nodes.

I would watch logs, metrics, and traces in the Observability Stack while capacity changes. If the extra traffic puts pressure on shared services, the design can also add cache nodes, Background Consumer JVMs, or Read Replicas.

The downside is spare-capacity cost. Keeping enough unused compute, network, power, and cooling capacity for a rack failure increases normal operating cost and power use.

16. How would you design a news app?System DesignMediumApple

Question Details

Design a news app and cover frontend, backend, data flow, ranking, storage, and scale considerations.

Short Interview Answer (30-60 seconds)

At a high level, I would treat this as a read-heavy news system. The main challenge is serving feeds quickly while keeping stories fresh and useful for each reader. I would explain three flows: reading the feed, opening an article, and processing new content in the background. Stateless Java service replicas handle requests, the Feed Cache speeds up hot feeds, and separate consumer JVMs update storage, search, analytics, and ranking data. The trade-off is that trending signals and feed updates can be slightly delayed.

Detailed Explanation

The goal is to give people a fast news feed and let them open full stories when they choose one. The difficult part is keeping feeds fresh while many users read at the same time. The app may also personalize results for signed-in users. New articles keep arriving from publishers and editors. The design separates fast reader requests from slower background processing. One path serves feeds and articles. Another path accepts and prepares new content. A third background path records reader activity and updates ranking signals.

Useful Questions to Ask the Interviewer
  1. Do we need personalized feeds for signed-in users and a general feed for anonymous users?
  2. How quickly should a newly published story appear in feeds and search?
  3. Do we need search, bookmarks, notifications, and editorial moderation from the first version?
  4. Is a small delay acceptable for trending data and feed freshness?
How would you design a news app? diagram
How to Explain It in an Interview
1. Explain how readers enter the system

I would start with the reader request path. The Mobile App and Web App send requests through the API Gateway / Load Balancer. It handles request validation, rate limiting, IP filtering, and routing. Static assets, images, videos, JavaScript, and CSS can be served through the CDN.

The Auth Service supports OIDC or OAuth 2.0 and JWT tokens. Reading news can stay anonymous. Login enables personalized feeds, bookmarks, and notifications.

2. Explain the feed read path

For GET /feed, the gateway routes the request to the Feed Service. The Feed Service first checks the Feed Cache for a hot personalized or global feed.

If the cache misses, the Feed Service asks the Search Service for candidate stories. The Search Service reads the Search Index for matching content. The Feed Service then sends those candidates to the Ranking Service for scoring and reordering.

The Ranking Service asks the User Profile Service for preferences and history. That service reads the User / Profile DB. The Ranking Service also reads popularity, trending, and freshness signals from the Analytics Store. The Feed Service returns the final feed items as JSON. If personalization or ranking is unavailable, the system can fall back to recent and popular stories.

3. Explain the article read path

When a reader opens a story, the request becomes GET /article/{id} and goes to the Article Service. The Article Service reads article information from the Article Metadata DB and content or media from Object Storage. It then returns the article detail to the client.

The Feed Cache can also keep hot article summaries. This reduces repeated work for commonly viewed stories.

4. Explain ingestion and background work

New content arrives from Publisher APIs, RSS, or the Editorial CMS. The Ingestion Service receives it and publishes an event to the durable Message Queue / Event Stream.

Separate Java consumer JVMs process those events. They perform Validation & Normalization, followed by Deduplication / Enrichment / Moderation. The processing path saves article metadata and content, updates the Search Index, and warms the Feed Cache. The Ranking Feature Updater / Trending Aggregator refreshes ranking signals.

Reader impressions, clicks, reads, watches, and bookmarks also enter the event stream. Those events update analytics and ranking signals in the background, so they do not slow the main read response.

5. Explain scale, failures, security, and operations

The online services are stateless Java 21/25 service replicas running as separate JVM processes. More replicas can be added behind the load balancer. Inside each JVM, virtual threads can handle many concurrent blocking I/O tasks. They do not replace the durable event stream or resource limits.

Trending data and feed freshness can be slightly delayed because those updates run in the background. Failed ingestion work can be retried. Poison messages can be moved to the DLQ so one bad event does not block the pipeline.

TLS, rate limiting, input validation, and secrets management protect the system. Metrics, centralized logs, distributed traces, alerts, and dashboards help operators understand failures and capacity.

Engineering Considerations / Design Trade-offs

The benefit is that the main read path stays fast. The Feed Cache avoids rebuilding popular feeds for every request, and stateless Java replicas can be added when traffic grows. Separating online serving from ingestion also keeps background content work from slowing readers. The downside is more moving parts, including caches, an event stream, search, analytics, and separate consumer JVMs. Trending information and feed updates may appear a little later because that work happens in the background. We accept this because fast reading is more important than perfectly instant trend data. Failed ingestion tasks also need retries and DLQ handling.

Why Interviewers Ask This

Interviewers want to see whether you can break a large product into clear request and background flows. They also want to see how you make a read-heavy system fast with caching, ranking, search, and stateless services. A strong answer shows that you understand which work must happen immediately, which work can happen later, how failures are isolated, and how to explain the trade-offs clearly.

Interviewer may ask next
What would you change if the news feed suddenly had ten times more read traffic during a major breaking-news event?

I would keep the same architecture and focus first on the read path. The main change would be adding more stateless Java service replicas behind the API Gateway / Load Balancer. Feed Service, Article Service, Search Service, and Ranking Service can scale horizontally because each replica runs in a separate JVM process and does not depend on local user state.

I would also rely more heavily on the Feed Cache for hot home and category feeds. During breaking news, many readers ask for similar stories, so cached feed results reduce repeated search and ranking work. Static assets and media can continue through the CDN.

If the Feed Cache misses, the normal fallback still works. The Feed Service can get candidates through the Search Service and Search Index, then send them to the Ranking Service. If personalization or ranking becomes overloaded, the existing recent-and-popular fallback keeps the app useful.

The downside is that some readers may temporarily see less personalized or slightly older feed results.

What happens if the ingestion pipeline keeps receiving an article event that repeatedly fails processing?

I would keep the same ingestion path and use the retry and DLQ behavior shown in the design. The Ingestion Service still publishes the article event to the durable Message Queue / Event Stream. Separate Java consumer JVMs then run Validation & Normalization and Deduplication / Enrichment / Moderation.

If processing fails because of a temporary problem, the task can be retried. That gives short-lived failures a chance to recover without losing the article. If the same event keeps failing, it becomes a poison message and moves to the DLQ, which is the Dead Letter Queue.

This prevents one bad article from blocking other ingestion work. Operators can use the existing logs, traces, alerts, metrics, and dashboards to investigate the failed event. Once the cause is fixed, the event can be processed again through the pipeline.

The downside is that the article will not appear in storage, search, or feeds until the failure is fixed and processing succeeds.

17. How would you design a scalable and efficient system for handling millions of requests per second?System DesignHardApple

Question Details

Explain how you would design a high-throughput system, including load balancing, scaling, and bottleneck management.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to handle a huge number of requests without letting one slow part overload the whole system. The main challenge is keeping the normal request path fast while protecting downstream systems during traffic spikes. I would explain the design in three flows: edge request handling, data access, and background work. Stateless JVM replicas scale horizontally, hot reads use the Distributed Cache, and non-critical work goes through the Durable Event Queue. The trade-off is more operational complexity.

Detailed Explanation

The system must handle a very large number of requests and keep responding quickly as traffic grows. The hard part is that one slow dependency can cause waiting work to build up and spread pressure through the system. The diagram solves this by protecting requests at the edge, spreading work across stateless Java replicas, using a cache for hot reads, partitioning the datastore, and moving non-critical work into the background. I would explain the request path, data path, background path, and failure controls.

Useful Questions to Ask the Interviewer
  1. Are most requests reads, writes, or a balanced mix?
  2. Which requests must finish before we return a response?
  3. How much delay is acceptable for background work?
  4. What should happen when the datastore or another dependency becomes slow?
How would you design a scalable and efficient system for handling millions of requests per second? diagram
How to Explain It in an Interview
1. Protect the request at the edge

I would first protect the system before work reaches the Java service. Clients send an HTTPS request to the Edge / API Gateway (Global). It handles TLS Termination, Authentication & Authorization, Request Validation, Rate Limiting, and Load Shedding.

Rate Limiting controls how much work a client can send. Load Shedding rejects some work when the system is overloaded. The gateway then sends the validated request to the Java API Service Tier.

2. Scale the Java API Service Tier

The Java API Service Tier stays stateless so we can add more JVM replicas horizontally. Each replica is a separate JVM with isolated heap memory. Shared state must live in external systems.

Inside the Representative JVM, Request Handling uses Virtual Threads for many blocking I/O operations. Virtual threads improve concurrency, but they do not remove resource limits or backpressure. Business Logic uses the Resilience Layer with Circuit Breaker, Timeouts, and Bulkhead controls. Bounded Connection Pools limit the Cache Client Pool, DB Connection Pool, and Queue Producer Pool.

3. Keep the data path fast

The service first sends a cache lookup to the Distributed Cache (In-Memory). On a cache hit, the cached JSON result returns quickly and reduces datastore pressure.

If the cache misses, the service sends a read/write query to the Partitioned Operational Datastore. The datastore routes work to the correct shard and returns the query result. The cache can then be updated after the read or write. Partitioning spreads data and work across shards instead of relying on one node.

4. Move non-critical work into the background

Some work should not delay the main response. The Java service publishes an event to the Durable Event Queue. Background Consumers (Workers) consume the event and process it separately.

Workers can read or write the Partitioned Operational Datastore as needed. A failed operation may retry with backoff only when repeating it is safe, meaning it will not create an incorrect duplicate result.

5. Handle failures and observe the system

Circuit breaking and Timeouts stop slow dependencies from holding resources forever. Bulkhead limits isolate resource pressure. Graceful degradation means the system may provide less functionality instead of failing every request.

The JSON response returns from the Java API Service Tier through the Edge / API Gateway to the Clients. Observability collects Metrics, Logs, and Traces from the important components. The trade-off is more operational complexity in exchange for better scale and resilience.

Engineering Considerations / Design Trade-offs

The benefit is that each layer protects the next one. Stateless JVM replicas let the service scale horizontally. The Distributed Cache makes hot reads faster and reduces pressure on the datastore. Partitioning spreads datastore work across shards. The Durable Event Queue keeps non-critical work away from the main response. The downside is more moving parts. Cache entries need updates after datastore work. Queue operations may need safe retries with backoff. More replicas also create more connections and more things to monitor. We accept this extra complexity because it reduces bottlenecks and keeps one slow dependency from overwhelming the full request path.

Why Interviewers Ask This

The interviewer wants to see whether you can break a large traffic problem into clear flows and protect the system as load grows. They also want to see whether you understand stateless scaling, caching, datastore partitioning, backpressure, background processing, and failure controls. The important skill is judgment. You should explain why each choice helps, where the next bottleneck may appear, and what extra complexity the design creates.

Interviewer may ask next
What would you change if the Partitioned Operational Datastore suddenly became much slower during a traffic spike?

I would keep the same design, but I would make the existing protection around the datastore more important. The Java API Service Tier should use Timeouts and the Circuit Breaker so requests do not wait forever for a slow datastore. Backpressure and the bounded DB Connection Pool stop too many waiting requests from consuming all service resources.

The Distributed Cache becomes especially valuable for hot reads because a cache hit avoids the datastore. At the Edge / API Gateway, Rate Limiting and Load Shedding can reduce incoming work when the system cannot process everything safely.

Non-critical work should still go through the Durable Event Queue so Background Consumers can process it separately. If workers also need the datastore, their work must respect the same pressure limits. The main downside is reduced functionality or rejected requests while the datastore is slow, but this is better than allowing the whole system to collapse.

How would you prevent background retries from creating duplicate results?

I would keep the Durable Event Queue and Background Consumers, but retries would happen only for work that is safe to repeat. In simple words, processing the same message again must not create an incorrect duplicate result.

If a worker fails, it can retry with backoff. Backoff means waiting before trying again, and usually waiting longer after repeated failures. This avoids hammering a dependency that may still be unhealthy. The worker can still read or write the Partitioned Operational Datastore as needed, but the repeated operation must produce a correct result if it runs again.

Nothing changes in the main synchronous path. The Java API Service Tier can publish non-critical work and continue returning the normal JSON response without waiting for the worker. The downside is extra application logic and testing because each retryable operation must be designed carefully in production.

18. How do you determine and provision EC2 for a new project?System DesignMediumApple

Question Details

Explain how you would choose instance type, size, memory, storage, CPU, and other baseline capacity for a new project.

Short Interview Answer (30-60 seconds)

At a high level, I would size EC2 from the workload instead of guessing an instance type. The main challenge is balancing CPU, memory, storage, network needs, availability, and future growth without buying too much unused capacity. I would explain it in three parts: understand the workload, choose the EC2 baseline, then deploy across multiple Availability Zones and measure real traffic. I would use an Auto Scaling Group behind an ALB, with load testing and production telemetry guiding later right-sizing.

Detailed Explanation

The goal is to choose enough EC2 capacity for a new Java service without paying for much more than we need. The difficult part is that a new project has limited production history. CPU use, memory use, storage activity, and traffic can grow differently. The diagram solves this in a clear order. First, we understand the workload. Next, we choose the instance family, size, storage, and launch settings. Then we run the service across two Availability Zones and use load testing and real production measurements to improve the sizing.

Useful Questions to Ask the Interviewer
  1. What traffic pattern and concurrency do we expect?
  2. What latency target should the service meet?
  3. Is the workload mainly CPU work or waiting on I/O?
  4. How much heap and total memory does one JVM need?
  5. What storage size, IOPS, and network needs do we expect?
  6. How quickly could traffic or data grow?
  7. What availability, security, and compliance requirements apply?
How do you determine and provision EC2 for a new project? diagram
How to Explain It in an Interview
1. Understand the workload first

I would start with the Requirement Inputs shown in the diagram. These include traffic pattern, latency, concurrency, CPU versus I/O behavior, memory footprint, storage needs, growth rate, HA target, and security constraints.

These inputs tell me which resource is most likely to become the limit. They also stop me from choosing an EC2 instance only because it is familiar.

2. Choose the EC2 family, size, and storage

Next, I would choose the instance family from the workload profile. General Purpose M fits balanced work. Compute Optimized C fits CPU-heavy work. Memory Optimized R fits memory-heavy work.

Then I would choose vCPU and RAM with some headroom. For storage, the diagram uses gp3 EBS and sizes capacity, IOPS, and throughput to the workload.

The Launch Template keeps the AMI, JDK, instance type, root volume, Security Group, IAM role, user data, and IMDSv2 settings consistent across new instances.

3. Deploy the Java service across two Availability Zones

The request enters from the Client through the DNS / Entry Point and reaches the Application Load Balancer. The ALB sends traffic to an Auto Scaling Group inside private subnets across Availability Zone A and Availability Zone B.

Each EC2 instance runs a separate JVM process. Inside it are the Java Application / Service, heap, off-heap or native memory, and Threads / Virtual Threads. Separate JVM processes do not share heap state.

The diagram also shows Local Temp / Ephemeral Storage as instance store. That storage is local to a supporting EC2 type that provides instance store, so it should not hold durable data.

4. Use shared services and observability

The Java service can use the optional Cache for hot reads and the Relational Database for durable data. The database shown is Amazon RDS. Cache and database requests return results to the application instance.

Metrics, logs, and traces flow to the external Observability area. The diagram shows CloudWatch, OpenSearch, X-Ray, S3, and Firehose as operational tools.

Private subnets keep EC2 instances off the public internet. Security Groups restrict network access, IAM uses least privilege, health checks keep unhealthy instances from receiving traffic, and rolling updates use Launch Template versioning and instance refresh.

5. Measure, scale, and right-size

The first sizing decision is only a baseline. Load Testing checks assumptions before production. Production Telemetry shows how the service behaves with real traffic.

The Auto Scaling Policy reacts to signals such as CPU utilization, latency, request rate, concurrency, and memory pressure. Memory pressure normally needs an OS or application metric rather than being assumed as a default EC2 metric.

For a stateless service, I would scale out first by adding replicas. I would scale up when one JVM needs more CPU or memory. The trade-off is cost versus safety margin, so I would keep measuring and right-size the instance type, storage, Auto Scaling settings, and JVM tuning over time.

Engineering Considerations / Design Trade-offs

The benefit is that this approach starts from workload needs instead of guessing. Multi-AZ deployment and the Auto Scaling Group also make the service more resilient when an instance or one Availability Zone has problems. The downside is cost. Extra headroom and multiple instances mean some capacity may sit unused. Scaling out works well for a stateless Java service, but every replica still needs memory, network connections, and supporting resources. Scaling up helps when one JVM needs more CPU or RAM, but a single instance can only become so large. We accept this trade-off and keep using measurements to right-size the service.

Why Interviewers Ask This

Interviewers ask this to see whether you can turn an unclear capacity problem into a practical plan. They want to know whether you understand CPU, memory, storage, JVM needs, availability, security, and scaling. They also want to see whether you test assumptions instead of guessing. A strong answer shows good judgment because EC2 sizing is an ongoing cycle of estimating, testing, observing, and adjusting.

Interviewer may ask next
What would you change if traffic grows much faster than expected after launch?

I would keep the same architecture and first use Production Telemetry to find the real bottleneck. I would check CPU utilization, memory pressure, latency, request rate, and concurrency before changing the instance type.

If each Java instance still has enough CPU and memory, I would scale out the Auto Scaling Group. The Application Load Balancer can then spread requests across more EC2 instances in both Availability Zones.

If each JVM is running out of CPU or memory, adding replicas may not fix the per-instance limit. I would re-evaluate the instance family or size and scale up where needed. I would also repeat Load Testing with the new traffic pattern.

The downside is higher cost. More instances or larger instances both increase spending, so I would continue right-sizing after traffic becomes stable.

How would you choose between a Compute Optimized instance and a Memory Optimized instance for this Java service?

I would choose based on the resource that becomes the limit under realistic load. If CPU stays very busy while the JVM still has comfortable memory headroom, I would lean toward a Compute Optimized C family.

If the service needs a large heap, significant off-heap or native memory, or reaches memory pressure before CPU becomes the main problem, I would lean toward a Memory Optimized R family. I would not size RAM from Java heap alone because the JVM, native memory, and operating system also need memory.

I would confirm the choice with Load Testing and Production Telemetry. If CPU and memory needs are balanced, a General Purpose M family may remain the better baseline.

The downside is cost. A more specialized or larger instance can improve performance, but I would only pay for it when measurements show that it solves the actual bottleneck.

19. How do you migrate millions or billions of Apple user accounts to a new service without negatively impacting users?System DesignHardApple

Question Details

Explain how you would migrate Apple user accounts to a new service safely, with minimal user impact and rollback planning.

Short Interview Answer (30-60 seconds)

At a high level, I would migrate accounts gradually instead of moving everyone at once. The main challenge is keeping the user experience stable while account data moves in the background. I would split the design into the live request path, the migration path, and the cutover and rollback path. Each account is routed to the old or new service using migration state. We verify data before switching traffic. The trade-off is more operational complexity in exchange for safer rollout and fast rollback.

Detailed Explanation

The goal is to move a very large number of Apple user accounts to a new service without users noticing the change. The difficult part is that normal account requests must keep working while data is copied, updated, and checked in the background. We also need to avoid moving everyone at once because one mistake could affect many users. The diagram solves this by keeping the client API unchanged, routing each account to either the old or new service, migrating data separately, checking the new copy, and changing routing only after those checks succeed.

Useful Questions to Ask the Interviewer
  1. Can we migrate accounts gradually by account or cohort?
  2. How long should the old service stay available after cutover?
  3. What verification checks are required before an account moves to the new service?
How do you migrate millions or billions of Apple user accounts to a new service without negatively impacting users? diagram
How to Explain It in an Interview
1. Keep the client-facing path stable

I would start by keeping the API seen by Apple user devices and apps unchanged. Requests enter through the API Gateway / Edge over HTTPS. That layer handles TLS termination, routing, WAF protection, and DDoS protection.

The request then passes through AuthN, AuthZ, validation, and rate limiting. After those checks, the Account Gateway / Access Facade handles the account operation. It runs as stateless Java 21/25 JVM replicas. Virtual-thread request handlers can support many concurrent blocking operations, but they do not provide durable storage or replace resource limits.

2. Route each account to the correct service

The Account Gateway / Access Facade uses the Migration Routing Registry / State Store to decide where the request should go. The store keeps per-account or per-cohort state such as MIGRATED or NOT MIGRATED. Its reads and writes are strongly consistent, meaning a routing change is immediately used for later routing decisions.

A NOT MIGRATED account goes to the Old Account Service and Old Account Database. A MIGRATED account goes to the New Account Service and New Account Database. The selected service performs the account read or write and returns the response through the facade. This lets the backend change without changing the client API.

3. Move data through the background migration path

The migration runs separately from live user requests. Backfill Export + Change Capture first copies the existing data from the Old Account Database. It also captures ongoing inserts, updates, and deletes so changes made during the migration are not lost.

Those CDC events enter the ordered Migration Event Queue. Java Migration Workers run as separate JVM consumers and process queued events with backpressure, which means they limit work when downstream systems are busy. Processed events then pass through Transform + Validate + Idempotent Upsert into New Account Database. Idempotent means processing the same account update again does not create a duplicate account.

4. Verify before changing routing

The Verification Service checks record counts, hashes, and sampled account reads against the migrated data. It sends status and reports to the Cutover Controller / Rollback Controller.

Only after verification succeeds does the controller update the Migration Routing Registry / State Store. The rollout follows Backfill, Sync Changes, Verify, Canary Cohorts, and Progressive Cutover. Moving accounts or cohorts gradually keeps the number of affected users small if a problem appears.

5. Retry failures and keep rollback fast

If migration processing fails, the event can go to the Retry / Dead-Letter Queue with error details. Failed work can be retried with backoff instead of blocking the live request path.

If verification fails or the new path has problems, the Cutover Controller / Rollback Controller can switch the affected account or cohort back to the old path. The old database remains available until the stability window finishes. Shared Observability / Audit collects metrics, logs, traces, and cutover audit events. The benefit is a safer migration. The downside is the extra routing, verification, retry, and operational complexity.

Engineering Considerations / Design Trade-offs

The benefit is that users keep the same API while accounts move gradually. Per-account or cohort routing limits how many users are affected by one problem. Verification before cutover protects data correctness. Keeping the old path available also makes rollback fast. The downside is more moving parts. We need migration state, a queue, background workers, verification, retry handling, rollback control, and shared monitoring. The migration also takes longer because we backfill, sync changes, verify, test small cohorts, and then expand. We accept this extra complexity because a slow and reversible migration is much safer than a one-time move of every account.

Why Interviewers Ask This

Interviewers ask this to see whether you can move a huge live system safely instead of only designing a new system from scratch. They want to see how you separate user traffic from background migration work, protect data correctness, limit the impact of failures, and plan rollback before cutover. They also want to see whether you understand Java service boundaries, reliable queue processing, verification, routing decisions, and practical trade-offs.

Interviewer may ask next
What would you do if the new service starts returning errors after some cohorts have already been cut over?

I would stop further cutovers and move the affected accounts back to the old path. The Cutover Controller / Rollback Controller would update the Migration Routing Registry / State Store from MIGRATED to NOT MIGRATED for those accounts or cohorts. New requests would then go back through the Old Account Service to the Old Account Database.

This works because the diagram keeps the old path available until the stability window is complete. Shared Observability / Audit would help identify the failing cohorts and show whether the problem is in the New Account Service, the New Account Database, or another part of the new path.

I would keep migration events retryable and continue using idempotent upserts so repeated background work does not create duplicate accounts. Verification would need to pass again before those accounts are cut over a second time. The downside is that keeping the old system available and supporting routing rollback increases operational complexity.

How would you keep the New Account Database current while the initial backfill is still running?

I would use the Backfill Export + Change Capture flow shown in the diagram. The initial backfill copies existing account data from the Old Account Database. At the same time, change capture records later inserts, updates, and deletes as CDC events.

Those events enter the ordered Migration Event Queue. Java Migration Workers consume them in the background. The events then pass through Transform + Validate + Idempotent Upsert into New Account Database. The idempotent upsert means the same account update can be retried without creating a duplicate account.

Before an account or cohort is marked MIGRATED, the Verification Service checks record counts, hashes, and sampled account reads. Only after those checks succeed does the Cutover Controller / Rollback Controller update the Migration Routing Registry / State Store. The downside is that this requires reliable queue processing, backpressure, retries, and careful verification while both systems remain active.

20. What is the use of an API Gateway?API DesignMediumApple

Question Details

Explain the role of an API Gateway in a backend system and what problems it solves.

Short Interview Answer (30-60 seconds)

At a high level, I would use the API Gateway as the single public entry point for backend APIs. The Web App, Mobile App, and Partner App send HTTPS requests with a JWT to the gateway. The gateway validates identity, checks authorization, applies rate limits, transforms requests when needed, and routes calls to the User, Order, or Inventory Service. Service responses return through the gateway to the client. The benefit is centralized control and hidden internal services. The trade-off is that the gateway becomes an important shared component that must be operated carefully.

Detailed Explanation

This question asks why we place one controlled entry point between outside applications and many backend services. Without it, each client may need to know every internal service and repeat the same security and traffic rules. The goal is to make access simpler and keep common rules in one place. In this design, all client requests first reach the API Gateway. The gateway checks who is calling, decides whether the request is allowed, chooses the correct service, and sends the response back.

Useful Questions to Ask the Interviewer
  • Should all external clients use the same gateway policies?
  • Which gateway responsibilities are most important: security, routing, rate limiting, or aggregation?
  • Do backend services remain private and reachable only through the gateway?
What is the use of an API Gateway? diagram
How to Explain It in an Interview
1. Start with the API boundary

I would first explain that the API Gateway is the single public entry point. The Web App, Mobile App, and Partner App do not call internal services directly. Each client sends an HTTPS API request with a JWT to the gateway. A JWT is a token that carries caller identity information and claims. This keeps the internal service layout hidden from external clients. It also gives the backend one place to apply common rules before requests reach business services.

2. Validate identity and authorization

The request next goes through security checks at the gateway. The gateway owns authentication and JWT validation in this diagram. It sends a JWT validation request, described as token introspection, to the Identity Provider (Auth Service). The identity system returns a validation result with token validity and claims. Authentication answers who the caller is. Authorization is a separate gateway responsibility that decides whether that caller may continue. The diagram does not show a separate failure path, so I would not invent one.

3. Apply shared gateway policies

After security checks, the gateway applies shared API concerns. These include rate limiting, request transformation, routing, response aggregation, and centralized logging or monitoring. Rate limiting controls how much traffic a caller can send. Request transformation can adapt the incoming request before forwarding it. Response aggregation lets the gateway combine backend results when that behavior is needed. These controls reduce repeated work across clients and services.

4. Route the request to the correct service

The gateway then routes the request to the matching backend service. The diagram shows HTTPS routes to /users, /orders, and /inventory. The User Service handles user work. The Order Service handles order work. The Inventory Service handles inventory work. Each backend sends a separate service response back to the gateway. The services stay behind the Internal Backend System boundary, so external clients do not need to know their locations or topology.

5. Keep data ownership with the service

The Order Service owns access to the Orders DB shown in the diagram. It sends an SQL query to the PostgreSQL database and receives the query result. The API Gateway does not query this database directly. This separation is important because the gateway handles cross-cutting API concerns, while the Order Service owns order business logic and order data access.

6. Return the response and record observability data

The service response returns to the API Gateway first. The gateway then sends a separate HTTPS response back to the original client. At the same time, the gateway sends logs, metrics, and traces to OBSERVABILITY using the shown OTLP or HTTPS flow. Observability is a side path. It helps operators understand traffic and behavior, but it does not own the business response. The main trade-off is centralization: the gateway simplifies clients and services, but it also becomes a shared component that needs careful reliability, security, and capacity planning.

Practical Complexity & Trade-offs

The benefit of this design is that clients see one stable entry point instead of many internal services. Security, routing, rate limiting, request transformation, response aggregation, and logging can be applied consistently at the gateway. This reduces repeated work in every backend service. Keeping the Orders DB behind the Order Service also preserves clear data ownership. The downside is that the gateway becomes a very important shared component. More gateway logic can increase operational complexity and can affect many APIs at once if configured badly. Token validation also adds another dependency between the gateway and the Identity Provider (Auth Service). We accept this because central control makes the external API simpler, hides internal service topology, and gives one place to enforce common policies.

Why Interviewers Ask This

Interviewers ask this question to see whether you understand API boundaries and shared backend concerns. They want to know if you can model request and response directions correctly, separate authentication from authorization, keep business data ownership inside services, and explain why routing and rate limiting belong at a gateway. They are also testing whether you can discuss the benefit of central control without ignoring the operational risk of making the gateway a critical shared component.

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

I would keep the same architecture and use the API Gateway's existing rate-limiting responsibility to control the spike. The affected flow is still Client → API Gateway → backend service → API Gateway → Client. The gateway would remain the single public entry point, so clients would not bypass it and call User Service, Order Service, or Inventory Service directly. Rate limiting is useful because it can reduce excessive traffic before that traffic reaches internal services. Authentication, JWT validation, authorization, routing, and observability would remain in the same places shown in the diagram. I would also watch the gateway's logs, metrics, and traces through OBSERVABILITY to understand which clients and routes are generating the load. The main downside is that the gateway must itself have enough capacity for the accepted traffic. The diagram does not show autoscaling, retries, queues, or fallback paths, so I would not claim those mechanisms are already part of this design.

Why centralize JWT validation and logging at the API Gateway instead of putting them only in each backend service?

Centralizing those responsibilities gives the external API one consistent control point. In this diagram, each client sends an HTTPS request with a JWT to the API Gateway. The gateway sends a validation request to the Identity Provider (Auth Service) and receives the validation result. It also owns authorization and sends logs, metrics, and traces to OBSERVABILITY. The backend services can then focus on their business responsibilities, such as the Order Service handling orders and accessing the Orders DB. This also helps hide the internal service topology from clients. Correctness is maintained because the gateway remains the component shown as owning those cross-cutting checks and routing decisions. The downside is stronger dependence on the gateway. A configuration mistake in one shared place can affect many routes. The design therefore gains consistency and simpler services, but it accepts more responsibility and operational importance at the gateway layer.

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.