Google Java Developer Interview Questions & Answers

google icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

11. Process start and finish logs and output the request intervals.CodingMediumGoogle

Question Details

Given log entries with start and finish times for requests, produce the start and end times for each request sorted by finish time.

Short Interview Answer (30-60 seconds)

I would first build a HashMap from each requestId to its startTime. Then I process every finish log, use the requestId to find the matching start time, and create an interval containing requestId, startTime, and finishTime. After building all matched intervals, I sort them by finishTime and return the list. HashMap lookup is O(1) on average. The total expected time is O(S + F + K log K), with O(S + K) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We receive one list that tells us when requests started and another list that tells us when requests finished. Each entry has a request name and a time. For every finish entry, we need to find the start entry with the same request name. We then combine those two times into one result for that request. After all matching results are created, we order them by finish time. The goal is to match each request correctly and return the completed request intervals in the required order.

Useful Questions to Ask the Interviewer
  1. Can I assume that each finish entry normally has a matching start entry with the same requestId?
  2. If two requests have the same finish time, is any order between them acceptable?
  3. If a finish entry has no matching start entry, should I ignore it, reject the input, or handle it separately?
Process start and finish logs and output the request intervals. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input contains startLogs and finishLogs. Each log entry stores a requestId and a timestamp. The output contains one interval for each matched request. Each interval stores requestId, startTime, and finishTime. The final intervals must be sorted by finishTime.

The diagram uses this exact example: startLogs = [(A,1), (B,2), (C,4), (D,6)] finishLogs = [(A,7), (B,5), (C,9), (D,8)]

The expected output is: [(B,2,5), (A,1,7), (D,6,8), (C,4,9)]

2. Choose the algorithm and data structure

I use a HashMap called startById. It stores requestId -> startTime. This lets me quickly find the start time when I process a finish log.

After reading all start logs, the map is: startById = {A:1, B:2, C:4, D:6}

The main invariant is that every interval added to the result uses the start time stored for the same requestId as the current finish log.

3. Initialize the state

I first create an empty HashMap. I process the start logs and put each requestId and startTime into the map. I also create an empty result list.

Initial state: startById = {A:1, B:2, C:4, D:6} result = []

Now every finish log can find its matching start time with an average O(1) HashMap lookup.

4. Walk through the example

Step 1: The finish log is (A,7). The map lookup A -> 1 gives startTime 1. I create (A,1,7). The result becomes [(A,1,7)].

Step 2: The finish log is (B,5). The map lookup B -> 2 gives startTime 2. I create (B,2,5). The result becomes [(A,1,7), (B,2,5)].

Step 3: The finish log is (C,9). The map lookup C -> 4 gives startTime 4. I create (C,4,9). The result becomes [(A,1,7), (B,2,5), (C,4,9)].

Step 4: The finish log is (D,8). The map lookup D -> 6 gives startTime 6. I create (D,6,8). The result becomes [(A,1,7), (B,2,5), (C,4,9), (D,6,8)].

Step 5: I sort the completed intervals by finishTime in ascending order. Their finish times become 5, 7, 8, 9.

The returned result is [(B,2,5), (A,1,7), (D,6,8), (C,4,9)].

5. Explain why the result is correct

For each processed finish log, the HashMap returns the start time stored under the same requestId. Therefore, each created interval contains the matching start and finish times for that request. After all matched intervals are created, sorting by finishTime places them in nondecreasing finish-time order, which is exactly what the question requires.

6. Explain the Java implementation

The Java method first builds startById from startLogs. It then processes finishLogs in their given order. For each finish entry, it looks up the matching startTime. If no startTime exists, the code uses the optional defensive behavior shown in the diagram and skips that malformed finish entry. Otherwise, it creates an Interval and adds it to the result list. Finally, it sorts the intervals using Comparator.comparingInt(Interval::endTime) and returns them.

7. Explain complexity and edge cases

Let S be the number of start logs, F the number of finish logs, and K the number of matched intervals. Building the map takes expected O(S) time. Processing the finish logs takes expected O(F) time because HashMap lookup is O(1) on average. Sorting K intervals takes O(K log K). Therefore, total expected time is O(S + F + K log K). Auxiliary space is O(S + K).

Relevant edge cases shown in the diagram are empty inputs, finish logs already being sorted, unmatched finish logs when defensive handling is desired, and equal finish times. When finish times are equal, any nondecreasing finish-time order is acceptable unless the interviewer requires a tie-break rule.

Key Insight / Why This Solution Works

The key insight is to use two phases. First, build a HashMap that stores requestId -> startTime. This makes it fast to match each finish log with its start time. Second, create all matched intervals and sort them by finishTime. The central invariant is that every interval added to the result uses the start time stored for the same requestId as the finish log being processed. The HashMap handles matching efficiently, while the final sort handles the required output order.

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

public class Main {

    // A log entry contains one request ID and one timestamp.
    record LogEntry(String requestId, int time) {}

    // A completed interval contains its request ID, start time, and finish time.
    record Interval(String requestId, int startTime, int endTime) {}

    public static List<Interval> buildIntervals(
        List<LogEntry> startLogs,
        List<LogEntry> finishLogs
    ) {
        // Store requestId -> startTime so each finish log can find its matching start quickly.
        Map<String, Integer> startById = new HashMap<>();

        // Build the lookup map from all start logs.
        for (LogEntry start : startLogs) {
            startById.put(start.requestId(), start.time());
        }

        // Collect every successfully matched request interval here.
        List<Interval> result = new ArrayList<>();

        // Process finish logs in their given order and pair each one with its start time.
        for (LogEntry finish : finishLogs) {
            Integer startTime = startById.get(finish.requestId());

            // Optional defensive handling for an unmatched finish log.
            if (startTime == null) {
                continue;
            }

            // Create the interval only after finding the matching start time.
            result.add(new Interval(finish.requestId(), startTime, finish.time()));
        }

        // The required final order is ascending finish time.
        result.sort(Comparator.comparingInt(Interval::endTime));

        // Return the matched intervals in finish-time order.
        return result;
    }

    public static void main(String[] args) {
        // Use the exact example from the approved diagram.
        List<LogEntry> startLogs = List.of(
            new LogEntry("A", 1),
            new LogEntry("B", 2),
            new LogEntry("C", 4),
            new LogEntry("D", 6)
        );

        List<LogEntry> finishLogs = List.of(
            new LogEntry("A", 7),
            new LogEntry("B", 5),
            new LogEntry("C", 9),
            new LogEntry("D", 8)
        );

        // Run the same HashMap-plus-sort algorithm shown in the diagram.
        List<Interval> intervals = buildIntervals(startLogs, finishLogs);

        // Format the result using the same interval notation as the diagram.
        String output = intervals
            .stream()
            .map(
                interval ->
                    "(" +
                    interval.requestId() +
                    "," +
                    interval.startTime() +
                    "," +
                    interval.endTime() +
                    ")"
            )
            .reduce((left, right) -> left + ", " + right)
            .map(text -> "[" + text + "]")
            .orElse("[]");

        System.out.println(output);
    }
}
Time & Space Complexity

Let S be the number of start logs, F the number of finish logs, and K the number of matched intervals. Building the HashMap takes expected O(S) time. Processing the finish logs takes expected O(F) time because a Java HashMap lookup is O(1) on average. Sorting the K intervals costs O(K log K). So the total expected time is O(S + F + K log K). The map and result list use O(S + K) auxiliary space.

Where it is used

This pattern is useful when separate records belong to the same request or job and share an identifier. Examples include matching request-start and request-finish events, job lifecycle logs, transaction events, and service traces. A map connects related records quickly, and a final sort puts completed records into the required reporting order.

Why Interviewers Ask This

This problem tests whether you can connect related records using the right key, choose a suitable Java data structure, and separate matching from ordering. The interviewer can see whether you understand HashMap behavior, build correct intervals, sort by the correct field, handle malformed unmatched entries carefully, and include the sorting cost in the complexity. It also tests whether your explanation, example, code, and returned result stay consistent.

Common interview mistakes

A common mistake is using the wrong mapping direction. The map must store requestId -> startTime. Another mistake is creating an interval from a finish log without first finding the matching start time. Candidates may also forget to sort by finishTime after building the intervals. A frequent complexity mistake is claiming O(S + F) time and ignoring the O(K log K) sorting step. Another mistake is adding an unnecessary tie-break rule when the question only requires nondecreasing finish-time order.

Interview tip

Say the map meaning out loud before coding: requestId -> startTime. Then describe the solution as two phases: match every finish log to its start time, and sort the completed intervals by finishTime. This makes both the code and the complexity easy to explain.

Interviewer may ask next
How would the solution change if the logs arrive continuously as a stream?

Keep a map of requestId -> startTime for requests that have started but not finished. When a start event arrives, store it. When the matching finish event arrives, create the interval and remove that requestId from the map if it is no longer needed. If the final output must still be globally sorted by finishTime, completed intervals must be stored and sorted later, so sorting remains O(K log K). Space is O(U + K), where U is the number of unfinished requests. If finish events already arrive in nondecreasing finish-time order and streaming output is allowed, the final sort can be avoided.

