Google Coding Strategy (L4) ​
Google's coding rounds grade more dimensions than any other FAANG. You are not just being asked "can you solve this problem" — you are being asked "can you solve this problem the way a senior Googler would solve it, in a stripped-down environment, while narrating, while thinking about trade-offs, while catching edge cases, and while writing code that another engineer could merge tomorrow."
This file is the tactical playbook.
1. The Google Doc Environment ​
Every coding round (phone screen + onsite) happens in a shared Google Doc. Not CoderPad. Not a real IDE. A Doc.
What you don't have ​
- No syntax highlighting — your
ifand yourforlook the same as your variable names - No autocomplete — you type every
System.out.printlnorstd::unordered_mapin full - No code execution — you cannot run, compile, or lint. You dry-run with your eyes.
- No linter squiggles — typos, missing semicolons, unclosed braces are invisible until you re-read
- No tab-stops or smart indent — the Doc converts tabs inconsistently; prefer spaces
What you do have ​
- A blank page and a ticking 45-minute clock
- An interviewer watching every keystroke in real time
Strategies to survive ​
Practice in the Doc before the interview. Open a fresh Doc, paste a problem statement in, and solve it end-to-end in the Doc. Do this for 10-15 problems across languages. You will be shocked how much slower you are without autocomplete.
Memorize the boilerplate. You should be able to type the following without thinking:
- Priority queue / heap construction in your language
- HashMap/HashSet declarations
- Input parsing if the problem gives you a string
- Binary search template (mid = lo + (hi - lo) / 2 to avoid overflow)
- BFS template with visited set
- DFS with memoization template
Pick one language and stay there. Switching from Python to Java mid-interview to "show flexibility" is a red flag. Use the language you are fastest and most accurate in. TypeScript, Python, C++, Java, Go are all fine.
Slow down your typing by 20%. A typo in a Doc costs 2 minutes: 30 seconds to spot, 90 seconds to stare at the surrounding 5 lines wondering why the logic is wrong. Type slightly slower and you save typos that cost 10x more time.
Use blank lines as visual scope cues. Without indentation highlighting, a 20-line function is a wall of text. Two blank lines between logical blocks makes the Doc scannable for you and the interviewer.
Leave your helpers at the top. Declare utility functions before main() / the solve function. Readers (interviewer + HC) scan top-to-bottom.
2. Pre-Coding Ritual (2-3 min) ​
Do NOT start typing code when the interviewer finishes reading the problem. The first 2-3 minutes are the highest-leverage part of the round.
Step 1: Clarify requirements (1-2 min) ​
Ask at least 2-3 clarifying questions, even on problems you recognize. Interviewers grade you on whether you thought about ambiguity.
Template questions that always apply:
- "What are the input size bounds? Is n up to 10^3, 10^5, or 10^9?"
- "Can the input be empty? Can it contain duplicates? Can values be negative?"
- "What's the expected output format — a list, a count, a boolean?"
- "Can I assume the input is valid, or do I need to handle malformed input?"
- "Is there a memory constraint beyond the obvious?"
For graphs: directed vs undirected, cyclic vs acyclic, connected vs multiple components, self-loops, multi-edges. For trees: balanced? BST? complete? leaf-values only? For strings: ASCII, lowercase only, Unicode, empty allowed, max length?
Step 2: State your approach verbally ​
Before writing a single line of code, say:
"My plan is to use a sliding window with a HashMap tracking character counts. I'll expand the window while the constraint holds and shrink when it's violated. This is O(n) time and O(k) space where k is the number of distinct characters."
The interviewer is now evaluating your approach, not your typing. If your approach is wrong, they will give you a hint NOW, saving you 20 minutes of wrong code.
Step 3: State complexity BEFORE coding ​
Google grades this heavily. Say:
"Target complexity: O(n log n) time for the sort, O(n) auxiliary space for the result array."
If the interviewer wanted a different complexity (e.g., O(n) expected), they'll say so. If they nod, proceed with confidence.
3. Code Cleanliness Expectations ​
At L4, code cleanliness is scored as its own dimension. Here is what "meets bar" looks like.
Variable naming ​
- Bad:
i, j, k, tmp, arr, res - Meets bar:
leftIdx, rightIdx, prefixSum, currentNode, result - Strong:
windowStart, windowEnd, charFrequencies, visitedNodes, mergedIntervals
Use i, j ONLY for trivial loops where the index has no semantic meaning (e.g., iterating a fixed-size 2D grid).
Function decomposition ​
- Any block exceeding ~15 lines should be extracted into a helper
- Follow single responsibility: each function does one conceptual thing
- Helpers live above main logic in the Doc
Bad (80-line monster):
function solve(input: string): number {
// parse input
// build graph
// BFS from each node
// compute answer
// format output
// ... all inline, no helpers
}Meets bar:
function parseEdges(input: string): Edge[] { ... }
function buildAdjacencyList(edges: Edge[], n: number): number[][] { ... }
function bfsShortestPath(graph: number[][], source: number, target: number): number { ... }
function solve(input: string): number {
const edges = parseEdges(input);
const graph = buildAdjacencyList(edges, countNodes(edges));
return bfsShortestPath(graph, 0, graph.length - 1);
}SOLID-lite in LC problems ​
You are NOT expected to apply full SOLID to a LeetCode problem. You ARE expected to:
- Keep functions small and focused (SRP)
- Not mutate inputs unless the problem requires it
- Return values rather than using globals
- Name functions by what they do, not how they do it (
getShortestPathnotrunBfs)
Comments ​
- No comments for obvious code (
// increment i) - YES comments for non-obvious decisions:
// Using BFS instead of DFS because we need shortest path, not reachability// Early return for empty input to avoid null pointer in the loop below// O(n log n) — sorted array lets us two-pointer in O(n)
4. Narration While Coding ​
Silent coding = bad signal. The HC packet will explicitly note: "Candidate coded in silence for 15 minutes — hard to evaluate problem-solving."
What to narrate ​
- Before each block: "Now I'll build the adjacency list..."
- When picking a data structure: "I'm using a HashMap here because I need O(1) lookup by node ID."
- When making a trade-off: "I could use a deque, but a simple array with two pointers is cleaner and O(1) amortized."
- When handling an edge case: "If the input is empty, I return an empty array early — prevents the null check in the loop."
- When you notice a bug: "Wait, this doesn't handle the case where the window is smaller than k. Let me fix that." (Catching your own bug is a positive signal.)
What NOT to narrate ​
- Every variable assignment (noise)
- Obvious facts ("I'm going to write a for loop now" — no)
- Your uncertainty ("I'm not sure this will work but let me try" — says low confidence without adding info)
Balance ​
Aim for ~60% coding, 40% talking in a round. Silent stretches of >90 seconds are a warning sign. If you need to think, say "Let me think about the recurrence for 30 seconds." Then pause intentionally.
5. Post-Coding Checklist ​
Once your solution compiles in your head, do NOT say "I'm done." Instead, run the checklist:
1. Dry-run with a concrete example (2-3 min) ​
Pick a non-trivial example from the problem (or make one up). Trace through your code line by line, updating variable state in your head OR in the Doc below the code.
Input: [1, 3, -1, -3, 5, 3, 6, 7], k = 3
Iteration | window | deque | result
--------- | ------ | ----- | ------
i=0 | [1] | [0] | []
i=1 | [1,3] | [1] | []
i=2 | [1,3,-1]| [1,2]| [3]
...2. Handle edge cases explicitly ​
State each one OUT LOUD as you verify:
- "Empty input — my early return on line 3 handles this, returns []."
- "Single element — the while condition fails, returns [nums[0]]."
- "All duplicates — the map-update still works, O(n) time."
- "Max input size 10^5 — O(n log n) is ~1.7M ops, well within 1s."
3. Write 2-3 test cases ​
Format as:
// assert solve("abc", 2) === 3
// assert solve("", 1) === 0
// assert solve("aaaa", 2) === 3 -- duplicates boundaryInterviewers write in the packet: "Candidate proactively wrote tests and caught an off-by-one in the process." That's a Strong Hire signal.
4. Discuss alternatives ​
"An alternative would be sort + two-pointer, O(n log n) time but O(1) space. I chose the HashMap because the constraints allow O(n) space and the coefficient is smaller for this input size."
6. Complexity Analysis Framework ​
The 2-sentence template ​
- Time: one sentence, state the dominant operation and its count
- Space: one sentence, state the auxiliary space (not counting input/output)
Example:
"Time is O(n log n) — we sort once, then do a linear two-pointer sweep. Space is O(n) for the result array; if we can overwrite the input, auxiliary space is O(1) for the sort (in-place quicksort)."
Trade-off one-liners to memorize ​
- "O(n log n) time for O(1) extra space if we sort in place; O(n) time for O(n) extra space if we hash."
- "O(n) query but O(n log n) preprocessing — worth it if we expect >log(n) queries."
- "O(1) amortized but O(n) worst-case — acceptable here because we have a budget of n total operations."
- "The sorting dominates, so the overall is O(n log n). The rest is O(n)."
Always mention ​
- Recursion depth for recursive solutions (space cost of the call stack)
- Hidden constants when relevant (hash map overhead, cache misses on a linked list)
- Best / average / worst when they differ (quicksort, hash collisions)
7. "I've Seen This Before" ​
You WILL get a problem you've seen on LeetCode. What to do:
Option A: Full disclosure (preferred) ​
"I've seen a similar problem before. Let me work through it fresh to make sure I get the details right — is that okay?"
The interviewer almost always says yes and continues. They grade your problem-solving from the next minute onward, not your memorization.
Option B: Quiet solve (risky) ​
If you jump straight to the optimal solution with clean code in 10 minutes and then can't explain WHY it works, the interviewer will suspect memorization and give you a harder follow-up. This is a trap.
The rule ​
Google grades problem-solving, not memorization. Solve it fresh. If you stumble on a step you "know" is wrong, the interviewer sees a thinking engineer, not a flashcard.
8. When Stuck ​
Getting stuck is not a failure. Failing to recover IS.
Recovery ladder ​
State where you're stuck. "I'm trying to find the recurrence relation for this DP, and I can't figure out what the state should include."
Try the brute force. "Let me write the O(n^2) solution first to make sure I understand the problem. I'll optimize after."
Look for a pattern. Try 2-3 small examples by hand. Trace the answer. Often the recurrence jumps out.
Ask for a hint strategically. "I'm considering sliding window and two pointers — would you nudge me in a direction?" The interviewer will give you a binary signal: "Sliding window is a good thought." This is better than flailing silently.
Degrade gracefully. If you cannot reach the optimal, write the brute force cleanly and explain the gap. "This is O(n^2). I believe we can get to O(n log n) with a segment tree but I'm not confident in the implementation. Would you like me to attempt it or move on?"
Golden rule ​
Narrating an imperfect solution beats silent clean solution. The packet will say:
- Silent clean: "Candidate solved but unclear if they understood trade-offs. Leaning Hire."
- Narrated imperfect: "Candidate struggled briefly, identified the pattern, degraded gracefully to O(n^2) with full explanation. Hire."
9. Common Google Coding Traps ​
These are the bugs HC writeups call out most often:
Off-by-one in binary search ​
// Wrong: hi = nums.length makes 'mid' potentially out of bounds on read
// Wrong: while (lo < hi) vs while (lo <= hi) — depends on target semantics
// Template (lower_bound, first index where pred is true):
let lo = 0, hi = nums.length; // hi is EXCLUSIVE
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (pred(nums[mid])) hi = mid;
else lo = mid + 1;
}
return lo;Integer overflow in sums ​
- In C++,
int + intoverflows at ~2.1B. Uselong longfor sums over arrays of ~10^5 ints with values up to 10^5. - In JS/TS,
Number.MAX_SAFE_INTEGERis ~9 quadrillion, but be aware when you mix in bitwise ops (which truncate to 32 bits). - Compute
mid = lo + (hi - lo) / 2, notmid = (lo + hi) / 2, to avoid overflow on the sum.
Iterator invalidation when modifying structure ​
// Wrong: modifying map while iterating
for (const [key, val] of map) {
if (val < 0) map.delete(key); // undefined behavior in some langs
}
// Right: collect, then delete
const toDelete = [];
for (const [key, val] of map) if (val < 0) toDelete.push(key);
toDelete.forEach(k => map.delete(k));Confusing indices with values ​
Naming saves you: idxOfMin vs valOfMin. A common bug is return arr[arr.indexOf(min)] when you meant return arr.indexOf(min).
Not handling duplicates ​
- In sorted arrays, duplicates break binary search for "find any" vs "find first" — state which you're finding.
- In graphs, duplicate edges or self-loops break naive BFS.
- In strings, duplicate characters change sliding window behavior.
Recursion without memoization ​
Stating "this is O(2^n)" and getting a nod is fine if the interviewer wants brute force. If they expected DP, your silent exponential recursion will cost you 10 minutes and the round.
Forgetting the empty / single-element case ​
Always handle:
input === null || input === undefinedinput.length === 0input.length === 1
Write the early returns FIRST, then the main logic.
10. Sample Problems: Good vs Great ​
Three problems showing the jump from "correct solution" (Leaning Hire) to "Google-grade solution" (Strong Hire).
Problem 1: Two Sum (LC 1) ​
Given an array of integers
numsand an integertarget, return indices of the two numbers such that they add up totarget.
Good (Leaning Hire) ​
function twoSum(nums: number[], target: number): number[] {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const c = target - nums[i];
if (map.has(c)) return [map.get(c), i];
map.set(nums[i], i);
}
return [];
}Correct, O(n), but: no edge case discussion, single-letter variable, no comment on complexity.
Great (Strong Hire) ​
/**
* Two Sum: returns indices of two numbers summing to target.
* Time: O(n) — single pass through nums.
* Space: O(n) — HashMap holds up to n entries.
* Assumes exactly one solution exists (per problem constraint).
*/
function twoSum(nums: number[], target: number): [number, number] | null {
if (nums.length < 2) return null;
const seenValueToIndex = new Map<number, number>();
for (let currentIdx = 0; currentIdx < nums.length; currentIdx++) {
const complement = target - nums[currentIdx];
if (seenValueToIndex.has(complement)) {
return [seenValueToIndex.get(complement)!, currentIdx];
}
// Only record the current value AFTER checking complement
// to prevent matching nums[i] + nums[i] when target = 2 * nums[i].
seenValueToIndex.set(nums[currentIdx], currentIdx);
}
return null; // no pair found
}
// Tests:
// twoSum([2, 7, 11, 15], 9) === [0, 1]
// twoSum([3, 3], 6) === [0, 1] — duplicates boundary
// twoSum([1], 2) === null — fewer than 2 elements
// twoSum([], 0) === null — emptyDifferences:
- Complexity in a docstring
- Descriptive names (
seenValueToIndex,complement,currentIdx) - Explicit edge case handling AND a returned
nullrather than arbitrary[] - Comment on the non-obvious "why set after check" decision
- Test cases written
Problem 2: Number of Islands (LC 200) ​
Given an m x n 2D binary grid, return the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically.
Good (Leaning Hire) ​
int numIslands(vector<vector<char>>& grid) {
int count = 0;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int i, int j) {
if (i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] != '1') return;
grid[i][j] = '0';
dfs(grid, i+1, j);
dfs(grid, i-1, j);
dfs(grid, i, j+1);
dfs(grid, i, j-1);
}Works, but: mutates input silently, no comment on recursion depth (stack overflow risk), magic strings '1' and '0'.
Great (Strong Hire) ​
/**
* Number of Islands.
* Time: O(m * n) — each cell visited at most twice.
* Space: O(m * n) worst case for recursion stack on a grid of all land.
* For very large grids (e.g., 10^6 x 10^6) switch to iterative BFS.
*
* Approach: DFS from each unvisited land cell, marking visited in-place.
* NOTE: mutates input grid. If callers need the grid intact, copy first
* or use a separate `visited` 2D array at O(m*n) extra space.
*/
class Solution {
static constexpr char LAND = '1';
static constexpr char WATER = '0';
static constexpr int DIRS[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int islandCount = 0;
const int rows = grid.size();
const int cols = grid[0].size();
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
if (grid[row][col] == LAND) {
++islandCount;
sinkIsland(grid, row, col);
}
}
}
return islandCount;
}
private:
void sinkIsland(vector<vector<char>>& grid, int row, int col) {
if (row < 0 || col < 0 ||
row >= (int)grid.size() || col >= (int)grid[0].size() ||
grid[row][col] != LAND) {
return;
}
grid[row][col] = WATER;
for (const auto& [dr, dc] : DIRS) {
sinkIsland(grid, row + dr, col + dc);
}
}
};Differences:
- Complexity + space caveat for huge grids (shows scale awareness)
- Named constants for
'1'/'0'/ directions - Named helper (
sinkIsland) communicates intent - Explicit mutation warning in the comment (trade-off articulation)
- Empty grid guarded
Problem 3: LRU Cache (LC 146) ​
Design a data structure for LRU cache with O(1)
getandput.
Good (Leaning Hire) ​
class LRUCache {
private map = new Map<number, number>();
constructor(private cap: number) {}
get(key: number): number {
if (!this.map.has(key)) return -1;
const v = this.map.get(key)!;
this.map.delete(key);
this.map.set(key, v);
return v;
}
put(key: number, value: number): void {
if (this.map.has(key)) this.map.delete(key);
else if (this.map.size >= this.cap) {
const oldest = this.map.keys().next().value;
this.map.delete(oldest);
}
this.map.set(key, value);
}
}Clever (using Map's insertion-order property), correct, O(1). But the interviewer might say: "Implement it WITHOUT leveraging Map's ordering — build your own DLL." Be ready.
Great (Strong Hire) ​
/**
* LRU Cache with O(1) get and put.
*
* Data structures:
* - HashMap<key, Node> : O(1) key lookup
* - Doubly Linked List : O(1) move-to-front and O(1) evict-from-tail
*
* The DLL maintains recency order: head = most recently used, tail = LRU.
* HashMap points to the node so we can detach it in O(1) without searching.
*/
class Node {
prev: Node | null = null;
next: Node | null = null;
constructor(public key: number, public value: number) {}
}
class LRUCache {
private readonly keyToNode = new Map<number, Node>();
private readonly head = new Node(-1, -1); // sentinel: most-recent side
private readonly tail = new Node(-1, -1); // sentinel: LRU side
constructor(private readonly capacity: number) {
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: number): number {
const node = this.keyToNode.get(key);
if (!node) return -1;
this.moveToFront(node);
return node.value;
}
put(key: number, value: number): void {
const existing = this.keyToNode.get(key);
if (existing) {
existing.value = value;
this.moveToFront(existing);
return;
}
if (this.keyToNode.size >= this.capacity) {
this.evictLRU();
}
const node = new Node(key, value);
this.keyToNode.set(key, node);
this.insertAtFront(node);
}
private moveToFront(node: Node): void {
this.detach(node);
this.insertAtFront(node);
}
private insertAtFront(node: Node): void {
node.next = this.head.next;
node.prev = this.head;
this.head.next!.prev = node;
this.head.next = node;
}
private detach(node: Node): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private evictLRU(): void {
const lru = this.tail.prev!;
if (lru === this.head) return; // empty
this.detach(lru);
this.keyToNode.delete(lru.key);
}
}Differences:
- Sentinel nodes eliminate null checks (classic DLL hygiene)
- Private helpers for each invariant operation (
moveToFront,insertAtFront,detach,evictLRU) - Top-of-file comment explains the data-structure choice AND why
readonlyoncapacityandkeyToNodesignals intent- Works even if interviewer forbids Map insertion-order tricks
11. Quick-Reference Checklist ​
Keep this open during practice. Every problem, every time.
Before coding ​
- [ ] 2-3 clarifying questions asked
- [ ] Approach stated verbally
- [ ] Target complexity stated (time + space)
- [ ] Edge cases enumerated
While coding ​
- [ ] Narrating key decisions
- [ ] Meaningful variable names
- [ ] Helper functions for non-trivial blocks
- [ ] Comments on non-obvious decisions
- [ ] Typing at 80% speed for accuracy
After coding ​
- [ ] Dry-run with a concrete example
- [ ] Edge cases verified explicitly
- [ ] 2-3 test cases written
- [ ] Alternative approach discussed
- [ ] Complexity restated post-implementation
Cross-references ​
- 01-interview-loop — round structure, HC rubric
- 03-backend-fundamentals — concepts that flavor coding follow-ups (concurrency, distributed edge cases)
- 04-behavioral-googleyness — R4 STAR bank
- C++ interview reference — language-specific patterns