Salesforce Interview Prep
Interview Profile
- Rounds: 4–5 (OOP design + graph/tree coding + system design for multi-tenant SaaS + behavioral)
- Tone: Clean, extensible solutions. Salesforce interviewers penalize micro-optimization tricks that sacrifice readability.
- Senior bar: Explicitly connect algorithmic choices to the multi-tenant SaaS data model. Discuss extensibility — how would you change this if the schema evolved? How would you add a new record type without rewriting the core logic?
- What Salesforce cares about: OOP design patterns (Strategy, Composite, Observer), multi-tenancy isolation, “Ohana” culture (collaboration, transparency), and trust/security as a first-class concern.
Pattern Frequency Breakdown
| Pattern | Approximate Frequency | Notes |
|---|---|---|
| Trees / Graphs | 30% | Org charts, account hierarchies, workflow DAGs |
| HashMap / OOP Design | 25% | Record metadata, dedup, in-memory stores |
| Dynamic Programming | 15% | Field tokenization, formula evaluation |
| Greedy / Intervals | 15% | Sales period scheduling, resource allocation |
| Stack | 10% | SOQL expression parsing, workflow rule nesting |
| Sliding Window | 5% | Report windowing, time-series aggregation |
Problems From This Repo
These problems from existing sessions map directly to Salesforce engineering domains. Study them in the context of CRM data models, not just as algorithmic exercises.
| Problem | Session Link | Why Salesforce |
|---|---|---|
| Binary Tree Level Order Traversal (LC 102) | [[Session-06-Trees|Session 6: Trees]] | BFS on account hierarchy for roll-up reports — every parent account aggregates metrics from all child accounts level by level |
| Lowest Common Ancestor (LC 236) | [[Session-06-Trees|Session 6: Trees]] | Shared account parent resolution — given two contacts, find the closest shared account owner for escalation routing |
| Course Schedule (LC 207) | [[Session-08-Graphs|Session 8: Graphs]] | Workflow dependency ordering — Process Builder / Flow rules form a DAG; detect circular dependencies before deployment |
| Number of Connected Components (LC 323) | [[Session-14-AdvancedGraphs|Session 14: Advanced Graphs]] | Account cluster analysis — identify isolated groups of accounts with no cross-links for territory planning |
| Copy List with Random Pointer (LC 138) | [[Session-04-HashMap|Session 4: HashMap]] | Deep-copy a CRM record including all metadata pointers (related lookups, formula fields) without corrupting the original |
| Valid Parentheses (LC 20) | [[Session-05-Stack|Session 5: Stack]] | SOQL query expression validation — balanced parentheses, bracket matching in dynamic query builders |
| Word Break (LC 139) | [[Session-09-DP1D|Session 9: DP 1D]] | Salesforce field API name tokenization — parse compound field names into component tokens against a known dictionary |
| Non-overlapping Intervals (LC 435) | [[Session-12-Greedy|Session 12: Greedy]] | Sales period de-duplication — given overlapping opportunity close date windows, find the minimum set of periods to remove so none overlap |
| Insert Delete GetRandom O(1) (LC 380) | [[Session-04-HashMap|Session 4: HashMap]] | Record pool management — O(1) add/remove/random-sample from a set of active Salesforce object records |
| IPO / Maximum Capital (LC 502) | [[Session-07-Heap|Session 7: Heap]] | Sales deal prioritization — given capital constraints and projected revenue, greedily select deals to maximize cumulative closed revenue |
New Problems (Salesforce-Specific)
These five problems do not appear in the core session files. Each has a direct Salesforce product mapping. Study both brute-force and optimal approaches — at the senior level, explaining why you moved from one to the other matters as much as the final solution.
Problem 1: Serialize and Deserialize N-ary Tree (LC 428) — Hard
Link: LeetCode 428
Quick Summary: Design an algorithm to serialize an N-ary tree (each node may have any number of children) to a string, and deserialize that string back to the original tree structure.
Why Salesforce: Salesforce’s core data model is a forest of N-ary trees — Account → Contacts → Cases → Activities, each with unbounded children. When exporting org chart snapshots or replicating account hierarchies across sandboxes, you need a reliable serialization format. Showing BFS level encoding vs. DFS parenthetical encoding maps directly to how Salesforce exports metadata bundles.
Approach 1: DFS Parenthetical Encoding (Brute Force / Recursive)
// Time: O(n) Space: O(n) output + O(h) recursion stack
// N-ary tree node definition
class NaryNode {
int val;
List<NaryNode> children;
NaryNode(int val) { this.val = val; this.children = new ArrayList<>(); }
}
public String serializeDFS(NaryNode root) {
if (root == null) return "";
StringBuilder sb = new StringBuilder();
serializeDFSHelper(root, sb);
return sb.toString();
}
// Format: val(child1(...)child2(...))
private void serializeDFSHelper(NaryNode node, StringBuilder sb) {
sb.append(node.val);
sb.append('(');
for (NaryNode child : node.children) {
serializeDFSHelper(child, sb);
}
sb.append(')');
}
public NaryNode deserializeDFS(String data) {
if (data == null || data.isEmpty()) return null;
int[] idx = {0};
return deserializeDFSHelper(data, idx);
}
private NaryNode deserializeDFSHelper(String data, int[] idx) {
// parse integer value (may be multi-digit)
int start = idx[0];
while (idx[0] < data.length() && data.charAt(idx[0]) != '(' && data.charAt(idx[0]) != ')') {
idx[0]++;
}
int val = Integer.parseInt(data.substring(start, idx[0]));
NaryNode node = new NaryNode(val);
idx[0]++; // consume '('
while (data.charAt(idx[0]) != ')') {
node.children.add(deserializeDFSHelper(data, idx));
}
idx[0]++; // consume ')'
return node;
}- Time: O(n) — visit each node once
- Space: O(n) for output string + O(h) recursion stack; h can equal n for a degenerate chain
- Tradeoff: Simple recursion, easy to reason about. Fails on very deep trees (JVM stack overflow) if account hierarchy is thousands of levels deep. The parenthetical format is human-readable but not the most compact.
Approach 2: BFS Level Encoding with Child Count (Optimal for Production)
// Time: O(n) Space: O(n) — no recursion stack risk, ideal for large org hierarchies
public String serializeBFS(NaryNode root) {
if (root == null) return "";
StringBuilder sb = new StringBuilder();
Queue<NaryNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
NaryNode node = queue.poll();
// encode: value + number of children (so deserializer knows how many to read)
sb.append(node.val).append(',').append(node.children.size()).append(',');
for (NaryNode child : node.children) {
queue.offer(child);
}
}
return sb.toString();
}
public NaryNode deserializeBFS(String data) {
if (data == null || data.isEmpty()) return null;
String[] tokens = data.split(",");
int idx = 0;
NaryNode root = new NaryNode(Integer.parseInt(tokens[idx++]));
int rootChildCount = Integer.parseInt(tokens[idx++]);
// Queue holds (node, remaining children to assign)
Queue<NaryNode> parentQueue = new LinkedList<>();
Queue<Integer> childCountQueue = new LinkedList<>();
parentQueue.offer(root);
childCountQueue.offer(rootChildCount);
while (!parentQueue.isEmpty()) {
NaryNode parent = parentQueue.poll();
int count = childCountQueue.poll();
for (int i = 0; i < count; i++) {
NaryNode child = new NaryNode(Integer.parseInt(tokens[idx++]));
int childCount = Integer.parseInt(tokens[idx++]);
parent.children.add(child);
parentQueue.offer(child);
childCountQueue.offer(childCount);
}
}
return root;
}- Time: O(n)
- Space: O(w) queue — w is the maximum width of the tree (max children at any level); no recursion stack
- Key Insight: Encoding
(val, childCount)pairs in BFS order lets the deserializer reconstruct parent-child links without delimiter ambiguity. The two-queue trick — one for parent nodes, one for child counts — synchronizes the reconstruction. This is the pattern Salesforce’s metadata export actually resembles: a breadth-first snapshot where each record announces how many related records follow.
Problem 2: Number of Provinces (LC 547) — Medium
Link: LeetCode 547
Quick Summary: Given an n x n adjacency matrix isConnected where isConnected[i][j] = 1 means city i and city j are directly connected, return the number of connected components (provinces).
Why Salesforce: Similar to LC 200 (Islands, covered in Session 8 on grid adjacency lists), but the input is an adjacency matrix — a common format when the relationship data comes from a CRM join table or a many-to-many object. Account ownership clustering: given a matrix of account-to-account relationships (shared contacts, shared opportunities), count distinct ownership clusters. Tests your ability to apply the same Union-Find / DFS pattern to a different encoding.
Approach 1: DFS on Adjacency Matrix (Brute Force)
// Time: O(n²) Space: O(n) visited array + O(n) recursion stack
public int findCircleNumDFS(int[][] isConnected) {
int n = isConnected.length;
boolean[] visited = new boolean[n];
int provinces = 0;
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfs(isConnected, visited, i);
provinces++;
}
}
return provinces;
}
private void dfs(int[][] isConnected, boolean[] visited, int city) {
visited[city] = true;
for (int neighbor = 0; neighbor < isConnected.length; neighbor++) {
if (isConnected[city][neighbor] == 1 && !visited[neighbor]) {
dfs(isConnected, visited, neighbor);
}
}
}- Time: O(n²) — must scan each row fully to find neighbors
- Space: O(n) visited + O(n) recursion stack
- Tradeoff: Simple and correct. The O(n²) is unavoidable given the adjacency matrix input — you must read all n² entries. For sparse graphs, an adjacency list representation would be O(n + e) but that’s an input format choice, not an algorithmic one.
Approach 2: Union-Find (Optimal — supports incremental merges)
// Time: O(n² · α(n)) ≈ O(n²) Space: O(n)
// α(n) is the inverse Ackermann function — effectively constant
public int findCircleNumUnionFind(int[][] isConnected) {
int n = isConnected.length;
int[] parent = new int[n];
int[] rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
int components = n;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // upper triangle only — matrix is symmetric
if (isConnected[i][j] == 1) {
if (union(parent, rank, i, j)) {
components--;
}
}
}
}
return components;
}
private int find(int[] parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]); // path compression
return parent[x];
}
private boolean union(int[] parent, int[] rank, int x, int y) {
int px = find(parent, x), py = find(parent, y);
if (px == py) return false; // already same component
if (rank[px] < rank[py]) parent[px] = py;
else if (rank[px] > rank[py]) parent[py] = px;
else { parent[py] = px; rank[px]++; }
return true;
}- Time: O(n² · α(n)) — iterating all pairs is O(n²); each union/find is nearly O(1) with path compression and union by rank
- Space: O(n) — only the parent and rank arrays
- Key Insight: Scanning only the upper triangle (
j = i + 1) halves the work since the matrix is symmetric — a small but production-relevant optimization worth calling out. More importantly, Union-Find shines when relationships arrive as a stream (new account links added over time) — you can process each new connection in O(α(n)) without re-running DFS on the entire dataset. This is the right data structure for incremental CRM deduplication pipelines.
Problem 3: Accounts Merge (LC 721) — Medium
Link: LeetCode 721
Quick Summary: Given a list of accounts where each account is [name, email1, email2, ...], merge accounts that share at least one email address. Return merged accounts with emails sorted.
Why Salesforce: This is literally Salesforce’s CRM data quality problem — duplicate account/contact deduplication. Two Salesforce records should be merged if they share an email address. The Union-Find by email key pattern is the production-grade approach: as new records arrive, union them by shared email, then collect groups.
Approach 1: DFS on Email Graph (Brute Force)
// Time: O(n·k·log(n·k)) Space: O(n·k)
// n = number of accounts, k = average emails per account
public List<List<String>> accountsMergeDFS(List<List<String>> accounts) {
// Build adjacency list: email → list of connected emails
Map<String, Set<String>> graph = new HashMap<>();
Map<String, String> emailToName = new HashMap<>();
for (List<String> account : accounts) {
String name = account.get(0);
for (int i = 1; i < account.size(); i++) {
emailToName.put(account.get(i), name);
graph.putIfAbsent(account.get(i), new HashSet<>());
if (i > 1) {
// connect email i to email i-1 (chain within this account)
graph.get(account.get(i)).add(account.get(i - 1));
graph.get(account.get(i - 1)).add(account.get(i));
}
}
}
Set<String> visited = new HashSet<>();
List<List<String>> result = new ArrayList<>();
for (String email : graph.keySet()) {
if (!visited.contains(email)) {
List<String> component = new ArrayList<>();
dfsEmail(graph, visited, email, component);
Collections.sort(component);
component.add(0, emailToName.get(email));
result.add(component);
}
}
return result;
}
private void dfsEmail(Map<String, Set<String>> graph, Set<String> visited,
String email, List<String> component) {
visited.add(email);
component.add(email);
for (String neighbor : graph.get(email)) {
if (!visited.contains(neighbor)) {
dfsEmail(graph, visited, neighbor, component);
}
}
}- Time: O(n·k·log(n·k)) — dominated by sorting emails within each merged group
- Space: O(n·k) — graph stores all email-to-email edges
- Tradeoff: Correct. Building a full email graph makes the connectivity explicit but uses more memory than Union-Find. DFS can also hit stack limits if one account cluster has thousands of emails.
Approach 2: Union-Find by Email Key (Optimal)
// Time: O(n·k·α(n·k)·log(n·k)) ≈ O(n·k·log(n·k)) Space: O(n·k)
// The log factor comes from sorting; Union-Find operations are near O(1)
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, String> parent = new HashMap<>(); // email → representative email
Map<String, String> emailToName = new HashMap<>();
// Initialize: each email is its own parent
for (List<String> account : accounts) {
String name = account.get(0);
for (int i = 1; i < account.size(); i++) {
parent.put(account.get(i), account.get(i));
emailToName.put(account.get(i), name);
}
}
// Union all emails within the same account
for (List<String> account : accounts) {
String first = account.get(1);
for (int i = 2; i < account.size(); i++) {
union(parent, first, account.get(i));
}
}
// Group emails by their root representative
Map<String, List<String>> groups = new HashMap<>();
for (String email : parent.keySet()) {
String root = find(parent, email);
groups.computeIfAbsent(root, k -> new ArrayList<>()).add(email);
}
List<List<String>> result = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : groups.entrySet()) {
List<String> emails = entry.getValue();
Collections.sort(emails);
emails.add(0, emailToName.get(entry.getKey()));
result.add(emails);
}
return result;
}
private String find(Map<String, String> parent, String x) {
if (!parent.get(x).equals(x)) {
parent.put(x, find(parent, parent.get(x))); // path compression
}
return parent.get(x);
}
private void union(Map<String, String> parent, String x, String y) {
String px = find(parent, x), py = find(parent, y);
if (!px.equals(py)) parent.put(px, py);
}- Time: O(n·k·log(n·k)) — sorting dominates; Union-Find operations are nearly O(1)
- Space: O(n·k) — parent map + groups map
- Key Insight: Using the email string itself as the Union-Find key (rather than an integer index) makes the code directly readable as “merge these two email identities.” The two-pass structure — first union all same-account emails, then group by root — matches the production pattern for batch CRM deduplication jobs: process all known links first, then emit the merged records. Calling out the sort cost matters at Salesforce: n·k can be millions of emails in a large org.
Problem 4: Decode String (LC 394) — Medium
Link: LeetCode 394
Quick Summary: Given an encoded string like 3[a2[c]], decode it to accaccacc. The encoding format is k[encoded_string] where the encoded_string inside the brackets is repeated exactly k times. k is always a positive integer.
Why Salesforce: SOQL nested query expansion — Salesforce’s formula engine and workflow rule template renderer process nested expressions with multiplier semantics. When a Process Builder template has nested merge fields or repeated structures, the expansion algorithm is stack-based decode. Also directly applicable to SOQL sub-select parsing and Apex string template rendering.
Approach 1: Recursive (Brute Force)
// Time: O(max_k^depth · n) Space: O(depth) recursion stack
// max_k = maximum multiplier; depth = nesting level
public String decodeStringRecursive(String s) {
int[] idx = {0};
return decode(s, idx);
}
private String decode(String s, int[] idx) {
StringBuilder result = new StringBuilder();
while (idx[0] < s.length() && s.charAt(idx[0]) != ']') {
char c = s.charAt(idx[0]);
if (Character.isLetter(c)) {
result.append(c);
idx[0]++;
} else if (Character.isDigit(c)) {
// parse full integer (may be multi-digit: "10[a]")
int k = 0;
while (Character.isDigit(s.charAt(idx[0]))) {
k = k * 10 + (s.charAt(idx[0]++) - '0');
}
idx[0]++; // consume '['
String inner = decode(s, idx);
idx[0]++; // consume ']'
result.append(inner.repeat(k)); // Java 11+
}
}
return result.toString();
}- Time: O(output length) — each character in the final decoded string is produced once; output length can be exponential in the depth of nesting
- Space: O(nesting depth) recursion stack
- Tradeoff: Clean and readable. Stack overflow risk if nesting depth is large (unlikely for SOQL but possible for adversarial input).
String.repeat(k)is Java 11+; note this in the interview.
Approach 2: Iterative Stack (Optimal — no recursion risk)
// Time: O(output length) Space: O(nesting depth) stack
public String decodeString(String s) {
Deque<Integer> countStack = new ArrayDeque<>();
Deque<StringBuilder> stringStack = new ArrayDeque<>();
StringBuilder current = new StringBuilder();
int k = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
k = k * 10 + (c - '0'); // accumulate multi-digit numbers
} else if (c == '[') {
countStack.push(k); // save repeat count
stringStack.push(current); // save string built so far
current = new StringBuilder(); // start fresh for the inner segment
k = 0;
} else if (c == ']') {
int repeatCount = countStack.pop();
StringBuilder prev = stringStack.pop();
String inner = current.toString();
prev.append(inner.repeat(repeatCount)); // Java 11+
current = prev;
} else {
current.append(c);
}
}
return current.toString();
}- Time: O(output length) — same asymptotic as recursive; each char produced once
- Space: O(d) where d is the maximum nesting depth — two stacks each holding at most d entries
- Key Insight: The two-stack idiom — one stack for pending repeat counts, one for partially built strings — is the canonical pattern for any bracket-scoped multiplier problem. When
[is seen: push current state. When]is seen: pop, multiply, and re-append. ThecurrentStringBuilder always holds “what I’ve built at this nesting level so far.” This same pattern applies to JSON parsing, nested macro expansion, and XML entity decoding — all relevant to Salesforce’s metadata processing.
Problem 5: LRU Cache (LC 146) — Medium
Link: LeetCode 146
Quick Summary: Design a data structure that supports get(key) and put(key, value) both in O(1) time, evicting the least recently used entry when capacity is exceeded.
Why Salesforce: Salesforce’s SOQL result caching — when 100,000 orgs share the same database cluster, query results are cached in a shared LRU cache to reduce DB load. The design question is more interesting than the coding: per-tenant LRU vs. shared LRU, what to do when a high-volume org evicts all cache entries for low-volume orgs (noisy neighbor problem), and whether a TTL should override LRU ordering.
Approach 1: LinkedHashMap (Brute Force / Java Library)
// Time: O(1) amortized for get/put Space: O(capacity)
public class LRUCacheLinkedHashMap {
private final int capacity;
private final LinkedHashMap<Integer, Integer> cache;
public LRUCacheLinkedHashMap(int capacity) {
this.capacity = capacity;
// accessOrder=true: iteration order = insertion order updated on access
this.cache = new LinkedHashMap<>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > LRUCacheLinkedHashMap.this.capacity;
}
};
}
// Time: O(1) Space: O(1)
public int get(int key) {
return cache.getOrDefault(key, -1);
}
// Time: O(1) Space: O(1)
public void put(int key, int value) {
cache.put(key, value);
}
}- Time: O(1) —
LinkedHashMapwithaccessOrder=truemaintains LRU order internally - Space: O(capacity)
- Tradeoff: Extremely concise. Acceptable in a Java interview if you explain why it works (access-order LinkedHashMap). However, interviewers will immediately ask you to implement it without the library — so always be ready with the HashMap + DLL approach.
Approach 2: HashMap + Doubly Linked List (Optimal / Expected Implementation)
// Time: O(1) for get and put Space: O(capacity)
public class LRUCache {
private final int capacity;
private final Map<Integer, DLLNode> map = new HashMap<>();
// Sentinel head (LRU end) and tail (MRU end)
private final DLLNode head = new DLLNode(0, 0);
private final DLLNode tail = new DLLNode(0, 0);
private static class DLLNode {
int key, val;
DLLNode prev, next;
DLLNode(int key, int val) { this.key = key; this.val = val; }
}
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
// Time: O(1)
public int get(int key) {
if (!map.containsKey(key)) return -1;
DLLNode node = map.get(key);
moveToTail(node); // most recently used
return node.val;
}
// Time: O(1)
public void put(int key, int value) {
if (map.containsKey(key)) {
DLLNode node = map.get(key);
node.val = value;
moveToTail(node);
} else {
if (map.size() == capacity) {
// evict LRU node (just after head sentinel)
DLLNode lru = head.next;
removeNode(lru);
map.remove(lru.key); // key stored in node for O(1) map removal
}
DLLNode newNode = new DLLNode(key, value);
addToTail(newNode);
map.put(key, newNode);
}
}
private void removeNode(DLLNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToTail(DLLNode node) {
node.prev = tail.prev;
node.next = tail;
tail.prev.next = node;
tail.prev = node;
}
private void moveToTail(DLLNode node) {
removeNode(node);
addToTail(node);
}
}- Time: O(1) for both
getandput— HashMap gives O(1) lookup; DLL gives O(1) insert/remove at any position given a pointer - Space: O(capacity) — map + list both bounded by capacity
- Key Insight: The HashMap stores pointers to DLL nodes — this is what enables O(1) removal from the middle of the list. Without the map, finding a node in the list would be O(n). The sentinel head/tail nodes eliminate null checks on boundary conditions. Storing
keyin the node itself is the subtle trick that allows O(1) map removal when evicting the LRU entry from the list’s head.
Salesforce Senior Discussion — Multi-Tenant Cache Design:
When 100,000 Salesforce orgs share the same SOQL result cache:
- Per-tenant LRU (separate cache per org): guarantees isolation but requires 100,000 small caches; memory overhead is high, and small orgs waste most of their capacity allocation.
- Shared LRU: memory-efficient but a high-query org can evict all entries for low-query orgs (noisy neighbor). Salesforce solves this with capacity quotas per org tier.
- TTL override: authorization holds must expire even if recently accessed — the
putshould accept an optional TTL; an eviction thread or lazy expiry ongethandles TTL enforcement. LRU alone is insufficient for security-sensitive caches. - Consistency: in a distributed shared cache (multiple app servers), you need distributed LRU (e.g., Redis with LRU eviction policy) — a single JVM DLL is not distributed-safe.
CRM Domain Connections
| Algorithm | Salesforce Product / Use Case |
|---|---|
| N-ary Tree Serialization | Org chart export, account hierarchy replication across sandboxes |
| Union-Find (Number of Provinces) | Territory cluster analysis, identifying isolated account groups |
| Union-Find Merge (Accounts Merge) | CRM duplicate detection, contact and account deduplication |
| Stack (Decode String) | Workflow rule template engine, SOQL nested query expansion |
| LRU Cache | SOQL query result caching, multi-tenant shared cache with per-org quotas |
| BFS Level Order | Account roll-up reports, org chart level-by-level aggregation |
| Graph DFS / BFS | Process Builder dependency validation, Flow rule cycle detection |
2-Week Sprint Plan
| Day | Focus |
|---|---|
| Day 1 | LC 102 (Level Order BFS), LC 236 (LCA) — solidify tree traversal |
| Day 2 | LC 428 (N-ary Serialize) — BFS + DFS both approaches |
| Day 3 | LC 547 (Provinces) + LC 323 (Connected Components) — Union-Find pattern |
| Day 4 | LC 721 (Accounts Merge) — Union-Find by string key |
| Day 5 | LC 207 (Course Schedule) — topological sort, cycle detection |
| Day 6 | LC 146 (LRU Cache) — HashMap + DLL; LinkedHashMap shortcut |
| Day 7 | LC 394 (Decode String) — stack-based multiplier expansion |
| Day 8 | LC 138 (Copy List with Random Pointer) — deep copy with HashMap |
| Day 9 | LC 380 (Insert Delete GetRandom) + LC 502 (IPO) |
| Day 10 | LC 139 (Word Break) + LC 435 (Non-overlapping Intervals) |
| Day 11 | Mock: OOP design — design a CRM record merge system |
| Day 12 | Mock: System design — multi-tenant SOQL query cache |
| Day 13 | Behavioral: “Ohana” culture questions, trust and security stories |
| Day 14 | Full mock interview simulation |
Senior-Level Talking Points
Multi-Tenancy: The Always-Ask Question
For every problem, ask yourself: “How does this scale when 100,000 orgs share the same infrastructure?”
- Graph algorithms: If you’re running BFS/DFS on account hierarchy, one org’s BFS should not block another. Discuss per-org job isolation, queue-based processing, and resource quotas.
- Caching: Shared caches create noisy-neighbor problems. Always mention per-tenant capacity limits and TTL enforcement alongside LRU.
- Serialization: When replicating data across sandboxes, the serialization format must be version-tolerant — new fields added to a later release should not break deserialization of older snapshots.
- Deduplication: At scale, account merge jobs must be idempotent — running the same merge twice should produce the same result with no side effects. Union-Find is inherently idempotent.
OOP Design Patterns
Mention these naturally alongside algorithmic solutions:
- Strategy Pattern: When you have multiple merge strategies (email-match, phone-match, fuzzy-name-match), encapsulate each as a
MergeStrategyinterface — the Union-Find structure stays the same, only the “should these be merged?” predicate changes. - Composite Pattern: N-ary tree naturally maps to the Composite pattern —
AccountNodeandLeafContactNodeimplement the sameCRMRecordinterface; recursive operations (serialize, roll-up) work uniformly on both. - Observer Pattern: When a cache eviction happens (LRU evicts an entry), downstream systems (report generators, formula evaluators) may need to be notified. Discuss how observers subscribe to eviction events rather than polling.
Trust and Security
Salesforce’s #1 value is trust. Volunteer these in discussions:
- Serialized account hierarchies must be encrypted at rest — discuss where in the serialize/deserialize flow you would apply tenant-specific encryption keys.
- LRU cache keys must be scoped by org ID —
cache.get(orgId + ":" + queryHash)not justqueryHash, or one org could read another org’s cached query results. - Graph traversals that cross tenant boundaries (shared accounts in Experience Cloud) require permission checks at each edge traversal, not just at the root.