What if two intervals have the same finish time and a deterministic order is required?

Add an explicit secondary key to the comparator. For example, sort first by endTime and then by requestId using Comparator.comparingInt(Interval::endTime).thenComparing(Interval::requestId). The HashMap matching phase does not change. Correctness is preserved because finishTime remains the primary sorting key. The total expected time stays O(S + F + K log K), and auxiliary space stays O(S + K). The tradeoff is that this adds a tie-break rule that the original problem did not require.

12. Given a matrix with S, F, 0, 1, and capital letters, find the shortest distance from A to B.CodingMediumGoogle

Question Details

Given a matrix whose cells may contain S, F, 0, 1, or capital letters, find the shortest distance from A to B and follow up by returning the path.

Short Interview Answer (30-60 seconds)

I treat the matrix like an unweighted graph. I find A and B, then run BFS from A with a queue, a visited grid, and parent links. BFS checks cells layer by layer, so the first time B comes out of the queue, I have the shortest route. I then walk back through the parent links to rebuild the path. Because each cell is processed at most once and each move is one step, the time is O(R*C) and the extra space is O(R*C).

Detailed Explanation

See the Code while reading this explanation.

This problem asks me to move from A to B on a grid. Some cells are blocked with F. The other cells can be used. I need the shortest walk and the exact path. I would check nearby cells one layer at a time and remember where each cell came from. That fits well because every move has the same cost. The first time I reach B, I know I have the shortest route. Then I can rebuild the path from the saved parent links.

Useful Questions to Ask the Interviewer
  1. Should I return just the shortest distance, or both the distance and the path?
  2. Are all cells except F walkable, even if they contain letters?
  3. What should I return if A or B is missing?
Given a matrix with S, F, 0, 1, and capital letters, find the shortest distance from A to B. diagram
How to Explain It in an Interview
1. Understand the input and required output

The grid has rows and columns. A is the start. B is the target. F means blocked. Every other cell can be entered. The answer is the shortest number of moves and one shortest path. The path must list the cells from A to B in order.

2. Choose the algorithm and data structure

I use BFS, which means breadth-first search. BFS checks all cells at distance 1, then all cells at distance 2, and so on. That is the right fit because every move has the same cost. I use a queue for the cells to process, a visited grid so I do not add the same cell twice, and a parent grid so I can rebuild the path later.

3. Initialize the state

First I scan the grid to find A and B. If one of them is missing, I return -1 and an empty path. Then I push A into the queue, mark it visited, and store a start marker in the parent grid. I also set the four movement directions: up, down, left, and right.

4. Walk through the example

The diagram uses A at (2,2) and B at (4,6). BFS starts from A and explores the grid one layer at a time. It skips F cells like a wall. When a new cell is reached, I save its parent, which is the cell I came from. The first shortest route shown in the diagram is (2,2) -> (3,2) -> (3,3) -> (3,4) -> (3,5) -> (3,6) -> (4,6). That path has 7 cells, so the distance is 6.

5. Explain why the result is correct

The key idea is that BFS processes cells in increasing distance order. That means the first time B is removed from the queue, no shorter path can exist. The parent links keep the exact route that led to each cell. When I follow those links backward from B to A, I rebuild one shortest path.

6. Explain the Java implementation

The Java code first searches the grid for A and B. Then it creates visited and parent arrays. The BFS loop removes one cell at a time from the queue. For each cell, it checks the four neighbors. It ignores out-of-bounds cells, blocked cells, and cells already visited. When it adds a neighbor, it marks it visited right away and stores the current cell as its parent. After BFS ends, it walks backward from B to A using the parent grid, reverses the list, and returns the distance and path.

7. Explain complexity and edge cases

The time is O(R*C) because each cell is processed at most once and each cell checks only four neighbors. The extra space is O(R*C) for the queue, visited grid, and parent grid. Important edge cases are a missing A or B, a blocked target, a grid where A has no open neighbors, and a case where A and B are very close or even adjacent.

Key Insight / Why This Solution Works

This is BFS on a grid. The grid acts like an unweighted graph. Each open cell is a node, and each move has cost 1. The queue keeps cells in the order they are discovered, so cells at a smaller distance are handled first. That is the key invariant. The first time B is dequeued, the path is already shortest. The parent grid stores where each cell came from, so I can rebuild the path after the search. This works because BFS never skips a shorter route when all edges have the same cost.

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

public class Main {

    public static class Result {

        public final int distance;
        public final List<int[]> path;

        public Result(int distance, List<int[]> path) {
            this.distance = distance;
            this.path = path;
        }
    }

    public static Result shortestPath(char[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;

        int startRow = -1;
        int startCol = -1;
        int endRow = -1;
        int endCol = -1;

        // Find A and B first. The diagram uses A as the start and B as the target.
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (grid[r][c] == 'A') {
                    startRow = r;
                    startCol = c;
                } else if (grid[r][c] == 'B') {
                    endRow = r;
                    endCol = c;
                }
            }
        }

        // If A or B is missing, there is no valid answer.
        if (startRow == -1 || endRow == -1) {
            return new Result(-1, new ArrayList<>());
        }

        boolean[][] visited = new boolean[rows][cols];
        int[][] parent = new int[rows][cols];
        for (int[] row : parent) {
            Arrays.fill(row, -1);
        }

        // Up, down, left, right.
        int[] dr = { -1, 1, 0, 0 };
        int[] dc = { 0, 0, -1, 1 };

        ArrayDeque<int[]> queue = new ArrayDeque<>();

        // Put the start cell into the queue and mark it visited immediately.
        queue.offer(new int[] { startRow, startCol });
        visited[startRow][startCol] = true;
        parent[startRow][startCol] = -2; // special marker for the start cell

        // BFS processes cells in increasing distance order.
        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int r = current[0];
            int c = current[1];

            // The first time we remove B from the queue, the path is shortest.
            if (r == endRow && c == endCol) {
                break;
            }

            for (int k = 0; k < 4; k++) {
                int nr = r + dr[k];
                int nc = c + dc[k];

                // Skip cells outside the grid.
                if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) {
                    continue;
                }

                // F means blocked. Any other cell type is open.
                if (grid[nr][nc] == 'F') {
                    continue;
                }

                // Do not add the same cell twice.
                if (visited[nr][nc]) {
                    continue;
                }

                // Mark visited when adding the cell. This keeps the queue clean.
                visited[nr][nc] = true;

                // Store where this cell came from, so we can rebuild the path later.
                parent[nr][nc] = r * cols + c;
                queue.offer(new int[] { nr, nc });
            }
        }

        // If B was never reached, return the failure result from the diagram.
        if (!visited[endRow][endCol]) {
            return new Result(-1, new ArrayList<>());
        }

        // Rebuild one shortest path from B back to A.
        List<int[]> path = new ArrayList<>();
        int cr = endRow;
        int cc = endCol;

        while (parent[cr][cc] != -2) {
            path.add(new int[] { cr, cc });
            int encodedParent = parent[cr][cc];
            cr = encodedParent / cols;
            cc = encodedParent % cols;
        }

        // Add A, then reverse the path so it goes from A to B.
        path.add(new int[] { cr, cc });
        Collections.reverse(path);

        int distance = path.size() - 1;
        return new Result(distance, path);
    }

    public static void main(String[] args) {
        // Example grid from the diagram.
        char[][] grid = {
            { 'S', '1', '0', 'C', '0', '1', 'F' },
            { '0', 'F', '1', '0', '1', '0', '1' },
            { '0', '0', 'A', '1', 'F', '0', '1' },
            { '1', '1', '0', '0', '1', '0', '0' },
            { 'F', '0', '1', '0', '1', 'F', 'B' },
            { '0', '0', 'F', '1', '0', '1', '0' },
        };

        Result result = shortestPath(grid);

        System.out.println("Distance = " + result.distance);
        System.out.print("Path = ");
        for (int i = 0; i < result.path.size(); i++) {
            int[] cell = result.path.get(i);
            System.out.print("(" + cell[0] + "," + cell[1] + ")");
            if (i + 1 < result.path.size()) {
                System.out.print(" -> ");
            }
        }
        System.out.println();
    }
}
Time & Space Complexity

I visit each cell at most once. Each cell checks only four neighbors. So the time is O(R*C). The visited grid, parent grid, and queue can all grow with the grid size, so the extra memory is O(R*C). The returned path can also be as long as O(R*C), but that is output memory.

Where it is used

This pattern is useful for shortest walks in mazes, map grids, puzzle games, robot movement on a board, and any grid where every move has the same cost.

Why Interviewers Ask This

The interviewer wants to see if you can model the grid as a graph, choose BFS for equal-cost moves, handle blocked cells, keep parent links for path recovery, and explain the cost clearly. They also check whether you mark visited at the right time and whether your Java code matches your explanation.

Common interview mistakes

A common mistake is to treat F as walkable. Another is to mark a cell visited only when it is removed from the queue. That can add the same cell many times. Another mistake is to forget the parent links, so the path cannot be rebuilt. Some candidates also return the path in reverse order or forget that distance equals path size minus 1.

Interview tip

When you explain the solution, say this clearly: "BFS grows in layers, so the first time I pop B, I stop." That makes the shortest-path idea easy to trust.

Interviewer may ask next
What changes if I need all shortest paths instead of just one?

I would keep BFS for the distance, but I would store every parent that gives the same shortest distance. Then I would backtrack through that parent graph to build all shortest paths. The result is still correct because BFS still finds the minimum distance first. The tradeoff is more memory and more work when many shortest paths exist.

What changes if diagonal moves are also allowed?

I would add the four diagonal directions to the direction arrays. The BFS logic stays the same because every move still has the same cost. The time and extra space stay O(R*C). If diagonal moves had a different cost, then I would need a different algorithm such as Dijkstra.

13. Find the shortest byte sequence that does not appear in the input.CodingHardGoogle

Question Details

Given a byte array using only a-f for the small testcase, find the shortest byte sequence that does not appear in the input. The source also includes much larger byte constraints and memory limits.

Short Interview Answer (30-60 seconds)

I build a trie from all suffixes, because every contiguous byte sequence is a prefix of some suffix. Then I run BFS from the root and check child bytes from 0 to 255 in order. The first missing child gives the shortest absent sequence, and the byte order makes it lexicographically smallest among ties. This matches the diagram’s search order. The diagram’s complexity is O(N^2 - L + 257·M) time and O(256·M) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The question asks for the shortest byte sequence that does not appear inside the input as a continuous block. If two answers have the same shortest length, we return the one with the smallest byte order. The diagram solves this by putting every suffix into a trie, then exploring the trie level by level. That makes shorter sequences come first. It also checks bytes from low to high, so the first missing child is the right answer. In the example [61, 62, 61, 63], the answer is [64].

Useful Questions to Ask the Interviewer
  1. Do you want the answer as raw bytes, hex, or text for display?
  2. Can the input be empty, and should I return the smallest one-byte sequence then?
Find the shortest byte sequence that does not appear in the input. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a byte array. The output is the shortest byte sequence that does not show up as a contiguous subsequence in that array. If more than one answer has the same shortest length, we choose the lexicographically smallest one. In the diagram’s example, the input is [61, 62, 61, 63], which is a, b, a, c. The output is [64], which is d.

2. Choose the algorithm and data structure

I use a trie. A trie is a tree where each edge stores one byte value. Every contiguous subsequence is a prefix of some suffix, so inserting all suffixes puts every present sequence into the trie. Then I use BFS. BFS is good here because it checks shorter sequences before longer ones. The central invariant is simple: the queue only holds prefixes that already exist in the input.

3. Initialize the state

I start with one root trie node. Each node has 256 child slots, one for each possible byte value. I also start a queue with the root node and an empty path. The root means the empty prefix. That is the correct start point, because every longer sequence must begin there. For the example, the trie will contain the paths for a, b, c, aa, ab, ba, ac, aba, bac, and acab.

4. Walk through the example

The example input is [61, 62, 61, 63]. The present one-byte values are a, b, and c. When BFS looks at the root, it checks byte 0 first, then 1, and so on. In the diagram’s simplified example, the first missing child at the root is byte 64, which is d. That means we can stop immediately. We do not need to explore deeper levels, because any deeper answer would be longer than 1.

5. Explain why the result is correct

The invariant is that every node in the trie represents a byte sequence that really appears in the input. BFS visits sequences in increasing length order. So the first missing child we see must be the shortest missing sequence. Because we test children from 0 to 255 in order, the first missing one at that shortest length is also lexicographically smallest. That is why the returned byte sequence is correct.

6. Explain the Java implementation

The Java code has three parts. First, it inserts every suffix of the byte array into the trie. That makes every present contiguous subsequence reachable as a prefix. Second, it runs BFS with a queue of node-path pairs. Third, for each node, it checks child bytes from 0 to 255 in order. If a child is missing, it copies the current path, adds that byte, and returns the result right away. The fallback return is only defensive because the problem guarantees an answer.

7. Explain complexity and edge cases

The diagram shows time as O(N^2 - L + 257·M), where N is the array length, L is the average processed depth, and M is the number of trie nodes. It also shows a worst case of O(N^3). Auxiliary space is O(256·M) bytes, because each trie node keeps 256 child pointers. Important edge cases are an empty array, which returns [0], and very large inputs, where the iterative trie and BFS help control memory.

Key Insight / Why This Solution Works

The key idea is that every contiguous byte sequence is a prefix of some suffix. So I insert every suffix into a trie, which means the trie stores every sequence that appears in the input. Then I do BFS from the root. BFS checks shorter sequences first. I also test child bytes from 0 to 255 in order. The first missing child at the shallowest level is the shortest missing sequence, and the byte order makes it lexicographically smallest. The queue only contains prefixes that already exist.

Code
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Main {

    private static final class Node {

        // One slot for each possible byte value 0..255.
        Node[] next = new Node[256];
        // Present in the diagram. It marks that some suffix ends here.
        boolean end;
    }

    private static final class NodeDepth {

        final Node node;
        final List<Byte> path;

        NodeDepth(Node node, List<Byte> path) {
            this.node = node;
            this.path = path;
        }
    }

    public static byte[] shortestMissing(byte[] arr) {
        Node root = new Node();
        int n = arr.length;

        // Build a trie from all suffixes.
        // Every contiguous subsequence is a prefix of one of these suffixes.
        for (int i = 0; i < n; i++) {
            Node node = root;
            for (int j = i; j < n; j++) {
                int b = arr[j] & 0xFF; // Convert signed byte to 0..255.
                if (node.next[b] == null) {
                    node.next[b] = new Node();
                }
                node = node.next[b];
                node.end = true;
            }
        }

        // BFS from the root. Shorter paths are tested first.
        Deque<NodeDepth> queue = new ArrayDeque<>();
        queue.addLast(new NodeDepth(root, new ArrayList<>()));

        while (!queue.isEmpty()) {
            NodeDepth current = queue.removeFirst();
            Node node = current.node;
            List<Byte> path = current.path;

            // Try child bytes in ascending order.
            for (int b = 0; b < 256; b++) {
                if (node.next[b] == null) {
                    // The first missing child at the shallowest level is the answer.
                    List<Byte> answerPath = new ArrayList<>(path);
                    answerPath.add((byte) b);

                    byte[] answer = new byte[answerPath.size()];
                    for (int i = 0; i < answerPath.size(); i++) {
                        answer[i] = answerPath.get(i);
                    }
                    return answer;
                }

                // This child exists, so the path stays valid.
                List<Byte> nextPath = new ArrayList<>(path);
                nextPath.add((byte) b);
                queue.addLast(new NodeDepth(node.next[b], nextPath));
            }
        }

        // Defensive fallback; the stated problem guarantees a missing sequence.
        return new byte[0];
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (int i = 0; i < bytes.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(String.format("%02X", bytes[i] & 0xFF));
        }
        sb.append(']');
        return sb.toString();
    }

    public static void main(String[] args) {
        // Example from the diagram: [61, 62, 61, 63] = a, b, a, c
        byte[] input = new byte[] { (byte) 0x61, (byte) 0x62, (byte) 0x61, (byte) 0x63 };
        byte[] output = shortestMissing(input);

        System.out.println("Input: [61, 62, 61, 63]");
        System.out.println("Output: " + toHex(output)); // Expected: [64]
    }
}
Time & Space Complexity

The diagram’s time formula is O(N^2 - L + 257·M), where N is the array length, L is the average processed depth, and M is the number of trie nodes. In simple words, we may insert many suffixes, and each trie node has 256 child slots. The diagram also shows a worst case of O(N^3). Auxiliary space is O(256·M) bytes. That space comes from the 256 child pointers stored in each trie node.

Where it is used

This pattern is useful when you need to find a missing pattern inside a large byte stream or text. It fits data validation, protocol checks, fuzzing, and test generation. It is also useful when the answer must be the shortest missing sequence, not just any missing sequence.

Why Interviewers Ask This

The interviewer wants to see whether I can recognize the right pattern, build the right data structure, and keep the order rules correct. They also want to see that I can explain why BFS gives the shortest answer, why byte order gives the lexicographically smallest answer among ties, and how to write the Java code without mixing up signed bytes and unsigned byte values. The question also checks memory awareness, because the trie can grow large.

Common interview mistakes

A common mistake is to treat the problem like a subsequence problem. The diagram is about contiguous subsequences, so the trie must be built from suffixes. Another mistake is to check bytes in the wrong order and lose lexicographic order. A third mistake is to keep searching after the first missing child is found, even though BFS already gave the shortest answer. Another easy mistake is to confuse signed byte values with 0 to 255 values in Java.

Interview tip

Say the invariant out loud: the queue only contains prefixes that already exist, so the first missing child I see is the shortest missing sequence.

Interviewer may ask next
What changes if the byte alphabet is larger or sparse, so a fixed 256-slot child array is too expensive?

I would replace the fixed child array with a map or a compressed trie. The idea stays the same: store every present prefix and use BFS to find the first missing child in order. Correctness is preserved because the traversal rule does not change. The tradeoff is slower lookup and more code, but the memory use can drop a lot when the alphabet is sparse.

How would you reduce memory if the input is very large?

I would use a compressed trie or another suffix structure instead of storing many full 256-slot nodes. That keeps the same search idea, but it changes how children are stored and how byte order is walked. The tradeoff is lower memory versus more complex code and possibly more expensive lookups. The shortest-missing-sequence logic still stays correct.

14. Simplify a formula with nested parentheses.CodingHardGoogle

Question Details

Given a formula of letters and parentheses, return a simplified equivalent version without parentheses, including nested parentheses.

Short Interview Answer (30-60 seconds)

I would scan the formula from left to right and keep a stack of cumulative sign contexts. The stack starts with +1. When I enter parentheses, I multiply the current context by the operator before the group and push the result. For each letter, I multiply the stack top by the current operator to get its final sign. When I leave a group, I pop the stack. This takes O(n) time and O(d) auxiliary space, where d is the nesting depth.

Detailed Explanation

See the Code while reading this explanation.

The input is a formula made from letters, plus signs, minus signs, and parentheses. The goal is to remove every parenthesis without changing the meaning of the formula. A minus before a group can reverse the signs inside that group. Nested groups can reverse them again. I keep the current sign effect for each open group in a stack. This lets me process the formula from left to right and decide the final sign of each letter without repeatedly rewriting parts of the expression.

Useful Questions to Ask the Interviewer
  1. Can I assume the parentheses are balanced and the formula is valid?
  2. Can I assume each term is a single letter, as shown in the problem and diagram?
  3. Should the output keep the letters in their original order and only remove parentheses by adjusting signs?
Simplify a formula with nested parentheses. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a formula such as a-(b-c-(d+e))-f. I need to return an equivalent formula with no parentheses. For this example, the correct result is a-b+c+d+e-f. The letters stay in the same order. Only their effective signs change when required by the surrounding parentheses.

2. Choose the stack-based sign method

I use a stack of integers. Each value is either +1 or -1. The top value represents the cumulative sign context for the current parenthesis level. +1 means a local sign is kept. -1 means it is flipped. This handles nested groups because entering a new group combines the inherited sign with the operator directly before that group.

The central invariant is: signStack.peek() always equals the cumulative sign multiplier for the current nesting level.

3. Initialize the state

I start with signStack = [1] because the top level is positive. I set currentOperator = +1. I also create an empty result string. Traversal begins at index 0.

4. Walk through the exact example

The input is a-(b-c-(d+e))-f.

At index 0, the character is a. The stack top is +1 and currentOperator is +1. The effective sign is 1 × 1 = +1, so I append a. The result is a.

At index 1, the character is -. I set currentOperator = -1.

At index 2, the character is (. The stack is [1] and the current operator is -1. I push 1 × (-1) = -1. The stack becomes [1, -1]. Then I reset currentOperator to +1.

At index 3, the character is b. The effective sign is -1 × 1 = -1, so I append -b. The result becomes a-b.

At index 4, the character is -. I set currentOperator = -1.

At index 5, the character is c. The stack top is -1 and the local operator is -1. The effective sign is -1 × (-1) = +1, so I append +c. The result becomes a-b+c.

At index 6, the character is -. I set currentOperator = -1.

At index 7, the character is (. The active stack top is -1. I push -1 × (-1) = +1. The stack becomes [1, -1, 1]. This is the double sign flip. I reset currentOperator to +1.

At index 8, the character is d. The effective sign is 1 × 1 = +1, so I append +d. The result is a-b+c+d.

At index 9, the character is +. I set currentOperator = +1.

At index 10, the character is e. The effective sign is 1 × 1 = +1, so I append +e. The result becomes a-b+c+d+e.

At index 11, the character is ). I pop the inner sign context. The stack returns to [1, -1].

At index 12, the character is ). I pop the outer sign context. The stack returns to [1].

At index 13, the character is -. I set currentOperator = -1.

At index 14, the character is f. The effective sign is 1 × (-1) = -1, so I append -f.

The final result is a-b+c+d+e-f.

5. Explain why the result is correct

The stack top always contains the combined sign effect of all surrounding parentheses. Every letter uses signStack.peek() × currentOperator. Therefore, each letter gets exactly the sign implied by the original expression. When a closing parenthesis appears, popping the stack restores the previous context before processing later characters.

6. Explain the Java implementation

The Java code uses ArrayDeque<Integer> as the stack. A plus or minus updates currentOperator. An opening parenthesis pushes a new cumulative context. A closing parenthesis pops the finished context. For a letter, the code calculates its effective sign, appends the correct sign when needed, appends the letter, and resets currentOperator to +1.

7. Explain complexity and edge cases

The algorithm processes each character once, so the time complexity is O(n). The stack contains one value per active nesting level, so auxiliary space is O(d), where d is the maximum parenthesis depth. In the worst case, d can be O(n). Relevant cases include a single letter such as a, a leading negative group such as -(a-b), nested flips such as a-(b-(c-d)), and a positive group such as a+(b-c).

Key Insight / Why This Solution Works

The key insight is to store the cumulative sign context instead of expanding parenthesized groups. The stack begins with +1. Its top value always represents the combined sign effect of all currently enclosing parentheses. When ( appears, push signStack.peek() × currentOperator. When ) appears, pop. For a letter, calculate effectiveSign = signStack.peek() × currentOperator. This invariant makes nested sign flips work naturally. For example, a negative group inside another negative context produces (-1) × (-1) = +1.

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

public class Main {

    public static String simplifyFormula(String formula) {
        // The stack top stores the cumulative sign context for the current nesting level.
        Deque<Integer> signStack = new ArrayDeque<>();
        signStack.push(1);

        // +1 means the next term keeps its local sign.
        // -1 means the next term or group has a local minus.
        int currentOperator = 1;

        // Build the simplified expression directly without parentheses.
        StringBuilder result = new StringBuilder();

        // Process every character from left to right.
        for (int i = 0; i < formula.length(); i++) {
            char ch = formula.charAt(i);

            if (ch == '+') {
                // The next term or group has a positive local operator.
                currentOperator = 1;
            } else if (ch == '-') {
                // The next term or group has a negative local operator.
                currentOperator = -1;
            } else if (ch == '(') {
                // Combine the inherited context with the operator before this group.
                // The pushed value becomes the cumulative context inside the group.
                signStack.push(signStack.peek() * currentOperator);

                // Inside the new group, start with a positive local operator.
                currentOperator = 1;
            } else if (ch == ')') {
                // Leave this group and restore the previous parenthesis context.
                signStack.pop();
            } else if (Character.isLetter(ch)) {
                // Combine the active parenthesis context with this term's local operator.
                int effectiveSign = signStack.peek() * currentOperator;

                // Terms after the first one need an explicit + or - separator.
                if (result.length() > 0) {
                    result.append(effectiveSign == 1 ? "+" : "-");
                } else if (effectiveSign == -1) {
                    // The first term only needs a sign when it is negative.
                    result.append('-');
                }

                // Append the letter after its final effective sign has been determined.
                result.append(ch);

                // Reset the local operator for the next term or group.
                currentOperator = 1;
            }
        }

        // All parentheses are removed and every term now has its final sign.
        return result.toString();
    }

    public static void main(String[] args) {
        // Run the exact example used in the approved diagram.
        String formula = "a-(b-c-(d+e))-f";
        String simplified = simplifyFormula(formula);

        // Expected output: a-b+c+d+e-f
        System.out.println(simplified);
    }
}
Time & Space Complexity

Let n be the number of characters in the formula and d be the maximum nesting depth. Time is O(n) because every character is processed once. Auxiliary space is O(d) because the stack stores one sign context for each active parenthesis level. If the formula can be nested almost n levels deep, the worst-case auxiliary space is O(n).

Where it is used

This stack pattern is useful when software processes nested structures where entering a group changes some context and leaving the group must restore the previous context. A simple example is normalizing symbolic expressions with nested sign changes. The same general idea also appears in parsers that keep state for nested scopes.

Why Interviewers Ask This

This problem tests whether you can recognize nested state and choose a stack to manage it. It checks whether you can maintain a precise invariant, combine local and inherited signs correctly, and restore previous state when a group ends. It also tests whether your Java implementation matches your explanation and whether you can give the correct O(n) time and O(d) auxiliary-space analysis.

Common interview mistakes
  1. Using only the local + or - and forgetting the inherited sign from outer parentheses.
  2. At (, pushing only currentOperator instead of signStack.peek() × currentOperator. That breaks nested sign flips.
  3. Forgetting to reset currentOperator to +1 after opening a group or after processing a letter.
  4. Forgetting to pop the stack at ), which leaves an old nesting context active.
  5. Adding a leading + before the first positive term instead of writing the first term directly.
Interview tip

State the invariant before coding: the stack top is the cumulative sign for the current nesting level. Then use the two pushes in a-(b-c-(d+e))-f to show the core idea: the outer group pushes -1, while the nested negative group pushes +1 because two negatives cancel.

Interviewer may ask next
What changes if the formula starts with a negative parenthesized group such as `-(a-b)`?

The algorithm does not change. The leading - sets currentOperator = -1. When ( is read, the code pushes 1 × (-1) = -1. Inside the group, a receives a negative effective sign and -b becomes positive, so the result is -a+b. The same cumulative-sign invariant proves correctness. Time remains O(n) and auxiliary space remains O(d).

Can the auxiliary space be reduced?

The stack is needed to restore earlier cumulative sign contexts when nested groups close. Its size is O(d), where d is the nesting depth. For arbitrary nesting, removing this stack would lose the information needed when ) is reached unless the input were modified or processed with another mechanism that stores equivalent state. Time remains O(n). The main tradeoff is that correct nested restoration requires memory proportional to the active nesting depth.

15. How would you design an event planner with venue voting and notifications?System DesignMediumGoogle

Question Details

Design an event planner where users create an event, add Google users, vote for a venue, and notify everyone once the event is decided.

Short Interview Answer (30-60 seconds)

At a high level, this system helps a group create an event, choose a venue together, and notify everyone after the choice is final. The main challenge is keeping votes and the final decision correct without making notification delivery block user requests. I would explain three flows: event setup, venue voting and finalization, and background notifications. PostgreSQL keeps the official event state, while separate Spring Boot Java services handle each flow. The trade-off is more service and messaging complexity.

Detailed Explanation

The system lets an organizer create an event, invite Google users, and offer possible venues. Invited users can vote and view results, while only the organizer can invite users or close voting. The difficult part is keeping each vote correct, saving one final venue, and notifying everyone afterward. The diagram separates normal user requests from background notification work. It also keeps the official event state in PostgreSQL, so the Java services have one clear source of truth.

Useful Questions to Ask the Interviewer
  1. Can voting close only when the organizer acts, or also when a deadline is reached?
  2. Should invited users see the current vote tally before voting closes?
  3. Which notification types are required: email, push, in-app, or all three?
How would you design an event planner with venue voting and notifications? diagram
How to Explain It in an Interview
1. Start with users, identity, and request checks

I would start with the Organizer and Invitees using the Web / Mobile App. The organizer creates events, invites users, and closes voting. Invitees propose or vote for venues and view results.

Requests go through the API Gateway. It handles authentication, authorization, request validation, and rate limiting. Google Identity provides OAuth 2.0 / OIDC login and returns an identity token.

2. Handle event setup and voting

For event setup, the API Gateway sends event and attendee requests to the Event Service. The service creates events, manages attendees, and stores event metadata in PostgreSQL.

Venue and vote requests go to the Voting Service. It stores venue options and votes in PostgreSQL and computes the current tally. A unique database constraint on (event_id, user_id) enforces one vote per user for each event.

These are Spring Boot services running on Java 21 or later. They are deployed as separate JVM services or replicas, so they do not share heap memory. This also allows Event, Voting, and Notification work to scale independently.

3. Save the final venue correctly

The Decision Service handles closing the vote. Voting can close when the organizer acts or when the deadline is reached.

The service reads the vote totals and saves the winning venue and final decision in PostgreSQL. PostgreSQL is the system of record, meaning it keeps the official users, events, attendees, venue options, votes, and final decision. The final decision is stored before notification work is published.

4. Send notifications in the background

After the final decision is saved, the Decision Service publishes an EventDecided event to the Message Broker / Event Queue. This lets notification work happen in the background instead of delaying the main user flow.

The Notification Service consumes the event using the asynchronous retry path shown in the diagram. It sends the final event notification through the Generic Email / Push / In-App Notification component. That component delivers the event-decided notice to users. Notification processing is idempotent, which means handling the same event again should not create an incorrect duplicate effect.

5. Explain operations and the main trade-off

The Java services send logs, metrics, and traces to Observability. These signals help the team find failures and understand slow requests.

The service split lets different paths scale independently. The downside is more moving parts. The team must operate several JVM services, PostgreSQL, the message broker, and the notification path. Notifications can also arrive after the database update because they run asynchronously.

Engineering Considerations / Design Trade-offs

The benefit is that PostgreSQL keeps one clear final answer for events, votes, and the winning venue. Separate Event, Voting, Decision, and Notification services can scale independently. Background notifications also keep email, push, or in-app delivery away from the main user request. The downside is more moving parts. The team must operate several Java services and a Message Broker / Event Queue. Notifications may arrive a little later because they run in the background. Retry handling also matters, so the Notification Service must safely handle the same EventDecided event more than once.

Why Interviewers Ask This

Interviewers use this question to see whether you can break a real product into clear flows. They want to see how you protect voting correctness, choose a source of truth, separate user requests from background work, and define useful service boundaries. They also want to know whether you understand authorization, retries, separate JVM services, independent scaling, and the operational cost of adding more components.

Interviewer may ask next
What would you do if the notification provider is unavailable for several minutes?

I would keep the same basic design and let the background notification path absorb the failure. The Decision Service would still save the winning venue and final event state in PostgreSQL first. That keeps the event correct even when email, push, or in-app delivery is temporarily unavailable.

The EventDecided event would still go through the Message Broker / Event Queue. The Notification Service would consume it through the existing asynchronous retry path. Because notification processing is idempotent, retrying the same event should not create an incorrect duplicate effect.

The main voting and finalization flow would not need to wait for the external notification path to recover. Logs, metrics, and traces in Observability would help the team see failed sends and retry activity.

The downside is delayed delivery. Users may see the final result in the application before their email, push, or in-app notice arrives.

What would you change if venue voting becomes much busier than event creation?

I would keep the same architecture and scale the Voting Service separately from the Event Service. The diagram already separates these Java services, so more Voting Service JVM replicas can handle extra venue and vote requests without increasing Event Service capacity at the same rate.

PostgreSQL would still store the official venue options, votes, and final event state. The unique (event_id, user_id) constraint would continue protecting the one-vote-per-user rule even when several Voting Service replicas handle requests at the same time.

The API Gateway would continue sending venue and vote requests to the Voting Service. The Decision Service would still read the vote totals and save the final venue before publishing EventDecided to the Message Broker / Event Queue.

The downside is more pressure on PostgreSQL. Adding application replicas increases service capacity, but all vote writes still reach the same system-of-record database shown in the design.

16. How would you monitor overloaded machines and redistribute processes?System DesignHardGoogle

Question Details

Suppose you have N machines running M processes and need to monitor health and redistribute processes when a machine is overloaded; explain what parameters you would evaluate and the design approach.

Short Interview Answer (30-60 seconds)

At a high level, the goal is to keep machines healthy while processes run across the fleet. The main challenge is detecting real overload without moving work too often. I would explain three flows: collect health metrics, decide when and where to rebalance, then move the process safely. Host Agent JVMs report metrics to a Java 21/25 Monitoring & Rebalancing Service. It drains the source, starts the process on an eligible machine, and updates ownership. The trade-off is slower reaction in exchange for avoiding unstable rebalancing.

Detailed Explanation

The system must watch many machines and notice when one is carrying too much work. It then needs to move some processes away without making the fleet less stable. The difficult part is deciding whether an overload is temporary or real, choosing a machine with enough spare capacity, and moving the process safely. The diagram solves this with Host Agent JVMs, Local Process Managers, and a Java 21/25 Monitoring & Rebalancing Service. The flow is to collect health data, detect overload, choose a target, move the process, update ownership, and continue monitoring.

Useful Questions to Ask the Interviewer
  1. Are the processes stateless, stateful, or a mix of both?
  2. How quickly should the system react to overload?
  3. Can a process be stopped and restarted, or must its state be preserved?
  4. What should happen when every machine is already near capacity?
How would you monitor overloaded machines and redistribute processes? diagram
How to Explain It in an Interview
1. Collect machine and process health

I would start by measuring both machine health and process health. Each machine has a Host Agent JVM that sends heartbeat and metric reports to the Monitoring & Rebalancing Service.

The Metrics Aggregator collects these reports. The important signals are CPU, memory, load average, disk I/O, network I/O, process liveness, error rate, and queue or latency backlog. The service also stores metrics in the Metrics Store / Time-Series DB.

2. Detect a real overload

Next, the Overload Detector decides whether Machine A is truly overloaded. I would not move a process because of one short CPU spike.

The detector uses a moving window, thresholds, and hysteresis. Hysteresis means the system does not use exactly the same boundary for entering and leaving an overloaded state. This reduces repeated moves caused by small metric changes.

3. Choose a process and an eligible target

Once overload is confirmed, the Placement / Rebalancer chooses a movable process, such as P1. It then looks for the least-loaded eligible machine with enough spare capacity.

The choice also respects anti-affinity. This means workloads that should remain separated are not placed together. Spare capacity must exist before work is moved. If no suitable machine exists, the system can shed load, queue work, and alert the SRE / Operator.

4. Move the process safely

The Placement / Rebalancer tells Machine A's Local Process Manager to drain and stop P1. A stateless process can then be restarted on Machine B or Machine C.

A stateful process needs an extra safety step. Its state must be checkpointed or handed off so the replacement process can recover it before normal work continues. This is a controlled stop-and-restart flow, not live movement of JVM threads or heap memory.

5. Update ownership and keep monitoring

After the target starts the replacement process, the Process Registry updates its location and ownership. The Monitoring & Rebalancing Service records the rebalance in the Event / Audit Log, and health monitoring continues.

A cool-down period waits before another rebalance decision. This gives the system time to stabilize and helps avoid oscillation. The SRE / Operator uses the Control Plane API to view status or set policy. Authentication and authorization protect these admin actions. The Monitoring & Rebalancing Service itself runs as stateless replicas, so ownership information is kept in the Process Registry rather than depending on one service replica's local memory.

Engineering Considerations / Design Trade-offs

The benefit is that the system can react automatically when one machine becomes overloaded. Looking at several health signals gives a better picture than checking CPU alone. Moving windows, hysteresis, and a cool-down period also reduce unnecessary process moves. The downside is that these protections make the system react a little more slowly. Stateful processes are harder to move because their state must be checkpointed or handed off before restart. Another limit appears when the whole fleet is full. Rebalancing cannot create new capacity, so the system must queue work, shed some load, or alert the operator.

Why Interviewers Ask This

Interviewers use this question to see whether you can design a stable control loop instead of reacting to one metric spike. They want to understand how you choose useful health signals, detect overload, select safe target machines, and handle stateless and stateful processes differently. They also test whether you recognize the limits of rebalancing, especially when the fleet has no spare capacity.

Interviewer may ask next
How would your design change if most processes were stateful and could not simply restart on another machine?

I would keep the same monitoring, overload detection, and target-selection flow. Host Agent JVMs would still report health to the Monitoring & Rebalancing Service, and the Placement / Rebalancer would still choose an eligible machine with spare capacity.

The main change would be the move step. Before Machine A's Local Process Manager stops P1, the process must checkpoint or hand off the state that the replacement needs. The target process should start only when that state can be recovered safely.

The Process Registry should update the process location and ownership after the replacement is ready to take over. The Event / Audit Log should also record the rebalance as in the original design.

The downside is that stateful moves are slower and have more failure points. Some stateful processes may also be poor candidates for automatic movement, which gives the rebalancer fewer safe choices during overload.

What would you do if Machine A is overloaded but Machine B and Machine C also have no spare capacity?

I would not force a process move because sending work to another full machine can make the fleet worse. The Overload Detector can still identify Machine A as overloaded, but the Placement / Rebalancer must verify spare capacity before choosing a target.

If no eligible target exists, I would use the fallback shown in the diagram. The system can queue work when waiting is acceptable. It can shed load when protecting the machines is more important than accepting every unit of work. It should also alert the SRE / Operator.

The existing monitoring loop continues, so fresh heartbeats and metrics may later show that capacity is available. The cool-down behavior also prevents repeated rebalance attempts while the fleet remains full.

The downside is that rebalancing cannot solve a fleet-wide capacity shortage. It can only redistribute capacity that already exists.

17. How would you design an email system like Gmail?System DesignMediumGoogle

Question Details

Design an email system that supports sending and receiving email and attachments at the stated scale, and explain the main architecture and tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, this is a mail system that must move messages safely and still feel fast to users. Sending mail must be correct, receiving mail must be protected, and reading or searching mail must stay quick. I would explain it in three parts: secure entry, the main send and read paths, and the background work for search, notifications, and cleanup. The main trade-off is that some views can lag a little, while the core mailbox data stays safe and responsive.

Detailed Explanation

The goal is to build a mail system that lets people send and receive email, open messages quickly, and keep attachments safe. The hard part is that some actions must finish right away, while other work can happen later. Search updates, notifications, and cleanup do not need to block the user. The diagram shows a secure front door, Java services, durable mail storage, object storage for files, and background workers. I would explain the system in the same order: secure entry, mail flows, then background work and trade-offs.

Useful Questions to Ask the Interviewer
  1. Do we need instant search for brand-new mail, or is a small delay okay?
  2. How large can attachments get, and how often do users send them?
  3. Is web and mobile traffic much larger than SMTP traffic from other servers?
How would you design an email system like Gmail? diagram
How to Explain It in an Interview
1. Explain the goal and the main idea

At a high level, this system is about safe mail delivery and fast mailbox access. The main challenge is that the user-facing path must feel quick, but the background work can be slower. The design separates those two needs. That lets the system stay responsive without losing mailbox data.

2. Explain secure entry and request control

A request starts at Clients, then moves through Edge & Security and API Gateway. This layer handles HTTPS, DNS, CDN / Anycast, WAF / DDoS protection, rate limits, request routing, and validation. AuthN / AuthZ checks login, tokens, and access rights before the request reaches the Java services. That keeps bad traffic out early.

3. Explain the Java service layer

The Java Services box holds Mail Service, User Service, Search Service, Attachment Service, and Notification Service. Mail Service handles compose, send, receive, threading, labels, folders, and search indexing. User Service keeps profile, preferences, contacts, and settings. Search Service handles full-text search, autocomplete, and faceting. Attachment Service handles upload, virus scan, metadata, and thumbnails. Notification Service can send email, push, and optional SMS.

4. Explain storage, cache, and messaging

The main data lives in the Data Stores box. User / Metadata DB stores users, preferences, contacts, labels, folders, and ACLs. Mail Store keeps raw email and message metadata. Object Storage keeps attachments. Search Index supports fast search, and Cache keeps hot sessions, tokens, metadata, and rate limits. The cache is a speed layer, not the main source of truth. Apache Kafka carries incoming mail events, outgoing mail events, indexing events, notification events, and DLQ messages.

5. Explain background work and trade-offs

The Java Runtime & Deployment box shows stateless JVMs for the web tier and separate JVMs for background workers. Spring Boot apps use Java 21 / 25, and virtual threads help handle many blocking requests. Thread-safe caches and connection pools help each JVM reuse resources well. Background workers handle send mail, indexing, attachment processing, cleanup, and retention. SMTP Ingress handles mail from other providers, and SMTP Egress sends mail out. Observability is also part of the design, with logs, metrics, tracing, dashboards, and alerts. The trade-off is simple: we keep the inbox fast and safe, but some search and update views may lag a little.

Engineering Considerations / Design Trade-offs

The benefit is that the main mailbox data stays safe while the user-facing paths stay fast. The system can scale the stateless Java services separately from the storage layer. Attachments move to object storage, so large files do not slow down normal mail reads. Background workers handle search indexing, notifications, and cleanup without blocking the user. The downside is more moving parts and a little delay in some views, like search or message counts. We accept that because it keeps the system usable under load and helps the mail service recover better from bursts of traffic.

Why Interviewers Ask This

Interviewers want to see if you can break a large product into clear flows. They also want to see whether you know where the main data lives, how cache helps, and why some work should move to the background. This question checks judgment about security, scaling, failures, and trade-offs, not just memory of common email terms, so they can see how you think under pressure.

Interviewer may ask next
How would you change the design if search must show new mail almost right away?

I would keep the same basic design, but I would make the indexing path much faster. The Mail Store would still save the email first, because that is the safest step. Then the Indexing Worker would run sooner, and the Search Service would read from a fresher Search Index. If needed, I would also let the inbox page show new mail directly from the Mail Store while the index catches up. That keeps the system correct and still gives a fast search experience. I would keep the cache short-lived for fresh mail so old results disappear sooner. The downside is more load on the main store and more work for the background workers, so the system gets a little more expensive to run.

What would you change if attachments became much larger and more common?

I would keep the same flow, but I would push even more work into Object Storage and the Attachment Service. The Mail Store would keep only the message and a pointer to the file, not the large file itself. The upload path would still validate and scan the attachment first, then save it in durable storage. That keeps the main mail data smaller and cheaper to serve. I would also stream downloads instead of loading the whole file into memory, and I would set clear size limits and retention rules for old files. I would keep metadata in the same mail path so the inbox can still show attachment names and status quickly. The downside is that attachment access depends more on object storage and on signed access rules, so the download path becomes a little more complex.

18. How would you design a URL phishing verifier?System DesignMediumGoogle

Question Details

Design a URL phishing verifier and explain how you would approach the detection pipeline and major tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, the system takes a URL and decides whether it is Safe, Suspicious, Malicious, or Unknown. The main challenge is giving fast answers without losing useful phishing signals. I would split the design into a cache-first verification path, a deeper detection path for cache misses, and a background feedback path. Java JVM replicas run independent checks in parallel, combine rules and ML scores, and return the verdict. The main trade-off is latency versus detection accuracy.

Detailed Explanation

The goal is to check a URL before a user trusts or opens it. Some URLs can be classified quickly because the system has seen them before. New or uncertain URLs need more work, such as checking redirects, domain information, network reputation, and page content. Those checks can be slow or unavailable. The design therefore uses a fast reputation path first, a deeper detection pipeline when needed, and background feedback that improves future decisions without delaying the current request.

Useful Questions to Ask the Interviewer
  1. How quickly should a normal verification request return?
  2. Is Unknown acceptable when important external signals time out?
  3. Should sandboxed page fetching always run, or only for uncertain URLs?
  4. How much manual analyst review should the product support?
How would you design a URL phishing verifier? diagram
How to Explain It in an Interview
1. Start with the request and protect the entry point

I would start by protecting expensive verification work. A Browser Extension, Email Gateway, or Mobile / Web Application sends an HTTPS verify request to the Entry Layer.

The HTTPS API / Load Balancer accepts the request. Authentication checks the API key or OAuth identity. Request Validation checks the URL format and size. Rate Limiting controls how much work one IP address or key can create.

The request then enters the URL Verification API running as JVM replicas.

2. Normalize the URL and try the fast path

The first step is URL Normalization & Canonicalization. The service trims and decodes the URL, lowercases the host, removes fragments, handles default ports, and sorts parameters.

It then performs a Reputation Cache Lookup. The in-memory cache can contain recent results for an exact URL, canonical URL, FQDN or eTLD+1, and IP address. Its entries are TTL based, so they expire after a configured time.

On a cache hit, the known reputation can go directly to Verdict Decision. On a cache miss, the request moves to the Detection Orchestrator.

3. Run deeper checks in parallel

The Detection Orchestrator builds the verification plan, sets a deadline and budget, chooses signals, fans out independent checks, and aggregates their results.

Verification workers use Java virtual threads for concurrent blocking network checks. Virtual threads are lightweight JVM-scheduled threads that work well when many tasks wait for network I/O. They do not remove the need for deadlines or resource limits.

The Detection Pipeline checks lexical URL structure, Punycode and homograph tricks, redirect chains, domain, DNS, TLS, and reputation signals. An optional sandbox can fetch page content. That sandbox is isolated and uses restricted network access to reduce SSRF and active-content risk.

4. Score the signals and produce the verdict

The Rules Engine + ML Scorer combines heuristic rules, risk signals, the model score, and extracted features. Verdict Decision then produces Safe, Suspicious, Malicious, or Unknown.

The verdict also carries a confidence score, reason codes, matched signals, and a timestamp. The URL Verification API returns that result through the Entry Layer as the JSON verdict.

The Reputation / Verdict Store is the durable source of truth for verdicts and metadata. The Feature / Score Results Store keeps raw signals, page snapshots, and extracted features. Recent verdict information can also be available through the Reputation Cache for faster later checks.

5. Improve the system without blocking the request

Verdict events, feedback events, and update events can move through the Message Queue / Event Bus. This work stays outside the main synchronous response path.

Users can report phishing through User Reports. Analysts can inspect URLs, confirm or override verdicts, and add reason codes in the Analyst Console. The Model & Rule Update Service uses feedback to retrain or recalibrate models, update rules and thresholds, and generate new signals.

Observability collects metrics, structured logs, traces, dashboards, and alerts. The important trade-off is latency versus accuracy. Cheap checks run first. Deeper sandbox work runs only when useful and within the deadline. If external signals time out, the service can return a partial-score verdict or Unknown instead of waiting indefinitely.

Engineering Considerations / Design Trade-offs

The benefit is that known URLs can return quickly from the Reputation Cache. The downside is that cached information may be older than the durable verdict data until updates reach it. Parallel checks reduce waiting time, but DNS, reputation, redirect, and page requests can still time out. Sandboxed page fetching gives stronger evidence, but it costs more time and resources. Strict thresholds catch more attacks but can increase false positives. Loose thresholds reduce false positives but can miss phishing. The design balances these problems with deadlines, manual review, background updates, and an Unknown or partial-score result when evidence is incomplete.

Why Interviewers Ask This

Interviewers want to see how you break a security problem into a fast path, deeper checks, and background work. They also want to see whether you understand caching, parallel Java work, durable data, failure handling, and safe page fetching. The important skill is judgment. A strong candidate explains why some checks are fast, why others are expensive, and how the design balances speed with detection quality.

Interviewer may ask next
What would you change if external DNS and reputation checks often take too long?

I would keep the same architecture, but I would enforce tighter deadlines inside the Detection Orchestrator. Each external check would receive part of the request budget and stop waiting when that time is used.

The other independent checks would continue in parallel. The Rules Engine + ML Scorer could then work with the signals that arrived before the deadline. If the available evidence is strong, Verdict Decision can still return Safe, Suspicious, or Malicious. If too much information is missing, it should return Unknown or a partial-score verdict instead of waiting indefinitely.

I would also use recent Reputation Cache results when they are available. Observability would track timeout rates, errors, and latency so operators can see which external signal is causing problems.

The downside is lower confidence when useful external evidence is missing.

How would you reduce false positives without making phishing detection too weak?

I would keep the same detection pipeline and tune the Rules Engine + ML Scorer using the existing feedback loop. One weak signal should not automatically make a URL Malicious. The final decision should combine several signals and use the confidence score, reason codes, and matched signals already shown in the design.

User Reports and the Analyst Console provide useful examples of mistakes. Analysts can confirm or override verdicts and add reason codes. The Model & Rule Update Service can use that feedback to recalibrate the model and adjust rules or thresholds.

I would also keep Suspicious and Unknown as real outcomes. They avoid forcing every uncertain URL into Safe or Malicious. The optional content sandbox can provide more evidence for difficult cases when the request budget allows it.

The downside is extra operational work for threshold tuning, model updates, and manual review.

19. How would you design a task scheduler?System DesignMediumGoogle

Question Details

Design a task scheduler that handles recurring class-design style requirements and explain the scheduling approach, state, and tradeoffs.

Short Interview Answer (30-60 seconds)

At a high level, this scheduler stores when tasks should run and sends due work to separate workers. The main challenge is supporting recurring schedules across several replicas without losing work or firing the same occurrence unnecessarily. I would explain three flows: creating schedules, finding and dispatching due tasks, and executing them through workers. Durable state, leases, a persistent Ready Queue, retries, and idempotency support at-least-once execution. The trade-off is extra coordination and possible duplicate delivery.

Detailed Explanation

The system must let users create one-time or recurring tasks and run them close to the requested time. The difficult part is keeping schedules safe when services restart while several scheduler and worker replicas run at once. We also need clear rules for retries, missed runs, time zones, and duplicate delivery. The diagram organizes the solution into schedule creation, due-task scanning, dispatch, worker execution, result recording, retry handling, and observability. Durable database state keeps the schedule safe, while the Ready Queue separates scheduling work from actual task execution.

Useful Questions to Ask the Interviewer
  1. Do we need one-time tasks, recurring tasks, or both?
  2. How much delay from the planned run time is acceptable?
  3. What should happen when a recurring run is missed?
  4. How many retries should a failed execution receive?
  5. Can task handlers safely receive the same logical run more than once?
How would you design a task scheduler? diagram
How to Explain It in an Interview
1. Create and store the schedule

For the create path, the Client or Admin UI sends an HTTPS schedule request to the API Gateway. The gateway handles authentication, authorization, validation, and rate limits. It then forwards the validated request to the Scheduler Service Cluster.

The Schedule API stores the definition in the Durable Schedule Store, which is the source of truth. The stored state includes the schedule expression, timezone, next_run_at, enabled flag, payload reference, attempts, lease or version data, and misfire policy. Keeping this state outside JVM memory means schedules survive scheduler restarts.

2. Calculate the next run

The Trigger Parser understands cron, interval, and one-time schedules. The Next-Run Calculator turns that trigger into a next_run_at value. The diagram stores the next run in UTC while keeping the IANA timezone needed for later calculations.

For recurring work, the scheduler computes and stores the following next_run_at after the current occurrence is leased or dispatched. This lets the schedule continue without depending on one scheduler process staying alive.

3. Claim due work and dispatch it

The Due Scheduler or Lease Manager periodically scans for schedules whose next_run_at is due. Scheduler replicas run in separate Java 21 or Java 25 JVM processes, so their heaps are not shared.

The design uses shard ownership and an atomic lease to reduce duplicate firing. A lease is a temporary claim on one scheduled occurrence. After a due occurrence is claimed, the Dispatcher publishes an execution event to the persistent Ready Queue. The queue buffers spikes and keeps slow execution from blocking schedule scanning.

4. Execute the scheduled job

Worker Cluster replicas consume execution events from the Ready Queue. Each worker performs an Idempotency Key Check before the Task Executor runs the job. Idempotency means a repeated delivery should not repeat the business effect when the task handler supports the same key.

Workers use bounded concurrency and backpressure, so they do not start unlimited work. Virtual threads help with many blocking tasks, but they do not replace those limits. The Task Executor calls the configured webhook, database, internal service, or internal job. The Result Writer records status, timings, attempts, codes, and related data in the Execution History Store.

5. Handle retries, terminal failures, and operations

The Retry Policy uses exponential backoff and a maximum attempt count. A retriable run can return to the Ready Queue for another attempt. The Execution History Store records states such as SUCCESS, FAILED, RETRYING, and SKIPPED, giving operators an audit trail of each run.

After the maximum retries, the run becomes a terminal failure. The diagram also treats terminal failures as an alerting condition. Observability collects logs, metrics, traces, queue lag, success and failure rates, and alerts. The main trade-off is at-least-once execution. Duplicate delivery can still happen, so leases protect scheduling and idempotency protects task execution.

Engineering Considerations / Design Trade-offs

The benefit is that important schedule state stays in the Durable Schedule Store, so schedules survive JVM or service restarts. The Ready Queue keeps slow task execution away from schedule scanning and also buffers spikes. Sharding and leases let several scheduler replicas share the work while reducing duplicate firing. The downside is more coordination and more moving parts. At-least-once execution may deliver the same logical run again, so task handlers need idempotency protection. Retries can also delay completion. Time zones, daylight-saving changes, clock differences, and missed runs need clear rules. Virtual threads help with blocking work, but bounded concurrency and backpressure are still required.

Why Interviewers Ask This

Interviewers use this question to see whether you can separate scheduling from execution and keep important state durable. They also want to see how you coordinate several replicas, handle recurring schedules, retries, duplicate delivery, and worker pressure. A strong answer shows practical judgment about queues, leases, idempotency, time handling, failure recovery, observability, and trade-offs instead of claiming perfect exactly-once execution.

Interviewer may ask next
What would you change if the scheduler had to support a much larger number of recurring tasks across many scheduler replicas?

I would keep the same basic design, but shard ownership would become more important. The Durable Schedule Store would still keep each schedule and its next_run_at value. Scheduler replicas would divide the due-task space into shards so every replica does not scan every schedule.

Each Due Scheduler or Lease Manager would normally scan only the shards it owns. The atomic lease would still protect an individual scheduled occurrence if ownership changes or two replicas briefly overlap. After the claim succeeds, the Dispatcher would publish the same execution event to the Ready Queue. The Worker Cluster therefore stays independent from scheduler shard ownership.

I would watch due-scan delay and queue lag through Observability. If either grows, more scheduler or worker replicas can be added. The main downside is extra coordination around shard ownership. Rebalancing must be careful because overlapping ownership can cause duplicate claims, while missing ownership could delay scheduled work.

What happens if a worker completes the target action but fails before it records the successful result?

I would keep the existing at-least-once design and rely on the Idempotency Key Check to make a repeated delivery safe. The first worker may complete the external action but fail before the Result Writer saves SUCCESS in the Execution History Store. That logical run may therefore be delivered again.

Another worker can consume the repeated execution from the Ready Queue. Before running it, the worker checks the same idempotency key. The task handler should also tolerate that same key when possible. This prevents the repeated delivery from performing the business action twice.

The Retry Policy still controls exponential backoff and the maximum attempt count. Execution History records the attempts and final state, while Observability exposes failures and retry activity. The downside is that this is not true exactly-once execution. Correctness depends on proper idempotency around the task's external side effect.

20. How would you design a system that collects and aggregates user activities?System DesignHardGoogle

Question Details

Design a Google system for collecting and aggregating user activities from multiple clients, then serving periodic aggregate metrics for internal consumption.

Short Interview Answer (30-60 seconds)

At a high level, I would separate accepting activity events from calculating the reports. The main challenge is keeping ingestion fast while hourly and daily aggregation can take longer. I would explain three flows: event ingestion, background aggregation, and internal metric serving. Java ingestion replicas validate and normalize events, then publish them to a durable queue. Worker replicas build periodic aggregates, and the Internal Metrics Service serves them through a cache. The trade-off is that aggregate results can appear a little later.

Detailed Explanation

The system must collect activity events from web apps, mobile apps, and backend services. It should accept those events quickly and safely without making clients wait for reporting work. The difficult part is that calculating hourly or daily totals can take longer than accepting one event. The diagram separates these jobs. Java services first accept and normalize events. A durable queue holds accepted work. Separate Java workers process events later and build periodic aggregates. Another Java service then serves those prepared results to internal dashboards and reporting jobs.

Useful Questions to Ask the Interviewer
  1. Which activity types and grouping fields must the aggregates support?
  2. How quickly should hourly or daily aggregates become visible?
  3. How long should raw events remain available for replay or audit?
How would you design a system that collects and aggregates user activities? diagram
How to Explain It in an Interview
1. Start with the ingestion path

For the write path, activity producers send events through the API Gateway / Load Balancer. The Admission Pipeline handles AuthN/AuthZ, schema validation, rate limiting, and the idempotency or dedup key. These checks protect the rest of the system from bad, unauthorized, repeated, or excessive requests.

The Activity Ingestion Service runs as Java 21/25 stateless replicas. Each replica has its own JVM or process boundary. Inside one replica, virtual-thread or thread-pooled request handlers can handle concurrent HTTP requests. The service validates and normalizes the event, generates the dedup key, and publishes the normalized event to the Durable Event Queue / Log.

After durable acceptance, the client receives 202 Accepted. The client does not wait for aggregation to finish.

2. Separate ingestion with the durable queue

The Durable Event Queue / Log separates the fast write path from slower background work. The diagram shows it as partitioned, replicated, and durable. This lets accepted events wait safely when workers cannot process them immediately.

The queue also supports backpressure and replay. Backpressure means work can wait instead of overwhelming the workers. Replay means stored events can be processed again when needed.

3. Build aggregates in background workers

Aggregation Workers run as separate Java consumer replicas. They consume queued events in the background. They group events by client and activity type, place them into time windows, and compute hourly or daily aggregates.

Workers write to two stores. The Raw Event Store keeps immutable events, meaning the stored raw events remain unchanged for replay or audit. The Aggregate Metrics Store keeps the precomputed periodic aggregates.

The writes are asynchronous and idempotent. Idempotent means repeating the same write should not create an extra result. If processing fails, workers retry with backoff. After retries are exhausted, poison events move to the Dead-Letter Queue for later inspection.

4. Serve prepared metrics

The Internal Metrics Service runs as Java 21/25 stateless replicas. It serves periodic metrics to the Internal Dashboard and Reporting / Batch Jobs.

For hot aggregate windows, the service checks the Cache. On a cache hit, the cached metrics return quickly. On a cache miss, the read falls back to the Aggregate Metrics Store. The cache is therefore a speed layer, while the prepared aggregates remain in the Aggregate Metrics Store.

5. Explain scale and the main trade-off

Stateless ingestion, aggregation, and metrics-service replicas can scale horizontally. Rate limiting protects the entry path. Logs, metrics, traces, and alerts provide observability across the services and workers.

The main trade-off is delayed reporting. Aggregates are eventually consistent, which means a newly accepted event may not appear in an hourly or daily result immediately. We accept that delay because ingestion stays fast and aggregation can scale independently.

Engineering Considerations / Design Trade-offs

The benefit is that clients do not wait for hourly or daily calculations. The durable queue separates fast event acceptance from slower worker processing. It also provides backpressure and replay when workers fall behind. Stateless Java replicas make the services and workers easier to scale. The cache makes hot metric reads faster. The downside is that reports may be a little behind because aggregation runs in the background. Retries and the Dead-Letter Queue also add operational work. We accept that extra complexity because it keeps ingestion responsive and makes failed events easier to inspect and process later.

Why Interviewers Ask This

The interviewer wants to see whether you can split a large system into clear flows. This question tests whether you can keep a request path fast while moving heavier work into background workers. It also checks your judgment around durable queues, caching, retries, scaling, rate limits, observability, and delayed aggregate results. The important part is explaining why each choice exists and what downside comes with it.

Interviewer may ask next
What would you change if activity volume became much larger and the Aggregation Workers started falling behind?

I would keep the same architecture and scale the parts that are already separated in the diagram. The Durable Event Queue / Log would continue holding accepted events while the Aggregation Workers worked through the backlog. I would add more worker replicas so more background work can be processed in parallel.

The existing observability path would be important here. Logs, metrics, traces, and alerts would show whether workers are falling behind or repeatedly failing. The retry-with-backoff path should remain bounded so failed work does not create an endless retry loop. Events that still fail after the allowed retries should move to the Dead-Letter Queue for inspection.

Rate limiting at the Admission Pipeline also protects the system from accepting unlimited pressure at once. The durable queue provides backpressure, so work can wait safely instead of overwhelming workers immediately.

The main downside is delay. Hourly or daily aggregates may become visible later until the worker backlog is cleared.

What happens if the Cache used by the Internal Metrics Service is unavailable?

I would keep the same serving design and use the fallback already shown in the diagram. The Cache is only used to make hot aggregate reads faster. The prepared periodic results still remain in the Aggregate Metrics Store.

If a cache lookup cannot return the value, the read falls back to the Aggregate Metrics Store. The Internal Metrics Service can then return those periodic metrics to the Internal Dashboard or Reporting / Batch Jobs. This keeps the serving path working without treating the Cache as the main data store.

The existing logs, metrics, traces, and alerts would help operators see the cache problem and the increased work reaching the Aggregate Metrics Store.

The main downside is slower reads and more load on the Aggregate Metrics Store. The system can still serve the prepared metrics, but it temporarily loses the faster hot-window path provided by the Cache.

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.