Java Backend Interview Prep
DSA — Arrays, Hashing & Sliding Window
1Two Sum (and variations: sorted array, k-sum)
Idea: HashMap of value → index. For each x, check if target - x was seen. O(n) time, O(n) space.
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
Integer j = seen.get(target - nums[i]);
if (j != null) return new int[]{j, i};
seen.put(nums[i], i);
}
return new int[0];
}
Sorted array variation: two pointers l=0, r=n-1; if the sum is too small do l++, if too big do r--. O(1) space.
k-Sum: sort, then recursively fix one element and reduce to (k-1)-sum. The base case is 2-sum with two pointers. O(n^(k-1)).
2Three Sum
Sort the array. Fix i, then run two pointers on the rest and skip duplicates. O(n²).
List<List<Integer>> threeSum(int[] a) {
Arrays.sort(a);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < a.length - 2; i++) {
if (i > 0 && a[i] == a[i - 1]) continue;
int l = i + 1, r = a.length - 1;
while (l < r) {
int s = a[i] + a[l] + a[r];
if (s == 0) {
res.add(List.of(a[i], a[l], a[r]));
while (l < r && a[l] == a[l + 1]) l++;
while (l < r && a[r] == a[r - 1]) r--;
l++; r--;
} else if (s < 0) l++; else r--;
}
}
return res;
}
3Top K Frequent Elements (heap + bucket sort)
Bucket sort, O(n): count frequencies, then put each number into bucket[freq] and read the buckets from highest to lowest.
int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int n : nums) freq.merge(n, 1, Integer::sum);
List<Integer>[] bucket = new List[nums.length + 1];
freq.forEach((n, f) -> {
if (bucket[f] == null) bucket[f] = new ArrayList<>();
bucket[f].add(n);
});
int[] res = new int[k]; int idx = 0;
for (int f = bucket.length - 1; f >= 0 && idx < k; f--)
if (bucket[f] != null) for (int n : bucket[f]) if (idx < k) res[idx++] = n;
return res;
}
Heap, O(n log k): keep a min-heap of size k ordered by frequency. When it grows past k, poll.
4Top-K Frequent Words (sorted by frequency, then lexicographically)
List<String> topKFrequent(String[] words, int k) {
Map<String, Integer> cnt = new HashMap<>();
for (String w : words) cnt.merge(w, 1, Integer::sum);
PriorityQueue<String> pq = new PriorityQueue<>(
(a, b) -> cnt.get(a).equals(cnt.get(b)) ? b.compareTo(a) : cnt.get(a) - cnt.get(b));
for (String w : cnt.keySet()) { pq.offer(w); if (pq.size() > k) pq.poll(); }
LinkedList<String> res = new LinkedList<>();
while (!pq.isEmpty()) res.addFirst(pq.poll());
return res;
}
The min-heap evicts the lowest frequency first. On a tie it evicts the lexicographically larger word, so the smaller word stays.
5Product of Array Except Self
Two passes without division: first store prefix products in the result, then multiply by suffix products. O(n), O(1) extra space.
int[] productExceptSelf(int[] a) {
int n = a.length; int[] res = new int[n];
res[0] = 1;
for (int i = 1; i < n; i++) res[i] = res[i - 1] * a[i - 1];
int suffix = 1;
for (int i = n - 1; i >= 0; i--) { res[i] *= suffix; suffix *= a[i]; }
return res;
}
6Longest Consecutive Sequence
Put every number in a HashSet. Only start counting from numbers where n-1 is not in the set (the start of a run). O(n).
int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int n : nums) set.add(n);
int best = 0;
for (int n : set) {
if (!set.contains(n - 1)) {
int len = 1;
while (set.contains(n + len)) len++;
best = Math.max(best, len);
}
}
return best;
}
7Subarray Sum Equals K
Prefix sum + HashMap of prefixSum → count. If sum - k was seen before, every one of those starting points gives a subarray summing to k. O(n). (Sliding window doesn't work here because of negative numbers.)
int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>(); map.put(0, 1);
int sum = 0, count = 0;
for (int n : nums) {
sum += n;
count += map.getOrDefault(sum - k, 0);
map.merge(sum, 1, Integer::sum);
}
return count;
}
8Maximum Subarray — Kadane's Algorithm (+ variations)
At each index, either extend the current subarray or start fresh. O(n).
int maxSubArray(int[] a) {
int cur = a[0], best = a[0];
for (int i = 1; i < a.length; i++) {
cur = Math.max(a[i], cur + a[i]);
best = Math.max(best, cur);
}
return best;
}
Variations:
- Return the indices: track
startwhenever you reset, and savestart/endwhenbestupdates. - Maximum product subarray: track both the max and the min (a negative times a negative flips the sign).
- Circular array:
max(normalKadane, total - minSubarray). Handle the all-negative case separately.
9Merge Intervals
Sort by start. If the current interval overlaps the last merged one, extend it. O(n log n).
int[][] merge(int[][] in) {
Arrays.sort(in, Comparator.comparingInt(a -> a[0]));
List<int[]> res = new ArrayList<>();
for (int[] cur : in) {
if (res.isEmpty() || res.get(res.size() - 1)[1] < cur[0]) res.add(cur);
else res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], cur[1]);
}
return res.toArray(new int[0][]);
}
10Insert Interval
Three phases: add the intervals that end before the new one, merge the overlapping ones, then add the rest. O(n).
int[][] insert(int[][] in, int[] nw) {
List<int[]> res = new ArrayList<>(); int i = 0, n = in.length;
while (i < n && in[i][1] < nw[0]) res.add(in[i++]);
while (i < n && in[i][0] <= nw[1]) {
nw[0] = Math.min(nw[0], in[i][0]); nw[1] = Math.max(nw[1], in[i][1]); i++;
}
res.add(nw);
while (i < n) res.add(in[i++]);
return res.toArray(new int[0][]);
}
11Trapping Rain Water (two pointer + stack)
Two pointers, O(n), O(1): move whichever side has the smaller max. Water at that position = sideMax − height.
int trap(int[] h) {
int l = 0, r = h.length - 1, lMax = 0, rMax = 0, water = 0;
while (l < r) {
if (h[l] < h[r]) { lMax = Math.max(lMax, h[l]); water += lMax - h[l]; l++; }
else { rMax = Math.max(rMax, h[r]); water += rMax - h[r]; r--; }
}
return water;
}
Stack approach: keep a stack of indices with decreasing heights. When a taller bar arrives, pop the bottom. Water = (min(left, current) − bottom) × width.
12Longest Substring Without Repeating Characters
Sliding window with a HashSet (or a last-seen index map). O(n).
int lengthOfLongestSubstring(String s) {
Map<Character, Integer> last = new HashMap<>();
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (last.containsKey(c) && last.get(c) >= left) left = last.get(c) + 1;
last.put(c, right);
best = Math.max(best, right - left + 1);
}
return best; // "abcabcbb" -> 3
}
13Minimum Window Substring ⭐
Expand right until the window covers every character of t, then shrink left while it still covers them, recording the minimum. O(m+n).
String minWindow(String s, String t) {
int[] need = new int[128];
for (char c : t.toCharArray()) need[c]++;
int missing = t.length(), left = 0, start = 0, minLen = Integer.MAX_VALUE;
for (int right = 0; right < s.length(); right++) {
if (need[s.charAt(right)]-- > 0) missing--;
while (missing == 0) {
if (right - left + 1 < minLen) { minLen = right - left + 1; start = left; }
if (++need[s.charAt(left++)] > 0) missing++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
14Longest Repeating Character Replacement
The window is valid while windowLen - maxFreq <= k. maxFreq never has to decrease. O(n).
int characterReplacement(String s, int k) {
int[] cnt = new int[26]; int left = 0, maxF = 0, best = 0;
for (int r = 0; r < s.length(); r++) {
maxF = Math.max(maxF, ++cnt[s.charAt(r) - 'A']);
while (r - left + 1 - maxF > k) cnt[s.charAt(left++) - 'A']--;
best = Math.max(best, r - left + 1);
}
return best;
}
15Maximum Sum Subarray of Size K
Fixed-size sliding window: add the incoming element, subtract the outgoing one.
int maxSumK(int[] a, int k) {
int sum = 0;
for (int i = 0; i < k; i++) sum += a[i];
int best = sum;
for (int i = k; i < a.length; i++) { sum += a[i] - a[i - k]; best = Math.max(best, sum); }
return best;
}
DSA — Strings & Stack
1Valid Anagram
Count characters in an int[26]: increment for s, decrement for t, then check every count is 0. O(n).
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] c = new int[26];
for (int i = 0; i < s.length(); i++) { c[s.charAt(i) - 'a']++; c[t.charAt(i) - 'a']--; }
for (int x : c) if (x != 0) return false;
return true;
}
2Group Anagrams
The key is the sorted characters (or a char-count signature).
List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] c = s.toCharArray(); Arrays.sort(c);
map.computeIfAbsent(new String(c), k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
3Longest Palindromic Substring
Expand around each center (2n−1 centers). O(n²), O(1).
String longestPalindrome(String s) {
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len = Math.max(expand(s, i, i), expand(s, i, i + 1));
if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; }
}
return s.substring(start, end + 1);
}
int expand(String s, int l, int r) {
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { l--; r++; }
return r - l - 1;
}
4Palindromic Substrings (count) — e.g. "abbc" → 5
Same expand-around-center idea, but count every expansion that succeeds. "abbc" → a, b, b, c, bb = 5.
int countSubstrings(String s) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
count += countExpand(s, i, i); // odd length
count += countExpand(s, i, i + 1); // even length
}
return count;
}
int countExpand(String s, int l, int r) {
int c = 0;
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { c++; l--; r++; }
return c;
}
5String Compression ("aabcccccaaa" → "a2b1c5a3")
Walk through runs of equal characters. The LeetCode in-place version writes the chars and counts back into the array and returns the new length.
String compress(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); ) {
int j = i;
while (j < s.length() && s.charAt(j) == s.charAt(i)) j++;
sb.append(s.charAt(i)).append(j - i);
i = j;
}
return sb.length() < s.length() ? sb.toString() : s;
}
// LeetCode 443 in-place variant
int compress(char[] chars) {
int write = 0, i = 0;
while (i < chars.length) {
char c = chars[i]; int j = i;
while (j < chars.length && chars[j] == c) j++;
chars[write++] = c;
if (j - i > 1) for (char d : String.valueOf(j - i).toCharArray()) chars[write++] = d;
i = j;
}
return write;
}
6Decode String ("3[a2[c]]" → "accaccacc")
Use two stacks: one for counts, one for the partial strings built before each '['.
String decodeString(String s) {
Deque<Integer> counts = new ArrayDeque<>();
Deque<StringBuilder> strs = new ArrayDeque<>();
StringBuilder cur = new StringBuilder(); int k = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) k = k * 10 + (c - '0');
else if (c == '[') { counts.push(k); strs.push(cur); cur = new StringBuilder(); k = 0; }
else if (c == ']') {
StringBuilder prev = strs.pop(); int n = counts.pop();
prev.append(cur.toString().repeat(n)); cur = prev;
} else cur.append(c);
}
return cur.toString();
}
7Valid Parentheses
Push opening brackets onto a stack. On a closing bracket, the top must be its match.
boolean isValid(String s) {
Deque<Character> st = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') st.push(')');
else if (c == '{') st.push('}');
else if (c == '[') st.push(']');
else if (st.isEmpty() || st.pop() != c) return false;
}
return st.isEmpty();
}
8Custom Expression Validation — {, } and * (wildcard = {, } or empty)
Greedy: track the range of possible open counts [lo, hi]. O(n), O(1).
boolean checkValid(String s) {
int lo = 0, hi = 0;
for (char c : s.toCharArray()) {
if (c == '{') { lo++; hi++; }
else if (c == '}') { lo--; hi--; }
else { lo--; hi++; } // '*' can be '}', '{' or empty
if (hi < 0) return false; // too many closes even in the best case
lo = Math.max(lo, 0);
}
return lo == 0;
}
9Min Stack
Each stack entry stores the pair (value, min so far). Every operation is O(1).
class MinStack {
private final Deque<int[]> st = new ArrayDeque<>();
public void push(int x) { st.push(new int[]{x, st.isEmpty() ? x : Math.min(x, st.peek()[1])}); }
public void pop() { st.pop(); }
public int top() { return st.peek()[0]; }
public int getMin() { return st.peek()[1]; }
}
10Daily Temperatures
Monotonic decreasing stack of indices. When a warmer day arrives, pop and fill in the answers. O(n).
int[] dailyTemperatures(int[] t) {
int[] res = new int[t.length]; Deque<Integer> st = new ArrayDeque<>();
for (int i = 0; i < t.length; i++) {
while (!st.isEmpty() && t[i] > t[st.peek()]) { int j = st.pop(); res[j] = i - j; }
st.push(i);
}
return res;
}
11Largest Rectangle in Histogram ⭐
Monotonic increasing stack. When you pop a bar, its width runs from the new stack top + 1 to i − 1. O(n).
int largestRectangleArea(int[] h) {
Deque<Integer> st = new ArrayDeque<>(); int max = 0;
for (int i = 0; i <= h.length; i++) {
int cur = (i == h.length) ? 0 : h[i];
while (!st.isEmpty() && cur < h[st.peek()]) {
int height = h[st.pop()];
int width = st.isEmpty() ? i : i - st.peek() - 1;
max = Math.max(max, height * width);
}
st.push(i);
}
return max;
}
DSA — Linked List
1Reverse Linked List
ListNode reverse(ListNode head) {
ListNode prev = null, cur = head;
while (cur != null) { ListNode next = cur.next; cur.next = prev; prev = cur; cur = next; }
return prev;
}
2Reverse Linked List in Pairs (1→2→3→4→5 ⇒ 2→1→4→3→5)
ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0, head), prev = dummy;
while (prev.next != null && prev.next.next != null) {
ListNode a = prev.next, b = a.next;
a.next = b.next; b.next = a; prev.next = b;
prev = a;
}
return dummy.next;
}
3Merge Two Sorted Lists
ListNode merge(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), t = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { t.next = a; a = a.next; } else { t.next = b; b = b.next; }
t = t.next;
}
t.next = (a != null) ? a : b;
return dummy.next;
}
4Detect Cycle in a Singly Linked List (Floyd's)
A slow pointer moves 1 step and a fast pointer moves 2. If they ever meet, there is a cycle. To find the cycle's start: reset one pointer to head and move both 1 step at a time. They meet at the start.
ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; fast = fast.next.next;
if (slow == fast) {
slow = head;
while (slow != fast) { slow = slow.next; fast = fast.next; }
return slow; // start of cycle
}
}
return null;
}
5Remove Nth Node From End
Move the fast pointer n+1 steps ahead from a dummy node, then move both pointers together.
ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head), fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next;
while (fast != null) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}
6Merge K Sorted Lists
Min-heap of the k list heads. O(N log k).
ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>(Comparator.comparingInt(n -> n.val));
for (ListNode l : lists) if (l != null) pq.offer(l);
ListNode dummy = new ListNode(0), t = dummy;
while (!pq.isEmpty()) {
ListNode n = pq.poll(); t.next = n; t = n;
if (n.next != null) pq.offer(n.next);
}
return dummy.next;
}
7LRU Cache ⭐⭐⭐
HashMap (key → node) plus a doubly linked list (most recently used at the head). get and put are both O(1).
class LRUCache<K, V> {
private class Node { K k; V v; Node prev, next; Node(K k, V v) { this.k = k; this.v = v; } }
private final int cap; private final Map<K, Node> map = new HashMap<>();
private final Node head = new Node(null, null), tail = new Node(null, null);
LRUCache(int cap) { this.cap = cap; head.next = tail; tail.prev = head; }
public synchronized V get(K k) {
Node n = map.get(k);
if (n == null) return null;
remove(n); addFirst(n);
return n.v;
}
public synchronized void put(K k, V v) {
Node n = map.get(k);
if (n != null) { n.v = v; remove(n); addFirst(n); return; }
if (map.size() == cap) { Node lru = tail.prev; remove(lru); map.remove(lru.k); }
n = new Node(k, v); addFirst(n); map.put(k, n);
}
private void remove(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }
private void addFirst(Node n) { n.next = head.next; n.prev = head; head.next.prev = n; head.next = n; }
}
Shortcut (state it as an alternative in the interview):
class LRU<K,V> extends LinkedHashMap<K,V> {
private final int cap;
LRU(int cap) { super(16, 0.75f, true); this.cap = cap; } // accessOrder=true
@Override protected boolean removeEldestEntry(Map.Entry<K,V> e) { return size() > cap; }
}
8LFU Cache + Pluggable LRU/LFU (user chooses the policy)
LFU: keyMap (key → node), freqMap (freq → LinkedHashSet of keys), and minFreq. When the cache is full, evict the oldest key in freqMap[minFreq]. Everything is O(1).
interface Cache<K, V> { V get(K k); void put(K k, V v); }
class LFUCache<K, V> implements Cache<K, V> {
private final int cap; private int minFreq = 0;
private final Map<K, V> vals = new HashMap<>();
private final Map<K, Integer> freq = new HashMap<>();
private final Map<Integer, LinkedHashSet<K>> buckets = new HashMap<>();
LFUCache(int cap) { this.cap = cap; }
public synchronized V get(K k) {
if (!vals.containsKey(k)) return null;
touch(k); return vals.get(k);
}
public synchronized void put(K k, V v) {
if (cap == 0) return;
if (vals.containsKey(k)) { vals.put(k, v); touch(k); return; }
if (vals.size() == cap) {
K evict = buckets.get(minFreq).iterator().next();
buckets.get(minFreq).remove(evict); vals.remove(evict); freq.remove(evict);
}
vals.put(k, v); freq.put(k, 1); minFreq = 1;
buckets.computeIfAbsent(1, x -> new LinkedHashSet<>()).add(k);
}
private void touch(K k) {
int f = freq.get(k); freq.put(k, f + 1);
buckets.get(f).remove(k);
if (f == minFreq && buckets.get(f).isEmpty()) minFreq++;
buckets.computeIfAbsent(f + 1, x -> new LinkedHashSet<>()).add(k);
}
}
enum EvictionPolicy { LRU, LFU }
class CacheFactory {
static <K, V> Cache<K, V> create(EvictionPolicy p, int cap) {
return switch (p) {
case LRU -> new LRUCacheImpl<>(cap); // LRUCache above implementing Cache
case LFU -> new LFUCache<>(cap);
};
}
}
Design talking points: Strategy pattern for the eviction policy, a factory to pick it, thread safety (synchronized, a ReadWriteLock, or striped locks), TTL support, and metrics (hit ratio).
DSA — Trees
1Binary Tree Level Order Traversal
BFS with a queue, processing one level (queue.size() nodes) at a time.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new LinkedList<>(); q.offer(root);
while (!q.isEmpty()) {
int size = q.size(); List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode n = q.poll(); level.add(n.val);
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
res.add(level);
}
return res;
}
2Lowest Common Ancestor (and edge cases)
TreeNode lca(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode l = lca(root.left, p, q), r = lca(root.right, p, q);
if (l != null && r != null) return root; // p and q are on different sides
return l != null ? l : r;
}
For a BST: if both values are smaller go left, if both are larger go right, otherwise the current node is the LCA. O(h).
Edge cases: one node is an ancestor of the other (it is the LCA); p or q not in the tree (the basic code returns a wrong node, so run a separate existence check or count the nodes found); p == q; an empty tree; a very deep or skewed tree (recursion can overflow the stack, so use an iterative version with parent pointers); duplicate values (compare node references, not values).
3Validate BST
Pass down a (min, max) range. Use long (or nullable bounds) so Integer.MIN/MAX values don't break it.
boolean isValidBST(TreeNode n) { return valid(n, Long.MIN_VALUE, Long.MAX_VALUE); }
boolean valid(TreeNode n, long lo, long hi) {
if (n == null) return true;
if (n.val <= lo || n.val >= hi) return false;
return valid(n.left, lo, n.val) && valid(n.right, n.val, hi);
}
4Diameter of Binary Tree
At each node, the diameter through it is leftHeight + rightHeight. Track the maximum globally.
int diameter = 0;
int diameterOfBinaryTree(TreeNode root) { height(root); return diameter; }
int height(TreeNode n) {
if (n == null) return 0;
int l = height(n.left), r = height(n.right);
diameter = Math.max(diameter, l + r);
return 1 + Math.max(l, r);
}
5Serialize & Deserialize Binary Tree ⭐
Preorder traversal with a "#" marker for null nodes.
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder(); ser(root, sb); return sb.toString();
}
private void ser(TreeNode n, StringBuilder sb) {
if (n == null) { sb.append("#,"); return; }
sb.append(n.val).append(','); ser(n.left, sb); ser(n.right, sb);
}
public TreeNode deserialize(String data) {
Deque<String> q = new ArrayDeque<>(Arrays.asList(data.split(",")));
return des(q);
}
private TreeNode des(Deque<String> q) {
String v = q.poll();
if (v.equals("#")) return null;
TreeNode n = new TreeNode(Integer.parseInt(v));
n.left = des(q); n.right = des(q);
return n;
}
6Implement Trie (prefix tree)
class Trie {
private static class Node { Node[] next = new Node[26]; boolean end; }
private final Node root = new Node();
public void insert(String w) {
Node n = root;
for (char c : w.toCharArray()) {
if (n.next[c - 'a'] == null) n.next[c - 'a'] = new Node();
n = n.next[c - 'a'];
}
n.end = true;
}
public boolean search(String w) { Node n = find(w); return n != null && n.end; }
public boolean startsWith(String p) { return find(p) != null; }
private Node find(String s) {
Node n = root;
for (char c : s.toCharArray()) { n = n.next[c - 'a']; if (n == null) return null; }
return n;
}
}
Used for autocomplete, spell check and Word Break. In a real system each node also stores its top-k suggestions.
DSA — Binary Search
1Search in Rotated Sorted Array
At least one half is always sorted. Check whether the target falls inside the sorted half.
int search(int[] a, int t) {
int l = 0, r = a.length - 1;
while (l <= r) {
int m = l + (r - l) / 2;
if (a[m] == t) return m;
if (a[l] <= a[m]) { // left half sorted
if (a[l] <= t && t < a[m]) r = m - 1; else l = m + 1;
} else { // right half sorted
if (a[m] < t && t <= a[r]) l = m + 1; else r = m - 1;
}
}
return -1;
}
2Find Peak Element
If a[m] < a[m+1], a peak exists to the right; otherwise it is at m or to the left. O(log n).
int findPeakElement(int[] a) {
int l = 0, r = a.length - 1;
while (l < r) { int m = (l + r) / 2; if (a[m] < a[m + 1]) l = m + 1; else r = m; }
return l;
}
3Koko Eating Bananas — "Binary Search on Answer" (capacity, days, weights)
Pattern: binary search over the answer space when feasible(x) is monotonic. The same template solves Ship Packages Within D Days (search capacity), Split Array Largest Sum and Min Days to Make Bouquets.
int minEatingSpeed(int[] piles, int h) {
int l = 1, r = Arrays.stream(piles).max().getAsInt();
while (l < r) {
int m = l + (r - l) / 2;
long hours = 0;
for (int p : piles) hours += (p + m - 1) / m; // ceil
if (hours <= h) r = m; else l = m + 1;
}
return l;
}
// Ship within D days: l = max(weights), r = sum(weights); feasible(cap) = days needed <= D
4Median of Two Sorted Arrays
Binary search the partition of the smaller array so that left half max ≤ right half min. O(log(min(m,n))).
double findMedianSortedArrays(int[] A, int[] B) {
if (A.length > B.length) return findMedianSortedArrays(B, A);
int m = A.length, n = B.length, lo = 0, hi = m;
while (lo <= hi) {
int i = (lo + hi) / 2, j = (m + n + 1) / 2 - i;
int aL = i == 0 ? Integer.MIN_VALUE : A[i - 1], aR = i == m ? Integer.MAX_VALUE : A[i];
int bL = j == 0 ? Integer.MIN_VALUE : B[j - 1], bR = j == n ? Integer.MAX_VALUE : B[j];
if (aL <= bR && bL <= aR) {
if ((m + n) % 2 == 0) return (Math.max(aL, bL) + Math.min(aR, bR)) / 2.0;
return Math.max(aL, bL);
} else if (aL > bR) hi = i - 1; else lo = i + 1;
}
throw new IllegalArgumentException();
}
DSA — Graphs
1Number of Islands ⭐
DFS (or BFS) flood fill from every unvisited '1'. O(m·n).
int numIslands(char[][] g) {
int count = 0;
for (int i = 0; i < g.length; i++)
for (int j = 0; j < g[0].length; j++)
if (g[i][j] == '1') { count++; sink(g, i, j); }
return count;
}
void sink(char[][] g, int i, int j) {
if (i < 0 || j < 0 || i >= g.length || j >= g[0].length || g[i][j] != '1') return;
g[i][j] = '0';
sink(g, i + 1, j); sink(g, i - 1, j); sink(g, i, j + 1); sink(g, i, j - 1);
}
2Clone Graph (and Copy List with Random Pointer)
Keep a HashMap from original node → clone so cycles are handled.
Map<Node, Node> seen = new HashMap<>();
Node cloneGraph(Node node) {
if (node == null) return null;
if (seen.containsKey(node)) return seen.get(node);
Node copy = new Node(node.val);
seen.put(node, copy);
for (Node nb : node.neighbors) copy.neighbors.add(cloneGraph(nb));
return copy;
}
Random pointers (list): the same map idea. Pass 1 creates the clones in the map; pass 2 sets clone.next = map.get(orig.next) and clone.random = map.get(orig.random).
3Course Schedule ⭐ / Detect Cycle in a Directed Graph
Kahn's topological sort: if fewer than n nodes get processed, there is a cycle. (Alternative: DFS with three colours, where reaching a GRAY node means a cycle.)
boolean canFinish(int n, int[][] prereq) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[] indeg = new int[n];
for (int[] p : prereq) { adj.get(p[1]).add(p[0]); indeg[p[0]]++; }
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
int done = 0;
while (!q.isEmpty()) {
int c = q.poll(); done++;
for (int nx : adj.get(c)) if (--indeg[nx] == 0) q.offer(nx);
}
return done == n; // false => cycle exists
}
4Pacific Atlantic Water Flow
Reverse the flow: run DFS from each ocean's border cells, going uphill. The answer is the cells reachable from both oceans.
List<List<Integer>> pacificAtlantic(int[][] h) {
int m = h.length, n = h[0].length;
boolean[][] pac = new boolean[m][n], atl = new boolean[m][n];
for (int i = 0; i < m; i++) { dfs(h, pac, i, 0); dfs(h, atl, i, n - 1); }
for (int j = 0; j < n; j++) { dfs(h, pac, 0, j); dfs(h, atl, m - 1, j); }
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (pac[i][j] && atl[i][j]) res.add(List.of(i, j));
return res;
}
int[][] D = {{1,0},{-1,0},{0,1},{0,-1}};
void dfs(int[][] h, boolean[][] vis, int i, int j) {
vis[i][j] = true;
for (int[] d : D) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < h.length && y < h[0].length && !vis[x][y] && h[x][y] >= h[i][j])
dfs(h, vis, x, y);
}
}
5Word Ladder (shortest transformation)
BFS where each word is a node. Neighbours are the words that differ by one letter. O(N · L · 26).
int ladderLength(String begin, String end, List<String> list) {
Set<String> dict = new HashSet<>(list);
if (!dict.contains(end)) return 0;
Queue<String> q = new LinkedList<>(); q.offer(begin);
int steps = 1;
while (!q.isEmpty()) {
for (int s = q.size(); s > 0; s--) {
char[] w = q.poll().toCharArray();
for (int i = 0; i < w.length; i++) {
char orig = w[i];
for (char c = 'a'; c <= 'z'; c++) {
w[i] = c; String nw = new String(w);
if (nw.equals(end)) return steps + 1;
if (dict.remove(nw)) q.offer(nw);
}
w[i] = orig;
}
}
steps++;
}
return 0;
}
Optimization: bidirectional BFS (expand the smaller frontier from both ends).
6Shortest Path in a Weighted Graph (Dijkstra) — and very large or changing graphs
int[] dijkstra(List<List<int[]>> adj, int src) { // adj: node -> [to, weight]
int[] dist = new int[adj.size()]; Arrays.fill(dist, Integer.MAX_VALUE); dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
pq.offer(new int[]{src, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll(); int u = cur[0];
if (cur[1] > dist[u]) continue; // stale entry
for (int[] e : adj.get(u))
if (dist[u] + e[1] < dist[e[0]]) { dist[e[0]] = dist[u] + e[1]; pq.offer(new int[]{e[0], dist[e[0]]}); }
}
return dist;
}
Complexity: O((V+E) log V). Negative weights need Bellman-Ford. Unweighted graphs only need BFS.
Very large graph: A with a heuristic (e.g. geo distance), bidirectional Dijkstra, Contraction Hierarchies (used by map engines), partitioning the graph across nodes, or graph DBs / Pregel-style processing.
Frequently changing graph: dynamic/incremental shortest-path algorithms (e.g. D Lite), recomputing only the affected regions, caching popular routes with a TTL, and accepting "good enough" approximations.
DSA — Heap, Scheduling & Greedy
1Kth Largest Element (array) / Kth Largest in a Stream
Keep a min-heap of size k; its top is the kth largest. O(n log k). (Quickselect averages O(n).)
class KthLargest {
private final PriorityQueue<Integer> pq = new PriorityQueue<>(); private final int k;
KthLargest(int k, int[] nums) { this.k = k; for (int n : nums) add(n); }
public int add(int val) {
pq.offer(val);
if (pq.size() > k) pq.poll();
return pq.peek();
}
}
2Find Median from Data Stream (Median Finder) ⭐
A max-heap holds the lower half and a min-heap holds the upper half. Keep their sizes balanced. add is O(log n); median is O(1).
class MedianFinder {
private final PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder());
private final PriorityQueue<Integer> high = new PriorityQueue<>();
public void addNum(int n) {
low.offer(n);
high.offer(low.poll());
if (high.size() > low.size()) low.offer(high.poll());
}
public double findMedian() {
return low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2.0;
}
}
3Design a Task Scheduler with Priorities
record Task(String id, int priority, long runAt, Runnable job) {}
class PriorityScheduler {
private final PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(11,
Comparator.comparingLong(Task::runAt).thenComparing(Comparator.comparingInt(Task::priority).reversed()));
private final ExecutorService workers = Executors.newFixedThreadPool(4);
private volatile boolean running = true;
public void submit(Task t) { queue.offer(t); }
public void start() {
Thread dispatcher = new Thread(() -> {
while (running) {
try {
Task t = queue.take();
long wait = t.runAt() - System.currentTimeMillis();
if (wait > 0) { queue.offer(t); Thread.sleep(Math.min(wait, 50)); continue; }
workers.submit(t.job());
} catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
}
});
dispatcher.start();
}
public void shutdown() { running = false; workers.shutdown(); }
}
Talking points: use a DelayQueue for delayed tasks; aging to prevent starvation of low-priority tasks; retries with backoff; idempotent tasks; and for distributed scheduling, persist tasks in a DB, claim them with SELECT ... FOR UPDATE SKIP LOCKED or a lease, or use Quartz cluster mode. (LeetCode "Task Scheduler" 621, with a cooldown n, has the answer max(len, (maxFreq-1)*(n+1) + countOfMaxFreq).)
4Minimum Number of Taps to Water the Garden
Turn each tap into an interval, then apply greedy jump game / interval cover. Example: N=5, ranges=[3,4,1,1,0,0] → 1 (the tap at index 1 covers [-3,5]). O(N).
int minTaps(int n, int[] ranges) {
int[] maxReach = new int[n + 1]; // furthest right reachable from each left point
for (int i = 0; i <= n; i++) {
int l = Math.max(0, i - ranges[i]), r = Math.min(n, i + ranges[i]);
maxReach[l] = Math.max(maxReach[l], r);
}
int taps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < n; i++) {
farthest = Math.max(farthest, maxReach[i]);
if (i == curEnd) { // must open another tap
if (farthest <= i) return -1; // gap that can't be covered
taps++; curEnd = farthest;
}
}
return taps;
}
// For T test cases: read T, loop, read N and the ranges array, print minTaps(N, ranges).
DSA — Dynamic Programming
1Climbing Stairs
dp[i] = dp[i-1] + dp[i-2] (Fibonacci). O(n), O(1).
int climbStairs(int n) { int a = 1, b = 1; for (int i = 2; i <= n; i++) { int c = a + b; a = b; b = c; } return b; }
2House Robber
dp[i] = max(dp[i-1], dp[i-2] + nums[i]).
int rob(int[] nums) { int prev = 0, cur = 0; for (int n : nums) { int t = Math.max(cur, prev + n); prev = cur; cur = t; } return cur; }
3Coin Change ⭐
Unbounded knapsack: dp[a] = min(dp[a - coin] + 1). O(amount × coins).
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1]; Arrays.fill(dp, amount + 1); dp[0] = 0;
for (int a = 1; a <= amount; a++)
for (int c : coins) if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
4Longest Increasing Subsequence (DP + patience sorting)
DP, O(n²): dp[i] = 1 + max(dp[j]) over j < i with a[j] < a[i].
Patience sorting, O(n log n): tails[k] is the smallest tail of any increasing subsequence of length k+1. Binary search where each element goes.
int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length]; int size = 0;
for (int x : nums) {
int l = 0, r = size;
while (l < r) { int m = (l + r) / 2; if (tails[m] < x) l = m + 1; else r = m; }
tails[l] = x;
if (l == size) size++;
}
return size;
}
5Longest Common Subsequence
If the characters match: dp[i][j] = dp[i-1][j-1] + 1; otherwise max(dp[i-1][j], dp[i][j-1]). O(m·n).
int lcs(String a, String b) {
int[][] dp = new int[a.length() + 1][b.length() + 1];
for (int i = 1; i <= a.length(); i++)
for (int j = 1; j <= b.length(); j++)
dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1) ? dp[i - 1][j - 1] + 1
: Math.max(dp[i - 1][j], dp[i][j - 1]);
return dp[a.length()][b.length()];
}
6Word Break (DP + Trie)
dp[i] is true if s[0..i) can be segmented. With a Trie, walk forward from each true position instead of building substrings.
boolean wordBreak(String s, List<String> dict) {
Set<String> set = new HashSet<>(dict);
int maxLen = dict.stream().mapToInt(String::length).max().orElse(0);
boolean[] dp = new boolean[s.length() + 1]; dp[0] = true;
for (int i = 1; i <= s.length(); i++)
for (int j = Math.max(0, i - maxLen); j < i && !dp[i]; j++)
dp[i] = dp[j] && set.contains(s.substring(j, i));
return dp[s.length()];
}
Trie version: for each i with dp[i] == true, walk the trie from s[i]. Each time a node has end == true at position j, set dp[j+1] = true. This avoids creating substrings.
Core Java & OOP
1Explain the OOP principles
- Encapsulation: hide state behind methods (private fields, getters, validation). Example:
BankAccount.withdraw()checks the balance itself instead of exposing a publicbalancefield. - Abstraction: expose what something does, not how. Example: a
PaymentGatewayinterface hides the Razorpay or Stripe details. - Inheritance: an IS-A relationship that reuses behaviour (
SavingsAccount extends Account). Prefer composition when the relationship is really HAS-A. - Polymorphism: compile-time (overloading) and runtime (overriding through dynamic dispatch). Example:
List<Shape>where each shape'sarea()resolves at runtime.
2SOLID principles with real-world examples
- S — Single Responsibility:
InvoiceServicecalculates;InvoicePrinterprints;InvoiceRepositorypersists. Each class has one reason to change. - O — Open/Closed: add a new payment type by writing a new
PaymentStrategyclass, not by adding anotherif/elsebranch to existing code. - L — Liskov Substitution: a subclass must be usable wherever its parent is. The classic violation is
Square extends Rectangle, where setting the width breaks the parent's contract. - I — Interface Segregation: split a fat
Worker {work(); eat();}intoWorkableandEatable, so that aRobotdoesn't have to implementeat(). - D — Dependency Inversion:
OrderServicedepends on aPaymentGatewayinterface, and Spring injects the concrete implementation. This makes it easy to test and to swap implementations.
3What design patterns are you familiar with?
- Creational: Singleton (Spring beans, an enum singleton), Factory (
CacheFactory,PaymentProcessorFactory), Builder (Lombok@Builder,HttpClient.newBuilder()). - Structural: Adapter (wrapping a legacy SOAP client), Decorator (
BufferedInputStream, adding logging or caching around a service), Proxy (Spring AOP,@Transactional), Facade (one API over several subsystems). - Behavioral: Strategy (eviction policies, pricing rules), Observer (Spring
ApplicationEvent, Kafka listeners), Template Method (JdbcTemplate,RestTemplate), Chain of Responsibility (Servlet filters, Spring Security filter chain), Command. - Microservice patterns: Circuit Breaker, Saga, Outbox, CQRS, API Gateway, Strangler Fig, Bulkhead, Sidecar.
Tip: for each pattern, say where you used it in a project.
4Why is String immutable? What is its internal structure?
The class is final and its internal storage (char[] in Java 8, byte[] plus a coder field in Java 9+ for Compact Strings) is private final with no mutators. Immutability gives:
- String Pool: many references can safely share one instance.
- Security: class names, file paths, URLs and DB credentials can't be changed after validation.
- Thread safety: no synchronization is needed.
- Hash caching: the
hashfield is computed once, which makes Strings fast HashMap keys. - Safe sharing: millions of references can point to one String.
5== vs .equals()
== compares references (for primitives it compares values). .equals() compares logical content when it is overridden; Object.equals falls back to ==. new String("a") == "a" is false; .equals is true. Watch out for Integer caching: Integer a = 127, b = 127; a == b is true, but with 128 it is false.
6How does the String Pool work? What happens internally with String.intern()?
String literals are interned into the String Pool, a hashtable that lives in the heap since Java 7 (it was in PermGen before). "abc" reuses the pooled instance; new String("abc") always creates a new heap object.
s.intern() checks the pool. If an equal string exists, it returns the pooled reference; otherwise it adds s to the pool and returns it. That makes new String("x").intern() == "x" true.
Use cases: reducing memory when there are many duplicate strings. The pool size is tunable with -XX:StringTableSize. G1 also offers -XX:+UseStringDeduplication.
7What happens if equals() is overridden but hashCode() is not? Why does the contract matter?
Contract: if a.equals(b) then a.hashCode() == b.hashCode() must also hold. (The reverse doesn't need to hold, because collisions are allowed.)
If you break it, two "equal" objects land in different buckets. map.put(new Emp(1), "x"); map.get(new Emp(1)) returns null, and a HashSet ends up with duplicates. Always override both, using the same fields, and make those fields immutable.
8Can we inherit overridden and overloaded methods? Can we override static methods?
- Overloaded methods are ordinary methods, so they are inherited, and a subclass can add more overloads.
- Overridden methods are inherited, and the subclass version runs at runtime through dynamic dispatch. The parent version is reachable with
super.method(). - Static methods can't be overridden, only hidden. Which one runs depends on the reference type at compile time, not on the object.
privateandfinalmethods can't be overridden either.
9Why doesn't Java support multiple inheritance (of classes)?
The diamond problem: if B and C both override A.m() and D extends both, which m() does D get? Java avoids the ambiguity for classes. Interfaces allow multiple inheritance of type. With Java 8 default methods, a conflict must be resolved explicitly: B.super.m().
10Why the final keyword?
finalvariable: assigned once (a constant or a blank-final set in the constructor). A final reference can still point to a mutable object.finalmethod: can't be overridden (fixes behaviour, e.g. template methods).finalclass: can't be extended (String,Integer), which gives security and immutability.finalfields also come with Java Memory Model guarantees: they are safely published after the constructor finishes.
11Can a constructor be synchronized?
No. synchronized isn't allowed on constructors (compile error). It isn't needed because only the thread creating the object can access it during construction. You can use a synchronized(lock){} block inside a constructor to protect shared static state.
12Checked vs unchecked exceptions
- Checked (
IOException,SQLException): subclasses ofExceptionthat aren'tRuntimeException. They must be caught or declared. Use them for recoverable conditions the caller should handle. - Unchecked (
NullPointerException,IllegalArgumentException): subclasses ofRuntimeException. They indicate programming errors. Spring prefers them, and@Transactionalrolls back only on unchecked exceptions by default. - Error (
OutOfMemoryError,StackOverflowError): JVM problems; don't catch them.
13Custom exception design
- Extend
RuntimeExceptionfor business errors (e.g.ResourceNotFoundException,InsufficientFundsException). - Include an error code and context, and keep the cause (
super(msg, cause)) so you don't lose the stack trace. - Build a hierarchy (
BusinessException→ specific exceptions) so@RestControllerAdvicecan map them to HTTP status codes. - Don't use exceptions for normal control flow. Never swallow them. Log once, at the boundary.
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String msg, Throwable cause) { super(msg, cause); this.errorCode = errorCode; }
public String getErrorCode() { return errorCode; }
}
14How does the JVM handle exceptions internally?
Every method's bytecode has an exception table (from-pc, to-pc, handler-pc, type). When a throw happens (the athrow instruction), the JVM searches the current frame's table for a matching handler. If none matches, it pops the frame and continues in the caller (stack unwinding). If nothing matches anywhere, the thread's UncaughtExceptionHandler runs and the thread dies. finally blocks are compiled into each exit path. Building the stack trace (fillInStackTrace) is the expensive part, so high-throughput code sometimes disables it for control exceptions.
15Comparable vs Comparator
Comparable<T>defines a class's natural order throughcompareToinside the class (e.g.String,Integer). There is only one natural order.Comparator<T>is an external, pluggable order throughcompare. You can define many, and they compose:Comparator.comparing(Emp::getDept).thenComparing(Emp::getSalary, Comparator.reverseOrder()).
16Optional — isPresent() vs ifPresent(); orElse() vs orElseGet() vs orElseThrow()
isPresent()returns a boolean, which leads to imperativeif/get()code (avoid it).ifPresent(Consumer)runs an action only if a value is present (functional style). Java 9 addedifPresentOrElse.orElse(x): x is always evaluated, even if a value is present. That is a problem when x is expensive, like a DB call.orElseGet(supplier): the supplier runs lazily, only when the Optional is empty. Prefer it for expensive defaults.orElseThrow()/orElseThrow(() -> new NotFoundException())throws when the Optional is empty.- Best practice: use Optional as a return type only, never as a field or parameter, and never call
get()without a check.
17Serialize and deserialize an object in Java (with code)
class Employee implements Serializable {
private static final long serialVersionUID = 1L; // version control
private String name;
private transient String password; // not serialized
Employee(String name, String password) { this.name = name; this.password = password; }
}
// Serialize
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("emp.ser"))) {
out.writeObject(new Employee("Anand", "secret"));
}
// Deserialize
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("emp.ser"))) {
Employee e = (Employee) in.readObject(); // password == null (transient)
}
Key points: serialVersionUID mismatch throws InvalidClassException; static and transient fields are skipped; the parent class must be Serializable or have a no-arg constructor; writeObject and readObject hooks let you customize the process; readResolve preserves singletons. Security: Java deserialization of untrusted data is a known RCE vector, so use ObjectInputFilter or prefer JSON (Jackson) or Protobuf in modern services.
18Explain sealed classes with a real-time example and their benefits (Java 17)
Sealed classes restrict which classes can extend them. Each permitted subclass must be final, sealed or non-sealed.
public sealed interface Payment permits CardPayment, UpiPayment, NetBanking {}
public record CardPayment(String cardNo, double amt) implements Payment {}
public record UpiPayment(String vpa, double amt) implements Payment {}
public final class NetBanking implements Payment { /* ... */ }
double fee(Payment p) {
return switch (p) { // exhaustive — no default needed
case CardPayment c -> c.amt() * 0.02;
case UpiPayment u -> 0;
case NetBanking n -> 10;
};
}
Benefits: a closed, well-modelled domain hierarchy (algebraic data types); the compiler checks that a pattern-matching switch is exhaustive (adding a new type breaks compilation wherever it isn't handled); no unexpected subclasses from other modules; and clearer API intent than final (allows none) or open (allows anything).
19Records (Java 16)
record Point(int x, int y) {} is an immutable data carrier. You get a canonical constructor, accessors, equals/hashCode/toString, and the class is implicitly final. Good for DTOs, keys and events. A compact constructor handles validation: record Point(int x, int y){ Point { if (x<0) throw ...; } }.
Java Versions — New Features (8 → 24)
1Java 8 (LTS) ⭐ — the most important upgrade
Lambdas, functional interfaces (Function, Predicate, Supplier, Consumer), the Stream API, method references, Optional, the java.time API, default and static methods in interfaces, parallel streams, CompletableFuture, and the Nashorn JS engine (later removed). Interview focus: Streams + Lambdas + Optional.
2Java 9 – 11
- 9: Module system (Project Jigsaw,
module-info.java), JShell,List.of()/Set.of()/Map.of(), StreamtakeWhile/dropWhile/iterate, private interface methods, OptionalifPresentOrElse/or/stream, Flow API (Reactive Streams), Stack-Walking API, and Compact Strings. - 10:
varlocal type inference, Application Class-Data Sharing, parallel full GC for G1. - 11 (LTS):
HttpClient(HTTP/2, async), String methodsisBlank(),lines(),strip(),repeat();Files.readString/writeString;varin lambda parameters; Flight Recorder opened up; single-filejava Hello.java.
3Java 12 – 16
- 12/14: switch expressions (preview in 12, standard in 14):
int d = switch(day){ case MON, TUE -> 1; default -> { yield 0; } }; - 13/15: text blocks (
"""), standard in 15. - 14: Helpful NullPointerExceptions (they name exactly which variable was null); records preview.
- 16: records final, pattern matching for
instanceof(if (obj instanceof String s)),Stream.toList(), strong encapsulation of JDK internals.
4Java 17 (LTS) — very important
Sealed classes final, pattern-matching switch (preview), Foreign Function & Memory API (incubator), a new macOS rendering pipeline, strong encapsulation enforced, Security Manager deprecated. Spring Boot 3 requires Java 17 or later. Interview focus: sealed classes + records + pattern matching.
5Java 19 – 21 (LTS) ⭐ — modern Java
- 19/20: virtual threads, structured concurrency and record patterns in preview; scoped values.
- 21 (LTS): virtual threads final; Sequenced Collections (
getFirst(),getLast(),reversed()); pattern-matching switch final; record patterns (if (o instanceof Point(int x, int y))); generational ZGC; string templates (preview, later removed).
6Java 22 – 24
- 22: Foreign Function & Memory API final, statements before
super()(preview), Stream Gatherers (preview), unnamed variables_. - 23: primitive patterns (preview), Markdown Javadoc, string templates removed.
- 24: Stream Gatherers final, virtual threads no longer pin on
synchronized(JEP 491), a big Loom improvement, compact object headers (experimental), and AOT class loading.
7What are virtual threads and how are they different from normal (platform) threads?
| Platform thread | Virtual thread | |
|---|---|---|
| Backed by | 1:1 OS thread | Many-to-few: JVM schedules them on carrier (ForkJoin) threads |
| Cost | ~1MB stack, expensive to create | A few KB, heap-allocated stack; millions possible |
| Blocking I/O | Blocks the OS thread | Unmounts from its carrier, which then runs other virtual threads |
| Pooling | Pool them | Never pool; create one per task |
| Best for | CPU-bound work | I/O-bound, high-concurrency work (thread-per-request) |
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> { Thread.sleep(1000); return id; }); // cheap blocking
}
}
Thread.startVirtualThread(() -> System.out.println("Hello"));
// Spring Boot 3.2+: spring.threads.virtual.enabled=true
Caveats: before Java 24, synchronized blocks pin the carrier thread (use ReentrantLock instead); ThreadLocal usage gets heavy with millions of threads (use ScopedValue); virtual threads give no speed-up for CPU-bound work; and you still need to limit downstream concurrency (e.g. with a Semaphore) to protect the DB pool.
8CompletableFuture vs virtual threads — when do you choose which?
- CompletableFuture: asynchronous, callback-style composition (
thenApply,thenCompose,allOf,exceptionally). Non-blocking, but the code is harder to read and debug (fragmented stack traces). Good for fan-out/fan-in pipelines and when you're on Java < 21. - Virtual threads: write plain blocking, sequential code that scales like async code. Readable, with normal stack traces and debugging.
- Choose virtual threads for thread-per-request I/O-bound services on Java 21+. Choose CompletableFuture when you need explicit composition of parallel tasks, timeouts (
orTimeout), or you're on an older JDK. They also combine well:CompletableFuture.supplyAsync(task, virtualThreadExecutor).
Multithreading & Concurrency
1Runnable vs Callable
| Runnable | Callable<V> |
|---|---|
void run() |
V call() throws Exception |
| No result, can't throw checked exceptions | Returns a result through Future, can throw checked exceptions |
| Use for fire-and-forget work, logging, cleanup | Use for parallel API calls, computations that need a result |
ExecutorService ex = Executors.newFixedThreadPool(2);
Future<Integer> f = ex.submit(() -> 42); // Callable
System.out.println(f.get()); // 42 (blocks)
ex.submit(() -> System.out.println("x")); // Runnable
2Thread lifecycle
NEW → start() → RUNNABLE (ready or running) → BLOCKED (waiting for a monitor lock) / WAITING (wait(), join(), LockSupport.park()) / TIMED_WAITING (sleep(ms), wait(ms)) → back to RUNNABLE → TERMINATED.
3What is a daemon thread?
A background service thread (GC, finalizer, timers). The JVM exits when only daemon threads remain; they don't keep it alive. Mark one with t.setDaemon(true) before start(). Don't use daemon threads for work that must complete (e.g. writing files), because their finally blocks may not run.
4synchronized vs ReentrantLock — which do you prefer and why?
| synchronized | ReentrantLock |
|---|---|
| Keyword; the lock is released automatically | You must unlock() in finally |
| No timeout or interrupt support | tryLock(timeout), lockInterruptibly() |
| Non-fair | Optional fairness (new ReentrantLock(true)) |
A single wait set (wait/notify) |
Multiple Conditions (notFull, notEmpty) |
| Pins virtual threads (before Java 24) | Virtual-thread friendly |
Preference: synchronized for simple critical sections (less error-prone, and the JVM optimizes it well). ReentrantLock when you need a tryLock or timeout (deadlock avoidance), interruptibility, fairness, multiple conditions, or virtual threads. Also consider ReadWriteLock or StampedLock for read-heavy workloads.
5What is the volatile keyword?
volatile guarantees visibility (a write is immediately visible to other threads because it bypasses CPU caches and goes through main memory) and ordering (no reordering across it, i.e. a happens-before edge). It does not give atomicity: count++ (read-modify-write) is still a race, so use AtomicInteger. Use cases: status flags (volatile boolean running), shutdown signals, config refresh, and double-checked locking for singletons.
class Singleton {
private static volatile Singleton instance;
static Singleton get() {
if (instance == null) synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
return instance;
}
}
6volatile vs synchronized vs ReentrantLock in a low-latency system — how do you decide by contention?
- Single writer, many readers, just a flag or reference:
volatile(cheapest, no lock). - Counters or simple state under contention: atomics (
AtomicLong) using CAS, orLongAdderunder heavy contention (it stripes cells to cut CAS retries). - Low contention, short critical section:
synchronized(biased/thin locking makes it nearly free when uncontended). - High contention, or you need a timeout, fairness or conditions:
ReentrantLock;StampedLockwith optimistic reads for read-heavy data. - Ultra low latency: avoid locks altogether: single-writer principle, lock-free queues (LMAX Disruptor), thread affinity, avoiding false sharing (
@Contended), and avoiding allocation on the hot path.
7Explain CAS (Compare-And-Swap)
An atomic CPU instruction (CMPXCHG): "if the value at this address is still the expected value, set it to the new value; otherwise fail." It is lock-free and non-blocking, and callers loop to retry. It is the basis of AtomicInteger, ConcurrentHashMap and ConcurrentLinkedQueue.
do { old = value.get(); next = old + 1; } while (!value.compareAndSet(old, next));
Problems: the ABA problem (fixed with AtomicStampedReference and a version number); spinning CPU under heavy contention (use LongAdder instead).
8What is ThreadLocal and what are its use cases?
Each thread gets its own copy of a variable (stored in the thread's ThreadLocalMap). Use cases: per-request context (user ID, correlation ID, MDC logging), Spring's transaction and security context (SecurityContextHolder), and non-thread-safe objects like SimpleDateFormat. Pitfall: in thread pools the thread is reused, so always remove() in finally. Otherwise the value leaks into the next request and leaks memory. With virtual threads, prefer ScopedValue.
9wait() vs sleep(); notify() vs notifyAll()
wait(): an Object method, called insidesynchronized; it releases the lock and waits fornotify.sleep(): a Thread method; it keeps the lock and just pauses.notify()wakes one arbitrary waiting thread.notifyAll()wakes all of them so they can re-check the condition. PrefernotifyAll(orConditions) to avoid missed signals, and always callwait()inside awhile (condition)loop because of spurious wakeups.
10Deadlock — detection and prevention
A deadlock needs all four Coffman conditions: mutual exclusion, hold-and-wait, no preemption, and circular wait.
Prevention: a global lock ordering (always lock accountA before accountB, e.g. ordered by ID); tryLock(timeout) with back-off; holding fewer locks for less time; using higher-level concurrency utilities.
Detection: jstack <pid> or a thread dump prints "Found one Java-level deadlock"; ThreadMXBean.findDeadlockedThreads(); JConsole or VisualVM; for databases, DB deadlock logs.
11Producer–Consumer problem
BlockingQueue<Integer> q = new ArrayBlockingQueue<>(10);
Runnable producer = () -> { for (int i = 0; ; i++) { try { q.put(i); } catch (InterruptedException e) { return; } } };
Runnable consumer = () -> { while (true) { try { System.out.println(q.take()); } catch (InterruptedException e) { return; } } };
Manual version with wait/notify:
class Buffer {
private final Queue<Integer> q = new LinkedList<>(); private final int cap = 10;
public synchronized void put(int v) throws InterruptedException {
while (q.size() == cap) wait();
q.add(v); notifyAll();
}
public synchronized int take() throws InterruptedException {
while (q.isEmpty()) wait();
int v = q.poll(); notifyAll(); return v;
}
}
12Implement your own thread pool
class SimpleThreadPool {
private final BlockingQueue<Runnable> queue;
private final List<Worker> workers = new ArrayList<>();
private volatile boolean shutdown = false;
SimpleThreadPool(int n, int queueSize) {
queue = new LinkedBlockingQueue<>(queueSize);
for (int i = 0; i < n; i++) { Worker w = new Worker(); workers.add(w); w.start(); }
}
public void submit(Runnable task) {
if (shutdown) throw new IllegalStateException("Pool shut down");
if (!queue.offer(task)) throw new RejectedExecutionException("Queue full");
}
public void shutdown() { shutdown = true; workers.forEach(Thread::interrupt); }
private class Worker extends Thread {
public void run() {
while (!shutdown || !queue.isEmpty()) {
try { queue.take().run(); }
catch (InterruptedException e) { if (shutdown) break; }
catch (RuntimeException e) { /* log, keep worker alive */ }
}
}
}
}
Discuss the real ThreadPoolExecutor parameters too: corePoolSize, maxPoolSize, keepAlive, the work queue, and rejection policies (Abort, CallerRuns, Discard, DiscardOldest).
13Implement a thread-safe cache in Java
class ThreadSafeCache<K, V> {
private record Entry<V>(V value, long expiresAt) {}
private final ConcurrentHashMap<K, Entry<V>> map = new ConcurrentHashMap<>();
private final long ttlMs;
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
ThreadSafeCache(long ttlMs) {
this.ttlMs = ttlMs;
cleaner.scheduleAtFixedRate(() ->
map.entrySet().removeIf(e -> e.getValue().expiresAt() < System.currentTimeMillis()),
ttlMs, ttlMs, TimeUnit.MILLISECONDS);
}
public V get(K key, Function<K, V> loader) {
Entry<V> e = map.compute(key, (k, old) ->
(old == null || old.expiresAt() < System.currentTimeMillis())
? new Entry<>(loader.apply(k), System.currentTimeMillis() + ttlMs) : old);
return e.value(); // compute is atomic per key -> no cache stampede for that key
}
public void put(K k, V v) { map.put(k, new Entry<>(v, System.currentTimeMillis() + ttlMs)); }
public void invalidate(K k) { map.remove(k); }
}
Mention the bounded version (LRU with locks, or Caffeine in production), stampede protection, and metrics.
14How do parallel streams work? Intermediate vs terminal operations
Parallel streams split the source with a Spliterator and process the chunks on the common ForkJoinPool (size = cores − 1) using work-stealing, then combine the results.
Use them for large, CPU-bound, stateless work on splittable sources (ArrayList, arrays). Avoid them for I/O or blocking calls (they starve the shared common pool), small data, LinkedList, order-dependent operations, or shared mutable state. A custom pool is possible: new ForkJoinPool(8).submit(() -> list.parallelStream()...).
Intermediate operations are lazy and return a Stream: filter, map, flatMap, distinct, sorted, peek, limit, skip. Nothing runs until a terminal operation is called.
Terminal operations trigger execution and produce a result or side effect: collect, forEach, reduce, count, findFirst, anyMatch, toList. A stream can only be consumed once. Short-circuiting operations: limit, findFirst, anyMatch.
15Can memory visibility or instruction reordering cause real production issues? How do you prevent them?
Yes. CPUs and the JIT reorder instructions, and each core caches values. Classic bugs:
- A
while(!stop)loop never exits because the JIT hoisted the read out of the loop (fix:volatile). - Double-checked locking without volatile publishes a partially constructed object.
- Lost updates on non-atomic counters.
- A config object updated field by field and read half-updated.
Prevention: use the happens-before rules of the Java Memory Model: volatile, synchronized/locks, final fields (safe publication), atomics, concurrent collections, immutable objects, and safe publication through a static final or a ConcurrentHashMap.
JVM Internals
1Describe the internal working of the JVM
- Class Loader subsystem: loading → linking (verify, prepare, resolve) → initialization.
- Runtime data areas: Heap (objects, shared), Metaspace (class metadata, native memory), Java Stacks (one per thread; frames with local variables and an operand stack), PC register, native method stack.
- Execution engine: the interpreter runs bytecode at first; the JIT compiler (C1 then C2, tiered) compiles hot methods to native code with inlining, escape analysis and loop unrolling; the Garbage Collector reclaims memory.
- JNI and native libraries.
Flow: .java → javac → .class bytecode → class loader → verify → interpret/JIT → run on the OS.
2Explain the class loader hierarchy and the class loading process
- Bootstrap loader (native code): core
java.base(java.lang.*). - Platform loader (called Extension before Java 9):
java.sql,java.xmland other modules. - Application/System loader: the classpath and your code.
- Custom loaders (Tomcat webapps, OSGi, hot reload).
Parent delegation model: a loader first asks its parent, and only loads the class itself if the parent can't. This prevents malicious replacement of java.lang.String and avoids duplicate classes. A class is identified by (class name + loader), which is why two loaders loading the same class causes ClassCastException.
Phases: Loading (read the bytecode) → Linking: Verify (bytecode safety) → Prepare (default static values) → Resolve (symbolic refs → direct refs) → Initialization (static blocks, done lazily on first active use).
3JVM memory structure (Heap, Stack, Metaspace)
- Heap: objects and arrays, shared across threads. Split into the Young generation (Eden + Survivor S0/S1) and the Old generation.
- Stack: per thread; frames hold local primitives and references.
StackOverflowErrorcomes from deep recursion. - Metaspace (replaced PermGen in Java 8): class metadata in native memory, auto-growing (cap it with
-XX:MaxMetaspaceSize). - Code cache: JIT-compiled code. Direct memory: NIO buffers.
4How does the JVM handle memory management?
Automatic allocation and GC. New objects go into Eden (fast bump-the-pointer allocation using TLABs). A Minor GC copies live objects to a Survivor space, and objects that survive N cycles (the tenuring threshold) get promoted to Old. Major/Full GC cleans the Old generation. GC finds live objects by reachability from GC roots (thread stacks, static fields, JNI refs), not by reference counting, so cycles get collected.
5What happens when you call new Object()? (allocation through the G1 GC lifecycle)
- The class is loaded, linked and initialized if it isn't already.
- Memory is allocated: usually in the thread's TLAB in an Eden region (pointer bump, no lock). Escape analysis may even scalar-replace the object and put it on the stack.
- Memory is zeroed and the object header is set (mark word with hash/lock/age bits, plus the klass pointer).
- The constructor chain runs (
<init>) and fields are initialized. - The reference is returned.
G1 lifecycle: the heap is split into equal regions (Eden, Survivor, Old, Humongous for objects ≥ half a region). A Young GC (stop-the-world, parallel) evacuates live Eden objects to Survivor regions, and objects get promoted to Old by age. When heap occupancy passes IHOP (~45%), concurrent marking runs. Then Mixed GCs collect young regions plus the old regions with the most garbage ("Garbage First") within the pause target (-XX:MaxGCPauseMillis=200). A Full GC is the fallback if evacuation fails.
6Garbage collection types (Serial, Parallel, CMS, G1, ZGC, Shenandoah)
- Serial: single-threaded, for small heaps or clients.
- Parallel: multi-threaded and throughput-focused, with longer pauses (batch jobs).
- CMS: concurrent mark-sweep, low pause, but fragments memory. Removed in Java 14.
- G1 (default since Java 9): region-based, predictable pauses, balanced.
- ZGC: sub-millisecond pauses even with TB heaps, concurrent compaction through coloured pointers and load barriers. Generational since Java 21.
- Shenandoah: Red Hat's low-pause concurrent compacting collector.
7G1 vs ZGC for a latency-sensitive financial system
- G1: pauses of roughly 10–200ms; good throughput; mature; lower CPU and memory overhead. Fine when a p99 of around 100ms is acceptable.
- ZGC: pauses under 1ms that don't depend on heap size; costs a little throughput (about 5–15%) and some extra memory and CPU for barriers. Best for strict tail latency (trading, payments under SLA) and large heaps.
- Decision factors: p99/p999 latency SLA, heap size, allocation rate, available CPU headroom, and JDK version (use generational ZGC on 21+). Always load-test with GC logs (
-Xlog:gc*). Also reduce allocation itself with object pooling and off-heap structures.
8What is the JIT compiler?
The Just-In-Time compiler turns hot bytecode (by invocation and loop counters) into optimized native code at runtime. Tiered compilation: interpreter → C1 (fast compile, basic optimizations) → C2 (aggressive optimizations: inlining, escape analysis, loop unrolling, lock elision, dead code elimination). If the assumptions behind an optimization break, the code is deoptimized. This is why Java needs warm-up before benchmarks or the first requests. GraalVM's native image does ahead-of-time compilation instead.
Collections & HashMap Deep Dive
1How does HashMap work internally?
- An array of buckets (
Node<K,V>[] table), default capacity 16, load factor 0.75. put(k, v): computehash = h ^ (h >>> 16)(spreads the high bits), get the index with(n - 1) & hash, and insert into the bucket. If the key already exists (same hash andequals), the value is replaced.- Collision: two keys in the same bucket are stored in a linked list. Since Java 8, a bucket turns into a red-black tree when it reaches 8 nodes and the table capacity is at least 64 ("treeify"), which takes the worst case from O(n) to O(log n). It goes back to a list when it shrinks to 6 or fewer.
getis O(1) on average.- One
nullkey is allowed; it always goes to bucket 0.
2What happens when two keys have the same hash? (collision handling)
Both go into the same bucket. On get, the map walks the bucket comparing hash first, then ==, then equals(). Java 7 used chaining with a linked list (head insertion); Java 8 appends at the tail and treeifies at 8 or more entries. Bad hashCode() implementations, or attackers sending crafted colliding keys (hash-flooding DoS), are the reason treeification exists.
3How does resize work internally? Why load factor 0.75?
When size > capacity × loadFactor (12 for capacity 16), the table doubles and every entry is redistributed. In Java 8 each node either stays at index i or moves to i + oldCap, depending on a single bit (hash & oldCap), so no full rehash is needed and the order is preserved. 0.75 is a time/space trade-off: lower values waste memory, higher values cause more collisions. Tip: presize the map with new HashMap<>(expected / 0.75f + 1) to avoid repeated resizes.
4Why is HashMap not thread-safe? Why does a concurrent resize break it (production impact)?
Concurrent puts can lose updates (two threads write the same bucket slot), make size wrong, and expose half-built nodes. In Java 7, a concurrent resize used head insertion, which reversed lists and could create a circular linked list. Then get() loops forever at 100% CPU, which is a famous production incident: the server hangs and thread dumps show threads stuck in HashMap.get. Java 8 fixed the loop (tail insertion), but data loss and corruption still happen. Fix: ConcurrentHashMap, never a plain HashMap shared between threads.
5HashMap vs ConcurrentHashMap
| HashMap | ConcurrentHashMap |
|---|---|
| Not thread-safe | Thread-safe, highly concurrent |
| 1 null key, null values allowed | No null keys or values |
| Fail-fast iterator | Weakly consistent (fail-safe) iterator |
| Fastest single-threaded | Lock-free reads, CAS plus per-bucket locking for writes |
| — | Atomic compute, merge, putIfAbsent |
Also compare it with Collections.synchronizedMap and Hashtable, which lock the whole map for every operation. |
6How does ConcurrentHashMap work internally? (CAS, bucket-level locking, resize under contention)
- Java 7: 16 Segments (each a ReentrantLock plus its own table), so at most 16 concurrent writers.
- Java 8+: a single
Node[]table. - Reads: lock-free, using
volatilereads of the node'svalandnextfields. - Insert into an empty bucket: CAS (no lock).
- Insert into a non-empty bucket:
synchronizedon the first node of that bucket only, so writers to different buckets never block each other. - Treeifies at 8 nodes, like HashMap.
- size:
baseCountplusCounterCell[](LongAdder style) to avoid contention. - Resize under contention: when resizing, a new table is allocated and the work is split into strides. Threads that
putduring a resize help transfer buckets (cooperative resizing). Moved buckets are marked with a ForwardingNode (hash = MOVED), so readers that hit one are redirected to the new table, and reads never block.sizeCtlcoordinates the threads.
7Why doesn't ConcurrentHashMap allow null keys or values (when HashMap does)?
Ambiguity under concurrency. If get(k) returns null, does that mean "key absent" or "key mapped to null"? In single-threaded code you could check containsKey(k), but in a concurrent map another thread can change the map between get and containsKey, so the answer is unreliable. By banning nulls, null from get() always means "not present". That avoids extra synchronization and keeps the semantics clean and lock-free. (Doug Lea's design decision.)
8Fail-fast vs fail-safe iterators
- Fail-fast (
ArrayList,HashMap): each iterator checksmodCount; a structural change during iteration (other than throughiterator.remove()) throwsConcurrentModificationException. This is best effort, not guaranteed. - Fail-safe / weakly consistent (
ConcurrentHashMap,CopyOnWriteArrayList): iterates over a snapshot (COW) or a live view that tolerates changes and never throws CME, but it may not reflect the latest updates.
9Internal working of ArrayList, LinkedList, HashSet
- ArrayList: a backed
Object[], default capacity 10 (allocated lazily on the first add); grows by 1.5× (old + old>>1) withArrays.copyOf. Random access is O(1); amortized add is O(1); inserting or removing in the middle is O(n) because of shifting. - LinkedList: a doubly linked list of nodes. Add/remove at the ends is O(1);
get(i)is O(n). It also implementsDeque. - HashSet: wraps a
HashMap<E, Object>, where the element is the key and a dummyPRESENTobject is the value. Uniqueness comes fromhashCode/equals.LinkedHashSetkeeps insertion order;TreeSetkeeps elements sorted.
10Why is ArrayList usually faster than LinkedList?
Contiguous memory means CPU cache locality and prefetching. LinkedList nodes are scattered in the heap (pointer chasing, cache misses, 24+ bytes of overhead per node, more GC). Even LinkedList's theoretical O(1) middle insert first needs an O(n) walk to find the position. Use ArrayDeque for queues and stacks instead of LinkedList.
11When do you use TreeMap?
When you need keys in sorted order or range/navigation queries: floorKey, ceilingKey, headMap, tailMap, subMap, firstKey. It is a red-black tree with O(log n) operations. Examples: a time-series lookup ("price at or before time t"), leaderboards, interval scheduling, consistent hashing rings. ConcurrentSkipListMap is the concurrent sorted alternative.
Java 8 Streams — Coding Questions
1Setup used in the employee examples
record Employee(String name, String dept, String gender, double salary) {}
List<Employee> emps = List.of(
new Employee("Ravi", "IT", "M", 90000), new Employee("Asha", "HR", "F", 60000),
new Employee("John", "IT", "M", 120000), new Employee("Meera", "Finance", "F", 85000));
21. Filter even numbers
List<Integer> evens = List.of(1,2,3,4,5,6).stream().filter(n -> n % 2 == 0).toList(); // [2, 4, 6]
32. Find the second highest number
Integer second = List.of(10, 20, 30, 40, 40).stream()
.distinct()
.sorted(Comparator.reverseOrder()) // safer than (a,b) -> b - a (overflow)
.skip(1).findFirst().orElse(null); // 30
43. Flatten a list of lists with flatMap()
List<Integer> flat = List.of(List.of(1,2), List.of(3,4), List.of(5)).stream()
.flatMap(Collection::stream).toList(); // [1, 2, 3, 4, 5]
map gives one output per input (Stream<List>); flatMap flattens each output stream into a single stream.
54. Use Optional to avoid NullPointerException
String result = Optional.ofNullable(name).map(String::toUpperCase).orElse("DEFAULT");
// chained null-safe access:
String city = Optional.ofNullable(user).map(User::getAddress).map(Address::getCity).orElse("NA");
65. Remove duplicates & 6. Find duplicates
List<Integer> unique = nums.stream().distinct().toList();
// duplicates — option A: Set.add returns false on repeats
Set<Integer> seen = new HashSet<>();
Set<Integer> dups = nums.stream().filter(n -> !seen.add(n)).collect(Collectors.toSet());
// option B (pure, no side effects — preferred in interviews):
Set<Integer> dups2 = nums.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey)
.collect(Collectors.toSet());
77. Sum, max, min and average & partition into even and odd
IntSummaryStatistics s = nums.stream().mapToInt(Integer::intValue).summaryStatistics();
s.getSum(); s.getMax(); s.getMin(); s.getAverage();
Map<Boolean, List<Integer>> parts = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0)); // {false=[1,3,5], true=[2,4,6]}
88. Filter palindrome words
List<String> pals = List.of("madam","java","level","spring").stream()
.filter(w -> w.equals(new StringBuilder(w).reverse().toString())).toList(); // [madam, level]
910–11. Sort employees by name; then by name and salary
emps.stream().sorted(Comparator.comparing(Employee::name)).toList();
emps.stream().sorted(Comparator.comparing(Employee::name)
.thenComparing(Employee::salary, Comparator.reverseOrder())).toList();
1012–13. Group employees by department; average salary per department
Map<String, List<Employee>> byDept = emps.stream().collect(Collectors.groupingBy(Employee::dept));
Map<String, Double> avg = emps.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.averagingDouble(Employee::salary)));
Map<String, Long> countPerDept = emps.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
1114. Highest and lowest paid employee in each department
Map<String, Optional<Employee>> highest = emps.stream().collect(Collectors.groupingBy(
Employee::dept, Collectors.maxBy(Comparator.comparingDouble(Employee::salary))));
// without Optional:
Map<String, Employee> lowest = emps.stream().collect(Collectors.toMap(
Employee::dept, Function.identity(),
BinaryOperator.minBy(Comparator.comparingDouble(Employee::salary))));
// Highest salary overall / Nth highest:
emps.stream().sorted(Comparator.comparingDouble(Employee::salary).reversed()).skip(n - 1).findFirst();
1215. Group by department, then by gender
Map<String, Map<String, List<Employee>>> m = emps.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.groupingBy(Employee::gender)));
Map<String, Map<String, Long>> counts = emps.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.groupingBy(Employee::gender, Collectors.counting())));
1316. Sort a Map by keys or values
Map<String, Integer> byValueDesc = map.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
// by key: .sorted(Map.Entry.comparingByKey()) — or simply new TreeMap<>(map)
Collect into a LinkedHashMap so the sorted order is kept.
1417. Count word frequency in a string
Map<String, Long> freq = Arrays.stream("java is fun and java is powerful".toLowerCase().split("\\s+"))
.collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()));
// character frequency:
Map<Character, Long> cf = str.chars().mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// first non-repeating character:
str.chars().mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() == 1).map(Map.Entry::getKey).findFirst();
1518. Group names by first letter and count them
Map<Character, Long> m = names.stream()
.collect(Collectors.groupingBy(n -> n.charAt(0), TreeMap::new, Collectors.counting()));
1619. Merge two integer arrays into a sorted array
int[] merged = IntStream.concat(Arrays.stream(a), Arrays.stream(b)).sorted().toArray();
// distinct merged: add .distinct() before sorted()
1720. Concatenate two lists of strings and remove duplicates
List<String> res = Stream.concat(l1.stream(), l2.stream()).distinct().toList();
18Lambda expressions & functional interfaces (concepts)
A lambda is an anonymous implementation of a functional interface (an interface with exactly one abstract method, optionally annotated @FunctionalInterface). The core ones: Predicate<T> (test → boolean), Function<T,R> (apply), Consumer<T> (accept), Supplier<T> (get), BiFunction, UnaryOperator. Lambdas can only capture effectively final variables. They compile to invokedynamic (no inner class file). Method references (String::length, System.out::println, ArrayList::new) are shorthand for lambdas.
Spring & Spring Boot
1What is Spring? Why use it? Spring vs Spring Boot
Spring is a framework built on IoC/DI with modules for web (MVC), data, security, AOP and transactions. It makes code loosely coupled and testable, and removes boilerplate (JdbcTemplate, declarative transactions).
Spring Boot is built on top of Spring and is opinionated: auto-configuration, starter POMs, an embedded server (Tomcat), production features (Actuator), externalized config, and a runnable fat JAR. The result is no XML, no WAR deployment, and minimal setup.
2How does Spring Boot handle dependency injection internally? How does autowiring work?
- Component scanning finds
@Component/@Service/@Repository/@Controllerclasses and registers BeanDefinitions. BeanFactoryPostProcessors (e.g.ConfigurationClassPostProcessor) process@Configurationand@Beanmethods.- The
ApplicationContextcreates singletons eagerly: it instantiates each one (choosing a constructor), then injects dependencies.AutowiredAnnotationBeanPostProcessorresolves@Autowiredby type, then by@Qualifier/@Primary/ parameter name. - Constructor injection (recommended) allows immutable
finalfields, makes it obvious which dependencies are required, and is easy to test. Field injection hides dependencies. - Circular dependencies: constructor injection fails fast (and Boot 2.6+ forbids cycles by default). Fix them by redesigning, or with
@Lazy.
3What happens if two beans of the same type exist without @Qualifier?
Startup fails with NoUniqueBeanDefinitionException: expected single matching bean but found 2. Fixes: mark one bean @Primary; use @Qualifier("name"); name the parameter after the bean name; or inject List<PaymentService> / Map<String, PaymentService> to get all of them (the strategy pattern).
4Lifecycle of a Spring Bean
Instantiate → populate properties (DI) → BeanNameAware / BeanFactoryAware / ApplicationContextAware → BeanPostProcessor.postProcessBeforeInitialization → @PostConstruct → InitializingBean.afterPropertiesSet() → custom init-method → postProcessAfterInitialization (AOP proxies are created here) → bean ready → on shutdown: @PreDestroy → DisposableBean.destroy() → destroy-method.
5Bean scopes & singleton bean management
- singleton (default): one instance per ApplicationContext, kept in the
DefaultSingletonBeanRegistrycache (a three-level cache that also resolves circular references). It must be stateless or thread-safe because all requests share it. - prototype: a new instance on every lookup; Spring doesn't manage its destruction.
- Web scopes: request, session, application, websocket.
- Pitfall: injecting a prototype into a singleton gives you only one instance. Use
ObjectProvider<T>,@Lookup, or a scoped proxy.
6@Component vs @Service vs @Repository vs @Controller; @RestController vs @Controller
All are stereotypes of @Component, so all are picked up by component scanning.
@Repositoryalso enables persistence exception translation: a post-processor converts SQLException and JPA exceptions into Spring'sDataAccessExceptionhierarchy.@Service: business layer (semantic only).@Controller: an MVC controller whose methods return view names by default.@RestController=@Controller+@ResponseBody: return values are written straight to the response body through HttpMessageConverters (Jackson → JSON) instead of being resolved as views.
7@ComponentScan vs @SpringBootApplication
@SpringBootApplication = @SpringBootConfiguration (@Configuration) + @EnableAutoConfiguration + @ComponentScan (scans the main class's package and its sub-packages). @ComponentScan alone just scans for components, with no auto-configuration. Classes outside the base package need scanBasePackages.
8How does Spring Boot auto-configuration work? How does it decide what to apply?
@EnableAutoConfiguration imports AutoConfigurationImportSelector. It reads the candidate classes from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Boot 3; before that it was spring.factories, loaded through SpringFactoriesLoader). Each auto-config class is guarded by conditions:
@ConditionalOnClass(DataSource.class): the library is on the classpath.@ConditionalOnMissingBean: back off if the user defined their own bean. This is how your configuration overrides Boot's defaults.@ConditionalOnProperty,@ConditionalOnWebApplication,@ConditionalOnBean.
Debug it with --debug or /actuator/conditions (the Conditions Evaluation Report).
9What is @ConditionalOnMissingBean?
It registers the bean only if no bean of that type (or name) exists yet. Auto-configs use it so that user-defined beans win: if you declare your own ObjectMapper or RestTemplate, Boot's default is skipped. Note that ordering matters: it only works reliably in auto-configuration classes, which are processed after user configuration.
10What is the role of SpringFactoriesLoader?
It is Spring's internal service-loader mechanism. It reads META-INF/spring.factories from every JAR on the classpath and instantiates the listed implementations for a given interface: ApplicationContextInitializer, ApplicationListener, EnvironmentPostProcessor, FailureAnalyzer, and (before Boot 2.7/3) EnableAutoConfiguration. This is what lets starters plug in behaviour just by being on the classpath.
11What happens internally when you add spring-boot-starter-web?
The starter is a POM that brings in spring-web, spring-webmvc, Jackson, spring-boot-starter-tomcat and validation. On startup, auto-configs see these classes on the classpath and configure: ServletWebServerFactoryAutoConfiguration (embedded Tomcat), DispatcherServletAutoConfiguration, WebMvcAutoConfiguration (message converters, resolvers, static resources), JacksonAutoConfiguration, ErrorMvcAutoConfiguration (/error), and HttpEncodingAutoConfiguration. The app type becomes SERVLET, and Tomcat starts on 8080.
12How does Spring Boot detect embedded Tomcat and configure it?
ServletWebServerFactoryAutoConfiguration imports EmbeddedTomcat, which has @ConditionalOnClass({Servlet.class, Tomcat.class}), so it creates a TomcatServletWebServerFactory. During refresh(), ServletWebServerApplicationContext.onRefresh() calls createWebServer(), which builds the Tomcat instance, registers the DispatcherServlet and filters, and starts it. Customize it with server.* properties or a WebServerFactoryCustomizer. Swap it by excluding Tomcat and adding Jetty or Undertow.
13Exact startup flow of a Spring Boot application
SpringApplication.run()creates aSpringApplication: it infers the app type (SERVLET, REACTIVE or NONE) and loads initializers and listeners via SpringFactoriesLoader.- It starts the
SpringApplicationRunListeners(firesApplicationStartingEvent). - Prepares the Environment: property sources and profiles,
application.yml,EnvironmentPostProcessors. - Prints the banner. Creates the ApplicationContext (
AnnotationConfigServletWebServerApplicationContext). prepareContext: applies initializers and registers the main class as a source.refreshContext(): invokes the BeanFactoryPostProcessors (component scan,@Configurationparsing, auto-config import), registers the BeanPostProcessors,onRefresh()→ starts the embedded web server, then instantiates all non-lazy singletons.afterRefresh. FiresApplicationStartedEvent. Runs theCommandLineRunner/ApplicationRunnerbeans.ApplicationReadyEvent: the app is ready to serve traffic.
14How does Spring Boot load application.properties? Externalized configuration & property priority
Boot builds a PropertySource chain inside the Environment (ConfigDataEnvironmentPostProcessor loads the files). Priority, highest first:
- Command-line arguments (
--server.port=9090) SPRING_APPLICATION_JSON- Java system properties (
-Dserver.port) - OS environment variables (
SERVER_PORT, relaxed binding) - Profile-specific files outside the JAR, then plain files outside the JAR (
./config/,./) - Profile-specific files inside the JAR (
application-prod.yml), thenapplication.ymlinside the JAR @PropertySource, then defaults
Bind configuration with @Value or, preferably, type-safe @ConfigurationProperties. For the cloud: Spring Cloud Config, Kubernetes ConfigMaps and Secrets, Vault.
15How does Spring Boot decide server port priority?
The same chain as above: command-line --server.port > -Dserver.port > the SERVER_PORT env var > the external application-{profile}.yml > the internal properties file > the default 8080. server.port=0 picks a random port. A WebServerFactoryCustomizer that sets the port programmatically overrides properties.
16How does Spring Boot handle profile-specific configuration? (dev / test / prod)
Use application-{profile}.yml files, or a multi-document YAML file with spring.config.activate.on-profile: dev. Activate a profile with -Dspring.profiles.active=prod, SPRING_PROFILES_ACTIVE=prod, or --spring.profiles.active. @Profile("dev") makes beans conditional on a profile. Profile groups are supported (spring.profiles.group.prod=db,mq). Keep secrets out of files: password: ${DB_PASSWORD} from env vars, Vault, or Kubernetes Secrets.
spring:
application.name: order-service
---
spring.config.activate.on-profile: prod
spring.datasource.url: jdbc:mysql://prod-db:3306/orderdb
spring.datasource.password: ${DB_PASSWORD}
logging.level.root: WARN
17How does Spring Boot manage dependency versions automatically?
Through spring-boot-starter-parent, which inherits from spring-boot-dependencies, a BOM with <dependencyManagement> that pins tested-compatible versions of hundreds of libraries. You declare dependencies without versions. Override a version with a property (<jackson.version>). Without the parent POM, import the BOM in dependencyManagement (scope import). Gradle uses the dependency-management plugin or a platform().
18Why does Spring Boot prefer convention over configuration?
Sensible defaults (port 8080, application.yml, the embedded H2 DB if present, Jackson for JSON, a default component scan base) remove boilerplate and decisions. You get to production faster with fewer misconfigurations and consistency across teams, and every convention can still be overridden.
19Fat JAR vs normal JAR — internal difference
A normal JAR holds only your classes; dependencies must be on the classpath. A Spring Boot fat (executable) JAR has BOOT-INF/classes (your code), BOOT-INF/lib/*.jar (dependency JARs nested as-is), and org/springframework/boot/loader classes. The manifest's Main-Class is JarLauncher, with your class as Start-Class. JarLauncher's custom class loader reads the nested JARs (standard Java can't). Layered JARs make Docker layer caching efficient.
20How DispatcherServlet works / what happens internally when you hit a REST endpoint
- Tomcat accepts the connection and a thread from its pool handles the request.
- The filter chain runs (Spring Security, CORS, logging/MDC).
- The DispatcherServlet (front controller) asks the
HandlerMapping(RequestMappingHandlerMapping) for the matching controller method plus any interceptors. - The interceptors'
preHandlemethods run. - The
HandlerAdapterinvokes the method: argument resolvers bind@PathVariable,@RequestParamand@RequestBody(an HttpMessageConverter deserializes the JSON), and@Validtriggers validation. - The controller → service → repository (through proxies:
@Transactional, AOP). - The return value goes through
@ResponseBody→ HttpMessageConverter (Jackson) → JSON. - Exceptions are handled by
HandlerExceptionResolver→@ControllerAdvice. postHandle/afterCompletionrun, then the response is written.
21How does @Transactional work internally? Why does it fail on self-invocation?
Spring creates an AOP proxy (a JDK dynamic proxy for interfaces, otherwise a CGLIB subclass) around the bean. The TransactionInterceptor gets a transaction from the PlatformTransactionManager (binding the connection to the thread through TransactionSynchronizationManager, a ThreadLocal), calls the method, then commits, or rolls back on a RuntimeException or Error (not on checked exceptions unless rollbackFor says so).
Self-invocation pitfall: this.innerMethod() calls the real object directly, bypassing the proxy, so @Transactional on innerMethod is ignored.
Fixes: move the method to another bean; inject the bean into itself (@Lazy) or use AopContext.currentProxy(); or use TransactionTemplate.
Other pitfalls: it only works on public methods (with proxies), catching and swallowing exceptions prevents rollback, @Async methods run on another thread with no transaction, and it doesn't apply on private or final methods. Propagation (REQUIRED, REQUIRES_NEW, NESTED…) and isolation levels are configurable. Keep transactions short: no remote calls inside them.
22Exception translation in Spring Boot
Two meanings: (1) Persistence: PersistenceExceptionTranslationPostProcessor wraps @Repository beans so that vendor exceptions (SQLException, Hibernate exceptions) become DataAccessException subclasses (DuplicateKeyException, DataIntegrityViolationException), using SQL error codes. (2) Web: HandlerExceptionResolvers (ExceptionHandlerExceptionResolver for @ExceptionHandler, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver) translate exceptions into HTTP responses, and Boot's /error (BasicErrorController) is the fallback. Spring 6 adds ProblemDetail (RFC 7807).
23How do you handle exceptions globally in a REST API?
Use @RestControllerAdvice with a standard error response, specific handlers per exception, validation handling, logging with a correlation ID, the correct HTTP status codes, and no stack traces leaked to clients.
public record ErrorResponse(String message, int status, String path, String errorCode, LocalDateTime timestamp) {}
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> notFound(ResourceNotFoundException ex, HttpServletRequest req) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(
ex.getMessage(), 404, req.getRequestURI(), "RESOURCE_NOT_FOUND", LocalDateTime.now()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> validation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(Exception.class) // fallback — never leak internals
public ResponseEntity<ErrorResponse> generic(Exception ex, HttpServletRequest req) {
log.error("Unhandled error path={}", req.getRequestURI(), ex);
return ResponseEntity.status(500).body(new ErrorResponse(
"Internal Server Error", 500, req.getRequestURI(), "GENERIC_ERROR", LocalDateTime.now()));
}
}
Validation needs @Valid @RequestBody UserDTO dto together with annotations like @NotBlank and @Email on the DTO's fields.
24How does Spring Boot integrate with Actuator internally?
spring-boot-starter-actuator auto-configures endpoints (@Endpoint beans: health, info, metrics, env, beans, conditions, loggers, threaddump, heapdump, prometheus) and exposes them over HTTP under /actuator (choose which with management.endpoints.web.exposure.include). Health is built from HealthIndicator beans (db, disk, redis, kafka), aggregated into a status, and used for Kubernetes liveness and readiness probes (/actuator/health/liveness). Metrics come through Micrometer (MeterRegistry) with auto-instrumentation of HTTP, JVM, Hikari and cache metrics, exported to Prometheus, Datadog and others. Secure the endpoints, and use a separate management port.
25Common performance mistakes in Spring Boot applications
N+1 queries and EAGER fetching; long @Transactional scopes that include remote calls (they hold DB connections); a pool size that is too small or too large (Hikari); no timeouts on RestTemplate or WebClient; creating RestTemplate or ObjectMapper per request; blocking calls inside WebFlux; excessive logging (DEBUG in prod, string concatenation in log statements); no pagination (loading whole tables); missing DB indexes; no caching; huge @Scheduled jobs on every instance; open-in-view enabled (spring.jpa.open-in-view=false); using parallel streams for I/O; and not warming up the JVM.
26RestTemplate vs WebClient vs Feign — when to choose each
- RestTemplate: synchronous and blocking, simple. In maintenance mode, so use
RestClient(Spring 6.1+) for new synchronous code. - WebClient: reactive, non-blocking (it can also block via
.block()). Use it for high concurrency, streaming, parallel fan-out calls, or WebFlux apps. - OpenFeign: declarative interface clients with very little code. Integrates with Eureka load balancing, Resilience4j and interceptors (auth headers). Good for many internal service-to-service calls.
- With virtual threads, blocking clients (RestClient, Feign) scale well too.
27Microservice communication using Feign (with circuit breaker)
// pom: spring-cloud-starter-openfeign ; main class: @EnableFeignClients
@FeignClient(name = "order-service", url = "${order.service.url}") // with Eureka: @FeignClient(name = "ORDER-SERVICE")
public interface OrderClient {
@GetMapping("/orders/{id}")
Order getOrder(@PathVariable("id") String id);
}
@Service
public class UserService {
private final OrderClient client;
public UserService(OrderClient client) { this.client = client; }
@CircuitBreaker(name = "orderService", fallbackMethod = "fallback")
public Order getOrder(String id) { return client.getOrder(id); }
public Order fallback(String id, Throwable e) { return new Order("0", "Default", 0); }
}
@RestController @RequestMapping("/users")
public class UserController {
private final UserService service;
public UserController(UserService service) { this.service = service; }
@GetMapping("/order/{id}")
public Order get(@PathVariable String id) { return service.getOrder(id); }
}
Golden rule: never call a microservice without a timeout, circuit breaker, retry (with backoff), fallback and monitoring. Every remote call must be protected. (Note: the original notes were missing @PathVariable in the controller and had a typo, cleint.)
28Microservice communication using RestTemplate / RestClient
@Configuration
public class AppConfig {
@Bean @LoadBalanced // enables service-name URLs with Eureka
public RestTemplate restTemplate(RestTemplateBuilder b) {
return b.setConnectTimeout(Duration.ofSeconds(2)).setReadTimeout(Duration.ofSeconds(3)).build();
}
}
@Service
public class OrderService {
private final RestTemplate rt;
public OrderService(RestTemplate rt) { this.rt = rt; }
public UserDTO getUser(Long id) { return rt.getForObject("http://user-service/users/{id}", UserDTO.class, id); }
}
Always configure timeouts; the default RestTemplate has infinite timeouts.
29Resilience4j: Retry with exponential backoff and jitter
Use the dependency io.github.resilience4j:resilience4j-spring-boot3 plus spring-boot-starter-aop. (@EnableRetry belongs to Spring Retry, a different library, and isn't needed for Resilience4j.)
@Retry(name = "userService", fallbackMethod = "fallback")
public String callExternalService() { return client.call(); }
public String fallback(Exception e) { return "Fallback response"; }
resilience4j.retry.instances.userService:
maxAttempts: 3
waitDuration: 2s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
enableRandomizedWait: true # jitter — avoids synchronized retry storms
randomizedWaitFactor: 0.5
retryExceptions: [java.io.IOException, java.util.concurrent.TimeoutException]
ignoreExceptions: [com.app.BusinessException] # don't retry 4xx / business errors
Flow: the call fails → it is retried up to maxAttempts with backoff → if it still fails, the fallback runs. Retry only idempotent operations and transient failures.
30Resilience4j: Bulkhead (semaphore & thread-pool)
It isolates resources so that one slow dependency can't use up every thread (named after a ship's watertight compartments).
resilience4j:
bulkhead.instances.paymentService: { maxConcurrentCalls: 5, maxWaitDuration: 0 }
thread-pool-bulkhead.instances.paymentService: { coreThreadPoolSize: 2, maxThreadPoolSize: 4, queueCapacity: 10 }
@Bulkhead(name = "paymentService", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "fallbackPayment")
public CompletableFuture<String> processPayment() { return CompletableFuture.completedFuture("Payment Successful"); }
public CompletableFuture<String> fallbackPayment(Throwable t) { return CompletableFuture.completedFuture("Payment Service is busy, try later"); }
Note: the THREADPOOL type requires the method to return CompletableFuture. The SEMAPHORE type works with synchronous methods.
31What is Spring Security? How do you implement JWT authentication?
Spring Security is a filter chain (SecurityFilterChain) that handles authentication (who you are) and authorization (what you can do), plus CSRF, CORS and session management. JWT flow: (1) /login verifies the credentials (AuthenticationManager) and issues a signed JWT (claims: sub, roles, exp) plus a refresh token. (2) The client sends Authorization: Bearer <token>. (3) A OncePerRequestFilter (or Spring's OAuth2 Resource Server with jwt()) validates the signature and expiry, builds an Authentication, and puts it in the SecurityContextHolder. (4) Use @PreAuthorize("hasRole('ADMIN')") for method security. Keep it stateless: SessionCreationPolicy.STATELESS.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(c -> c.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a.requestMatchers("/auth/**").permitAll().anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
Microservices & Distributed Systems
1Why microservices over a monolith? What are the challenges?
Why: independent deployment and scaling per service, team autonomy (Conway's law), fault isolation, technology freedom, and faster releases.
Challenges: distributed-system complexity (network failures, latency), data consistency across services (no ACID transactions spanning them, so Sagas), distributed debugging (tracing), operational overhead (CI/CD, Kubernetes, monitoring), API versioning, testing across service boundaries, and duplicated data.
Honest answer: start with a modular monolith, and split when team size or scaling needs justify it.
2How do microservices communicate? Sync vs async — when to choose synchronous?
- Synchronous: REST (Feign, RestClient, WebClient) or gRPC (binary, HTTP/2, fast for internal calls). Simple request/response, but temporal coupling: if the callee is down, the call fails.
- Asynchronous: messaging through Kafka or RabbitMQ (events, commands). Loose coupling, buffering, resilience and scaling, at the cost of eventual consistency and harder debugging.
- Choose sync when the caller needs the answer right now to continue (e.g. validate a user, fetch a price, check balance before a debit), for queries, and for low-latency user-facing reads.
- Choose async for side effects and fan-out (notifications, audit, analytics), long-running work, spike buffering, and cross-service workflows (Sagas).
3What is Service Discovery and why is it needed?
In the cloud, instances scale and move, so IPs keep changing. Instances register with a registry (Eureka, Consul) and clients look them up by name. Client-side discovery: Eureka plus Spring Cloud LoadBalancer (@LoadBalanced). Server-side discovery: Kubernetes Services (DNS name → ClusterIP → kube-proxy load-balances across pods), so on Kubernetes you usually don't need Eureka.
4What is an API Gateway, why is it required, and how does it work?
A single entry point (Spring Cloud Gateway, Kong, Apigee, AWS API Gateway) that handles the cross-cutting concerns: routing, authentication (JWT validation, OAuth), rate limiting, SSL termination, CORS, request/response transformation, aggregation (BFF), logging, tracing, and canary routing.
How it works (Spring Cloud Gateway): the request comes in → route predicates match on path, host or header → a filter chain runs (pre filters: auth, rate limit, add headers) → the request is proxied to the service (through discovery and load balancing) → post filters → response. It is non-blocking (Netty/WebFlux).
5Circuit Breaker — what it is and how it works
It stops calling a failing dependency so that cascading failures and thread exhaustion don't spread.
- CLOSED: normal. Failures are counted in a sliding window (count- or time-based).
- When the failure rate (or slow-call rate) passes a threshold, e.g. 50%, it goes OPEN: calls fail fast and go straight to the fallback.
- After
waitDurationInOpenState, it moves to HALF_OPEN and lets a few trial calls through. If they succeed it goes back to CLOSED; if not, back to OPEN.
resilience4j.circuitbreaker.instances.orderService:
slidingWindowSize: 20
failureRateThreshold: 50
slowCallDurationThreshold: 2s
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
6When do you use Circuit Breaker, Retry, Timeout and Bulkhead?
- Timeout: always, on every remote call. It bounds how long a thread waits (connect timeout ~1–2s, read timeout based on the downstream p99).
- Retry: for transient failures (network blips, 503, timeouts) on idempotent operations, with exponential backoff, jitter and a small max-attempts. Never retry 4xx or business errors.
- Circuit Breaker: when a dependency is persistently failing or slow, to fail fast and give it time to recover.
- Bulkhead: to isolate resources (separate thread pools or semaphores per dependency) so one slow service can't starve the others.
- Fallback: graceful degradation (cached data, a default response, queueing the request for later).
- Order (Resilience4j aspects): Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( call ) ) ) ) ). Retries happen outside the circuit breaker, so each attempt gets recorded.
7How do you prevent cascading failures? How do you design resilient microservices?
Timeouts + circuit breakers + bulkheads + controlled retries + graceful degradation. Also: rate limiting and load shedding at the gateway, async messaging to decouple services, caching, health checks with auto-healing (Kubernetes probes), autoscaling, idempotency, no single point of failure (multi-AZ), backpressure, and chaos testing.
Principle: in microservices, failure is expected, but failure propagation should never be allowed.
8Circuit breakers are configured, but cascading failures still happen. Why?
- Timeouts are too long or missing, so threads pile up before the breaker ever opens (the breaker measures failures, and slow calls tie up threads first).
- No bulkhead: the shared Tomcat pool is exhausted by one slow dependency.
- Retry storms: retries at several layers multiply load (3 × 3 × 3 = 27×).
- The breaker's window or threshold is too lenient, or slow calls aren't counted (
slowCallRateThreshold). - Fallbacks call another overloaded service, or fallbacks are expensive themselves.
- A shared resource (DB, cache) is the real bottleneck.
- Breakers are configured per instance, so they don't help a dependency that is slow for everyone.
Fix: tight timeouts, bulkheads, retries at one layer with a retry budget, load shedding, and backpressure.
9One microservice goes down and impacts the whole system — what do you do?
Approach: isolate the failure → maintain availability → recover the service → identify the root cause → improve resilience.
- Verify: Grafana, Prometheus and logs. Is it down, unhealthy or just slow? What is the blast radius?
- Contain: the circuit breakers should be open (fail fast); fallbacks serve cached or default responses (e.g. recommendations down → show popular items); retries with backoff, not aggressive ones; timeouts so threads don't block.
- Recover: check pod status, liveness and readiness probes, HPA, recent deployments and config changes. Restart unhealthy pods, roll back a bad deployment, scale out, or shift traffic to another region.
- RCA: logs, resource usage, DB connectivity, external dependencies, thread and heap dumps.
- Improve: graceful degradation, bulkheads, health checks, alerts, and a postmortem.
10Service A → Service B suddenly shows increased latency / a downstream service is slow and affecting the system
Approach: Trace → Metrics → Logs → Dependencies → Infrastructure → Resilience checks.
- Distributed tracing (Zipkin, Jaeger, Dynatrace, Tempo): where exactly is the time spent? Network, B's code, or B's database?
- B's health: CPU, memory, GC pauses, thread pool usage, connection pool usage (exhausted pools are a classic cause).
- What changed? Recent deployments, config changes, traffic spikes.
- Deeper: slow-query logs, B's own downstream dependencies, Kafka lag, cache hit ratio (Redis evictions).
- Retry storms from A amplifying the problem.
- Protect A: timeouts, circuit breaker, bulkhead, retry with backoff and jitter, fallback; consider async processing.
- Mitigate: scale B horizontally, restart unhealthy pods, roll back, enable caching, throttle traffic.
- Document the RCA and add alerts (on p99 latency and error rate).
11How do you handle partial failures in distributed workflows? Distributed transactions — Saga vs 2PC
- 2PC (two-phase commit): a coordinator runs prepare, then commit. It gives strong consistency, but it is blocking (the coordinator is a single point of failure, and locks are held across the network), slow, and poorly supported by NoSQL and Kafka. Avoid it in microservices.
- Saga: a sequence of local transactions, each paired with a compensating action (refund, release inventory). It gives eventual consistency.
- Choreography: services react to each other's events. Decoupled, but the flow is hard to follow as it grows.
- Orchestration: a central orchestrator (a state machine, Temporal, Camunda) commands each step. Clear flow and easier error handling; preferred for complex workflows.
- Make it reliable: the Outbox pattern (write the business row and the event row in the same DB transaction, then a relay or Debezium CDC publishes to Kafka), idempotent consumers, and persisted saga state.
12When do you use the Saga pattern, and how do you handle compensation failures?
Use Sagas for long-running, multi-service business transactions (order → payment → inventory → shipping) where 2PC isn't possible.
Compensation failures: compensations must be idempotent and retryable (retry with backoff until they succeed). Persist the saga state so it survives crashes. After N failures, send it to a DLQ or manual intervention queue and alert operations. Design compensations as semantic undos (issue a refund rather than deleting the payment). Order steps so that pivot/irreversible steps come last, and use reservations or holds (reserve inventory, then confirm) to cut down on compensation.
13How do you ensure data consistency across microservices?
Database per service; Saga for business transactions; the Outbox pattern with CDC so a DB write and an event publish can't diverge; idempotent consumers (deduplicate by event ID); eventual consistency with reconciliation jobs; optimistic locking (versions) for concurrent updates; event sourcing where you need a full audit trail; and CQRS for read models. Accept that consistency is eventual, and design the UX for it (e.g. "order is processing").
14How do you design idempotent APIs? (duplicate payments due to retries or double clicks)
Root cause: no idempotency. Retries are unavoidable, so design for idempotency from the start.
- The client (or gateway) sends an
Idempotency-Keyheader (a UUID or order ID). - The server tries to insert the key into a store with status = IN_PROGRESS, protected by a unique constraint. This is atomic, so only one request wins.
- If the key already exists and is COMPLETED, return the stored response. If it is IN_PROGRESS, return 409 (or wait).
- Process the payment, then store the result (status, transaction reference, response payload, timestamp).
- Give keys a TTL (e.g. 24h).
Also: retry only on transient failures, with exponential backoff and a cap on attempts; disable the button in the UI; and deduplicate Kafka events too.
"In payment systems I always assume retries will happen, so APIs must be idempotent by design, not fixed later."
15Where do you store the idempotency key, and how do you handle concurrent requests with the same key?
Storage: a DB table (idempotency_key PK/UNIQUE, request_hash, status, response, created_at), ideally in the same transaction as the payment for atomicity; or Redis with SET key value NX EX 86400 for speed (but Redis isn't the source of truth, so use the DB for money).
Concurrency: rely on an atomic insert-if-absent. With a unique constraint, the second concurrent insert fails, and that request returns 409 "in progress" or polls for the result. Also store a request hash: the same key with a different payload means 422 (misuse). Handle crashes with a timeout on IN_PROGRESS so the request can be safely retried.
16Who should generate the idempotency key — the client or the server?
The client, because only the client knows that two requests are the "same intent" (e.g. a retry after a timeout). It generates a UUID per operation and reuses it on retries. A server-generated key can't deduplicate a retry when the client never received the first response. Alternatively, the server can derive a natural key (order ID + action) for internal calls.
17Duplicate debits still happen even after idempotency — design end-to-end idempotency (API → service → DB → messaging)
- API: an idempotency key with an atomic insert-if-absent plus a request hash, and a stored response.
- Service: a state machine (
INITIATED → PROCESSING → SUCCESS/FAILED) with only legal transitions; conditional updates (UPDATE ... SET status='PROCESSING' WHERE id=? AND status='INITIATED') or optimistic locking (@Version); a distributed lock (Redis or a DB row lock) if several instances could work on the same account. - DB: a unique constraint on
(account_id, txn_reference)in the ledger; one ACID transaction for debit plus ledger entry plus outbox row. - Messaging: Outbox + CDC (no dual writes); consumers keep a
processed_eventstable (unique event ID) written in the same transaction as the side effect; Kafka idempotent producer / exactly-once semantics within Kafka. - External PSP calls: pass the idempotency key on to the payment provider (Stripe and Razorpay support this); on a timeout, query the status before retrying instead of retrying blindly.
- Reconciliation: a daily job compares the ledger with the bank/PSP statements and alerts on mismatches.
18Your service receives the same request twice because of a network retry — how do you guarantee consistent results?
Idempotency keys with a stored response (return the same result); naturally idempotent operations (PUT/set-to-value instead of increment); conditional updates or optimistic locking; unique constraints; deduplication tables on the consumer side. GET, PUT and DELETE are idempotent by HTTP spec; POST needs an idempotency key.
19How do you implement distributed locking?
- Redis:
SET lock:key <uuid> NX PX 30000; release with a Lua script that deletes only if the value matches (so you never release someone else's lock). Use Redisson (RLockwith a watchdog that auto-extends the lease). Redlock across multiple nodes is debated. - DB:
SELECT ... FOR UPDATE, or a lock table with a unique key; PostgreSQL advisory locks. - ZooKeeper/etcd: ephemeral sequential nodes, which give strong guarantees.
- ShedLock for
@Scheduledjobs. - Important: a lock with a TTL can expire while the holder is paused (e.g. by GC), so use fencing tokens (a monotonically increasing number checked by the storage layer). Prefer avoiding locks altogether with idempotency, optimistic locking, or partitioning (a single writer per key).
RLock lock = redisson.getLock("seat:" + seatId);
if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {
try { bookSeat(seatId); } finally { lock.unlock(); }
}
20A scheduled job starts running multiple times after scaling — how do you fix it?
Every replica runs its own @Scheduled. Fixes: ShedLock (a DB- or Redis-based lock: @SchedulerLock(name="job", lockAtMostFor="10m")); Quartz in clustered JDBC mode; a Kubernetes CronJob (concurrencyPolicy: Forbid) instead of in-app scheduling; leader election (Spring Integration or the Kubernetes lease API); and make the job idempotent anyway.
21API versioning and backward compatibility (version maintenance)
Principle: backward compatibility first → version only when needed → support multiple versions → deprecate gradually.
- URI versioning:
/api/v1/orders→/api/v2/orders. Simple, explicit, and easy to route at the gateway. - Header or media-type versioning:
Accept: application/vnd.app.v2+json. - Expand and contract: add new optional fields → deploy → migrate consumers → remove old fields later. Never rename or remove fields in place.
- Events: Schema Registry with backward-compatible Avro/Protobuf (add fields with defaults); versioned topics only for breaking changes.
- Consumer-driven contract tests (Pact, Spring Cloud Contract); a tolerant reader (ignore unknown fields); deprecation headers and timelines.
22How do you trace a request across multiple microservices? Logs exist everywhere but tracing is still hard — why?
Use distributed tracing with OpenTelemetry or Micrometer Tracing (formerly Sleuth): a traceId and spanId are propagated through W3C traceparent headers and Kafka message headers, then exported to Jaeger, Zipkin or Tempo. Put the traceId into the logging MDC so every log line carries it, and centralize logs in ELK or Loki.
Why tracing still fails: context isn't propagated across async boundaries (thread pools, @Async, CompletableFuture, Kafka) without context-propagating executors; a manually built HTTP client skips instrumentation; sampling drops the traces you needed; services use different trace formats; or the trace collector is overloaded and drops spans during incidents (so give it buffering and capacity).
23How do you secure microservices communication?
- North-south (external): an API Gateway with OAuth2/OIDC (an authorization server like Keycloak or Okta), JWT validation, rate limiting, WAF, TLS.
- East-west (internal): mTLS (both sides present certificates), usually automated by a service mesh (Istio, Linkerd) with certificate rotation; service-to-service tokens (OAuth2 client credentials) or propagated user JWTs; Kubernetes NetworkPolicies (zero trust).
- Secrets in Vault or Kubernetes Secrets (never in code); least-privilege service accounts; input validation; audit logs; OWASP practices.
24OAuth2 vs JWT; token expiration, refresh and JWT invalidation
- OAuth2 is an authorization framework/protocol (grant flows: Authorization Code + PKCE, Client Credentials for service-to-service). JWT is a token format (header.payload.signature, self-contained, signed). OAuth2 often uses JWTs as access tokens. OIDC adds identity (the ID token).
- Expiration: short-lived access tokens (5–15 minutes, the
expclaim) plus long-lived refresh tokens (stored securely, e.g. in an HttpOnly cookie). - Refresh workflow: the access token expires → the API returns 401 → the client calls
/tokenwith the refresh token → gets a new access token. Use refresh token rotation: each refresh issues a new refresh token and invalidates the old one, so reuse of an old one signals theft. - Invalidation (JWTs are stateless): short expiry; a denylist of revoked
jtivalues in Redis (TTL = remaining lifetime); a per-user token version (tokenVersionclaim checked against the DB); signing-key rotation; or opaque tokens with introspection for instant revocation. - Managing token refresh between services: cache the client-credentials token and refresh it shortly before expiry (Spring's
OAuth2AuthorizedClientManagerdoes this automatically); add it with a Feign or WebClient interceptor.
25Rate limiting / API throttling — how do you design it (millions of users, microservices)?
Algorithms:
- Token bucket: tokens refill at rate r up to capacity b; each request takes a token. Allows bursts. Most common.
- Leaky bucket: smooth, constant outflow.
- Fixed window counter: simple, but allows a burst at the window boundary.
- Sliding window log/counter: accurate.
Distributed design: enforce at the API Gateway (Spring Cloud Gateway RequestRateLimiter with Redis). State lives in Redis with atomic Lua scripts (INCR plus EXPIRE, or the token bucket math), keyed by user ID, API key or IP plus endpoint. Return 429 Too Many Requests with Retry-After and X-RateLimit-Remaining headers. Use tiered limits per plan. For millions of users: shard Redis by key, keep a local in-memory pre-limiter to cut Redis calls, and allow approximate counts. Fail-open vs fail-closed if Redis is down is a business decision.
26Rate limiter — LLD (max 5 requests per user per minute)
public class TokenBucketRateLimiter {
private static class Bucket {
double tokens; long lastRefill;
Bucket(double cap) { tokens = cap; lastRefill = System.nanoTime(); }
}
private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
private final double capacity, refillPerNano;
public TokenBucketRateLimiter(int capacity, Duration period) {
this.capacity = capacity;
this.refillPerNano = capacity / (double) period.toNanos();
}
public boolean allow(String userId) {
Bucket b = buckets.computeIfAbsent(userId, k -> new Bucket(capacity));
synchronized (b) { // per-user lock, not global
long now = System.nanoTime();
b.tokens = Math.min(capacity, b.tokens + (now - b.lastRefill) * refillPerNano);
b.lastRefill = now;
if (b.tokens >= 1) { b.tokens -= 1; return true; }
return false;
}
}
}
// new TokenBucketRateLimiter(5, Duration.ofMinutes(1)).allow("user-42")
Sliding window log alternative: keep a Deque<Long> of timestamps per user; drop entries older than 60s; allow if size < 5. For distributed limiting, keep the same logic in Redis (a sorted set or a Lua script). Talking points: a Strategy interface for the algorithm, eviction of idle buckets, and handling clock skew.
27How do you implement feature flags?
A flag service (LaunchDarkly, Unleash, Flagsmith, or Spring Cloud Config / a DB table plus a cache) evaluated at runtime: if (flags.isEnabled("new-checkout", user)). Supports percentage rollouts, user/segment targeting, kill switches, and A/B tests. Cache flags locally with a refresh (polling or streaming). Use @ConditionalOnProperty for startup-time toggles. Clean up stale flags (tech debt). This decouples deploy from release.
28How do you scale microservices / handle 10x traffic?
- Horizontal scaling: stateless services behind a load balancer; Kubernetes HPA on CPU or custom metrics (RPS, Kafka lag); cluster autoscaler for nodes.
- Caching: Redis or a CDN for read-heavy paths; HTTP caching.
- Database: read replicas, connection-pool tuning, indexing, partitioning/sharding, CQRS.
- Async: move heavy work to Kafka consumers; queue-based load leveling.
- Efficiency: virtual threads or reactive, fewer chatty calls (batching), compression, pagination.
- Protection: rate limiting, backpressure, circuit breakers, graceful degradation.
- Validate it: load testing (Gatling, JMeter, k6) and capacity planning.
29Autoscaling adds more instances, but latency still increases — why?
The bottleneck is not the app tier: the database (more pods means more connections, so DB connection or CPU saturation, plus locks), a shared downstream service or third-party rate limits, a Redis hot key, Kafka partitions limiting consumer parallelism (consumers beyond the partition count sit idle), cold starts and JVM warm-up of new pods, an unbalanced load balancer (sticky sessions, long-lived connections), or contention on a distributed lock. Scaling a stateless tier can't fix a stateful bottleneck. Find it with tracing and saturation metrics.
30Zero-downtime deployment & graceful shutdown (in-flight requests during pod termination)
Deployment: a Kubernetes rolling update (maxUnavailable: 0, maxSurge: 1) with readiness probes so traffic only reaches ready pods; blue-green (switch traffic all at once, instant rollback); canary (1% → 10% → 100% with metrics gates, via Argo Rollouts or Istio); backward-compatible DB migrations (expand/contract with Flyway or Liquibase); and API compatibility between versions.
Graceful shutdown: Kubernetes sends SIGTERM and at the same time removes the pod from the Endpoints. Because propagation takes time, add a preStop hook (sleep 5–10) so new traffic stops arriving. Spring server.shutdown=graceful plus spring.lifecycle.timeout-per-shutdown-phase=30s stops accepting new requests and lets in-flight ones finish. Kafka consumers commit their offsets and close. terminationGracePeriodSeconds must be longer than preStop plus drain time, otherwise SIGKILL arrives. Readiness should turn false on shutdown.
31Message ordering breaks between asynchronous services — why and how do you fix it?
Kafka only guarantees order within a partition. Causes: events for the same entity sent with different or no keys (round-robin across partitions); producer retries with max.in.flight > 1 without idempotence; consumers processing a partition with multiple threads; retry topics or DLQs reprocessing events later; multiple producers. Fixes: key by entity ID (orderId); enable.idempotence=true (keeps order with in-flight ≤ 5); process each key sequentially (key-based parallelism, as in Confluent's Parallel Consumer); version or sequence numbers in events so consumers can drop stale ones; design consumers to be order-tolerant where possible.
Kafka & Messaging
1Why Kafka instead of RabbitMQ?
| Kafka | RabbitMQ |
|---|---|
| Distributed commit log; messages are retained and can be replayed | Message broker; messages are deleted once acked |
| Very high throughput (millions/sec), horizontal scaling through partitions | Lower throughput, rich routing (exchanges, topic/fanout/headers) |
| Pull-based consumers, consumer groups, ordering per partition | Push-based, per-message acks, priority queues, easy request/reply |
| Event streaming, event sourcing, CDC, analytics pipelines | Task queues, complex routing, low-latency RPC-style work |
| Choose Kafka for event-driven architecture, high volume, replay and stream processing; RabbitMQ for traditional work queues and flexible routing. |
2Kafka core concepts — producers, consumers, partitions, consumer groups, compaction, DLQs
- Topic → partitions: each partition is an ordered, immutable log; the partition is the unit of parallelism and ordering. Messages with the same key go to the same partition (
hash(key) % partitions). - Replication: each partition has a leader plus followers; the ISR (in-sync replicas) set;
min.insync.replicas=2withacks=allfor durability. - Producer:
acks(0/1/all), batching (linger.ms,batch.size), compression,enable.idempotence=true(no duplicates from retries), and transactions for exactly-once. - Consumer groups: each partition goes to exactly one consumer in a group, so max parallelism = the partition count. Different groups each get every message independently (pub-sub).
- Offsets: stored in
__consumer_offsets; auto or manual commit. - Rebalancing: happens when consumers join or leave; the cooperative sticky assignor minimizes disruption.
- Log compaction: keeps only the latest value per key (good for changelogs and state); a null value is a tombstone that deletes the key.
- Schema evolution: Schema Registry with Avro or Protobuf and compatibility modes (BACKWARD means new consumers can read old data; add fields with defaults).
- DLQ: after N failed retries, the message is published to
topic.DLTfor inspection and replay (Spring KafkaDefaultErrorHandler+DeadLetterPublishingRecoverer).
3What happens if a consumer crashes?
The broker stops getting heartbeats → after session.timeout.ms (or when max.poll.interval.ms is exceeded) → the group rebalances and the crashed consumer's partitions go to the other consumers → they resume from the last committed offset. Any messages processed but not committed get redelivered, which gives at-least-once delivery, so consumers must be idempotent.
4A consumer processes a message successfully but crashes before committing the offset — what happens?
On restart (or after the rebalance), the message is consumed again, a duplicate. Handle it with idempotent consumers:
- A
processed_eventstable with a uniqueeventId, written in the same DB transaction as the business change (if the insert fails because the row exists, skip the message). - Naturally idempotent operations (upserts, set-to-value).
- Business-key unique constraints.
- For Kafka-to-Kafka flows, exactly-once semantics (transactional producer +
isolation.level=read_committed, or Kafka Streams withprocessing.guarantee=exactly_once_v2), where the offset commit is part of the transaction.
5How do you prevent the same Kafka event from being processed twice?
- Producer side:
enable.idempotence=true(sequence numbers per partition remove duplicates caused by producer retries); the Outbox pattern so the event isn't double-published. - Consumer side: keep an eventId (UUID) in every message header or payload; check it against a dedup store (a DB unique constraint, or Redis
SETNXwith a TTL) atomically with the side effect; commit offsets manually after processing (AckMode.MANUAL/RECORD). - EOS for read-process-write within Kafka. Exactly-once processing in practice = at-least-once delivery + idempotent processing.
6How do you maintain message ordering?
Use the same key for related events (e.g. orderId), so they land in one partition and keep their order. Producer: enable.idempotence=true (safe with max.in.flight.requests.per.connection ≤ 5); without idempotence, max.in.flight=1. Consumer: process a partition sequentially, or parallelize by key. Retries: blocking retries (non-blocking retry topics break ordering). For global ordering you need a single partition, which caps throughput.
7How do you handle message failures in Kafka in a microservices setup?
- Classify errors: transient (DB down, timeout) → retry; poison or non-retryable (deserialization, validation) → DLQ immediately.
- Retries: Spring Kafka
DefaultErrorHandlerwithExponentialBackOff(blocking, preserves order), or non-blocking retry topics (@RetryableTopic→topic-retry-1s,-10s, … →topic-dlt) that don't block the partition but give up ordering. - DLQ: keep the original headers and exception info; monitor it and alert; have a replay tool.
- Handle deserialization errors with
ErrorHandlingDeserializer. - Idempotent consumers, plus a circuit breaker that pauses the consumer (
container.pause()) when a downstream system is down.
@RetryableTopic(attempts = "4", backoff = @Backoff(delay = 1000, multiplier = 2), dltTopicSuffix = ".DLT")
@KafkaListener(topics = "payments")
public void consume(PaymentEvent e) { service.process(e); }
@DltHandler
public void dlt(PaymentEvent e, @Header(KafkaHeaders.EXCEPTION_MESSAGE) String err) { alert(e, err); }
8Kafka consumers are healthy, but lag keeps increasing — why?
Consumption is slower than production: processing per message is too slow (synchronous DB or HTTP calls per record, a slow downstream, GC pauses); too few partitions (adding consumers beyond the partition count doesn't help); a hot partition (key skew); frequent rebalances (processing takes longer than max.poll.interval.ms, so the consumer gets kicked out, causing a rebalance storm); blocking retries stuck on a poison message; max.poll.records or fetch sizes too small; a traffic spike.
Fixes: batch processing (batch listeners, bulk DB writes), more partitions plus more consumers, async or parallel processing per key, fixing the slow dependency, tuning poll settings, sending poison pills to the DLQ, and scaling on lag (KEDA).
9What is idempotency in distributed systems?
An operation is idempotent if performing it multiple times has the same effect as performing it once. Networks force retries (timeouts are ambiguous: did the request succeed?), and brokers give at-least-once delivery, so every write path must tolerate duplicates. Techniques: idempotency keys, unique constraints, upserts, conditional updates, dedup tables, and versioning.
Databases, JPA, SQL & Caching
1Why JPA instead of plain JDBC?
JPA/Hibernate gives ORM mapping (entities instead of ResultSet boilerplate), Spring Data repositories (derived queries), dirty checking, first- and second-level caching, lazy loading, relationship mapping, @Version optimistic locking, database portability (dialects), and auditing. JDBC (or JdbcTemplate/jOOQ) gives full control and performance for complex reporting, bulk operations, or when ORM overhead and N+1 risks aren't worth it. Many teams use both.
2First-level vs second-level cache (Hibernate)
- L1: per
EntityManager/Session (one transaction), always on. The same entity loaded twice in a session hits the DB only once. It is also the identity map behind dirty checking. - L2: shared across sessions at the
SessionFactorylevel, optional (Ehcache, Hazelcast, Infinispan). Best for read-mostly reference data. Watch out for staleness in multi-instance deployments; a distributed provider is needed. Plus the query cache.
3What is the N+1 query problem? How do you solve it?
Loading N parents with 1 query, then lazy-loading each parent's children with N more queries (e.g. orders.forEach(o -> o.getItems().size())).
Fixes: JOIN FETCH in JPQL (select o from Order o join fetch o.items); @EntityGraph(attributePaths = "items") on the repository method; @BatchSize(size = 50) / hibernate.default_batch_fetch_size (turns N queries into N/50 IN queries); DTO projections; FetchMode.SUBSELECT. Detect it with SQL logging, Hibernate statistics, or tests that assert query counts. Note: JOIN FETCH combined with pagination on collections fetches in memory, so use batch fetching or two queries instead.
4Optimistic vs pessimistic locking (real-world example); handling concurrent updates
- Optimistic: no lock; a
@Versioncolumn.UPDATE ... SET qty=?, version=version+1 WHERE id=? AND version=?; if 0 rows change,OptimisticLockException→ retry or show an error. Best when conflicts are rare and reads dominate: e.g. editing a user profile, product catalogue updates, wiki edits. - Pessimistic:
SELECT ... FOR UPDATE(@Lock(PESSIMISTIC_WRITE)) holds a row lock until commit. Best when conflicts are frequent or costly: e.g. debiting the same bank account, the last few seats or stock items. Risks: deadlocks and lower throughput, so set lock timeouts. - Other options: atomic SQL updates (
UPDATE stock SET qty = qty - 1 WHERE id=? AND qty > 0), and serializable isolation for critical sections.
5Design a seat booking system where the same seat can't be booked by multiple users at once
- Seat hold/reservation:
seat_status (show_id, seat_id, status, held_by, hold_expires_at, version); the statuses are AVAILABLE → HELD → BOOKED. - Atomic conditional update (the core):
UPDATE seats SET status='HELD', held_by=?, hold_expires_at=now()+5min WHERE show_id=? AND seat_id IN (...) AND status='AVAILABLE'. If the updated count is less than requested, roll back and tell the user the seat is gone. This is effectively optimistic concurrency at the row level. The alternatives areSELECT ... FOR UPDATEor@Version. - Unique constraint on
bookings(show_id, seat_id)as a final safety net. - Payment within the hold window → confirm (HELD → BOOKED); otherwise a scheduled job or Redis TTL releases expired holds.
- Scale: a Redis hold with
SET seat:{show}:{seat} user NX EX 300for a fast path, with the DB as the source of truth; partition by show; a virtual waiting room for hot events; idempotency keys on booking requests.
6How do you optimize slow queries? (SQL optimization)
EXPLAIN/EXPLAIN ANALYZE: look for full table scans, filesorts, temporary tables, and bad join orders.- Indexes: on WHERE, JOIN and ORDER BY columns; composite indexes in leftmost-prefix order; covering indexes; avoid functions on indexed columns (
WHERE YEAR(created)=2024kills the index, so use a range instead); avoid leading wildcards (LIKE '%x'). - Avoid
SELECT *: fetch only the needed columns (enables covering indexes and moves less data). - Pagination: keyset/seek pagination instead of a large
OFFSET. - Rewrite subqueries as joins or EXISTS; batch inserts and updates; avoid N+1.
- Denormalize or use materialized views for heavy reads; partition large tables; archive old data.
- Keep statistics up to date (
ANALYZE); check the slow query log; read replicas for reporting; a cache layer.
7What is the purpose of a database index?
A data structure (usually a B+ tree) that speeds up lookups and range scans from O(n) to O(log n), and also helps ORDER BY and joins. Clustered index: the table rows themselves are stored in index order (the InnoDB primary key). Non-clustered/secondary index: a separate structure pointing to the rows. Trade-offs: slower writes (every index must be updated), extra storage, and low benefit on low-cardinality columns. Others: hash, bitmap, GIN (full text, JSON), partial indexes.
8SQL: number of tables and their columns; detecting duplicate rows
-- tables and column counts (MySQL / PostgreSQL / standard)
SELECT table_name, COUNT(*) AS column_count
FROM information_schema.columns
WHERE table_schema = 'mydb'
GROUP BY table_name;
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'mydb';
-- Oracle: SELECT table_name, COUNT(*) FROM user_tab_columns GROUP BY table_name;
-- duplicate rows
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
-- delete duplicates, keep lowest id
DELETE FROM users WHERE id NOT IN (
SELECT MIN(id) FROM users GROUP BY email); -- MySQL: wrap the subquery in a derived table
-- with window function
SELECT * FROM (
SELECT u.*, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) rn FROM users u) t
WHERE rn > 1;
-- Nth highest salary
SELECT DISTINCT salary FROM emp ORDER BY salary DESC LIMIT 1 OFFSET 1; -- 2nd highest
SELECT * FROM (SELECT e.*, DENSE_RANK() OVER (ORDER BY salary DESC) r FROM emp e) x WHERE r = 2;
9A SQL query takes 10 seconds in production but runs fast in dev — how do you investigate?
Data volume and distribution differ. Steps:
- Get the real execution plan in prod (
EXPLAIN ANALYZE), not the dev plan. - Compare: data size, missing or different indexes (migration drift), stale statistics (the optimizer picks a bad plan, so run ANALYZE), data skew, parameter sniffing / bind-variable peeking (a plan cached for an atypical value).
- Locks and contention: blocked by other transactions (
SHOW PROCESSLIST,pg_stat_activity, lock waits); concurrent load; buffer pool too small (cache misses, disk I/O). - Infrastructure: network latency to the DB, a noisy-neighbour replica, IOPS limits.
- App side: the ORM generates a different query in prod (N+1), large result sets, missing pagination, or the time is really spent waiting for a pool connection rather than in the query.
Fix: add or adjust indexes, rewrite the query, update stats, paginate, cache, or move to a read replica.
10The DB connection pool is exhausted in production — what could cause it?
- Long-running transactions holding connections, especially remote calls inside
@Transactional(a classic). - Connection leaks: connections not closed (manual JDBC without try-with-resources).
- Slow queries and lock waits → connections held longer.
- A traffic spike; the pool too small for the concurrency (or too large for the DB to handle).
- Open-Session-In-View holding a connection for the whole request.
- Thread pool size far larger than the pool size → many threads waiting.
- DB failover or network issues leaving dead connections.
Diagnose: Hikari metrics (active, idle, pending, timeouts), leakDetectionThreshold, thread dumps (where are threads waiting?), DB-side sessions.
Real example: "We saw pool exhaustion errors. I enabled Hikari leak detection and found a long-running transaction calling an external service. I refactored to shrink the transaction scope (making the remote call outside the transaction) and the problem went away."
11How do you handle DB connection leaks?
Use HikariCP with leakDetectionThreshold=20000 (it logs a stack trace of where the connection was borrowed); always use try-with-resources or Spring templates; keep transactions short; set maxLifetime lower than the DB's idle timeout; set connectionTimeout to fail fast; monitor hikaricp_connections_pending; set statement and query timeouts.
12What changes are needed when migrating from MySQL to Oracle?
- Dialect/driver:
ojdbcand the Hibernate Oracle dialect. - ID generation: MySQL
AUTO_INCREMENT→ Oracle sequences (or 12c+IDENTITY);@GeneratedValue(strategy=SEQUENCE). - Data types:
VARCHAR→VARCHAR2,TEXT→CLOB,DATETIME→DATE/TIMESTAMP,TINYINT(1)booleans →NUMBER(1)(Oracle had no BOOLEAN before 23c),BLOB. - SQL syntax:
LIMIT→FETCH FIRST n ROWS ONLY/ROWNUM;IFNULL→NVL;NOW()→SYSDATE/SYSTIMESTAMP;GROUP_CONCAT→LISTAGG; backticks → double quotes; Oracle's empty string = NULL behaviour. - Identifiers: case sensitivity, name length limits (30 chars before 12.2), reserved words.
- Native queries and stored procedures need rewriting (PL/SQL); transaction isolation defaults differ; migrate the data (ETL, GoldenGate); update Flyway/Liquibase scripts; run performance tests and tune indexes and stats.
13Why Redis instead of in-memory (local) caching?
A local cache (Caffeine, HashMap) is the fastest (nanoseconds), but it is per instance: with N pods there are N inconsistent copies, invalidation is hard, it is lost on restart, it is limited by heap, and it adds GC pressure. Redis is shared across instances (consistent), lives outside the JVM heap, survives app restarts (optional persistence), scales independently, and supports rich structures (sorted sets for leaderboards, TTLs, pub/sub, distributed locks, rate limiting). Costs: a network hop (~sub-ms) and an operational component. Common pattern: a two-level cache, Caffeine L1 in front of Redis L2.
14How does cache invalidation work? Solving it with multiple service instances; stale data
- Strategies: TTL expiry (a simple safety net); cache-aside (read: cache → on miss, read the DB and populate; write: update the DB, then delete the cache key; delete rather than update avoids race conditions); write-through or write-behind; event-driven invalidation.
- Multiple instances with local caches: broadcast invalidations through Redis Pub/Sub or Kafka events so every instance evicts the key; or keep only a shared Redis; or use short TTLs on the L1.
- Stale data causes: a race where a reader repopulates old data after the writer's delete (fix with delayed double delete, versioned keys, or CDC-driven invalidation using Debezium on the DB binlog); TTLs that are too long; a missed invalidation event.
- Other issues: cache stampede (many misses on one hot key, fixed with a lock or single-flight, request coalescing, or early probabilistic refresh); penetration (repeated queries for nonexistent keys, fixed with null caching or a Bloom filter); avalanche (many keys expiring together, fixed with TTL jitter).
15Redis is down — should your application go down too? How do you design the fallback?
No. A cache is an optimization, not the source of truth. Design: short Redis timeouts; a circuit breaker around Redis calls; fall back to the DB on errors (but protect the DB from the sudden load with rate limiting, a local Caffeine cache, or request coalescing); serve stale data from a local cache; turn off non-critical features (degraded mode). For Redis used as a primary store (sessions, locks, rate limits), use Redis Sentinel or Cluster with replicas for HA and decide fail-open vs fail-closed (e.g. a rate limiter usually fails open). Alert on it.
Production Scenarios & Troubleshooting
1API response time suddenly goes from 200 ms to 5 seconds (or the API becomes slow right after a deployment) — what do you check?
- Scope: all endpoints or one? All instances or one? Does it line up with a deployment or config change? (Most incidents start after a release, so compare the versions and roll back if it's clearly the cause.)
- Tracing: where is the time going? The app, the DB, a downstream call, or the network?
- App metrics: CPU, GC pause time and frequency, heap, thread pool saturation (Tomcat busy threads), connection pool pending count.
- Dependencies: DB slow queries and locks, downstream latency, cache hit ratio (did a deploy flush the cache or change keys?), Kafka lag.
- Traffic: a spike, a bot, a retry storm.
- What changed in the deploy: a new query without an index, N+1, a removed cache, a changed timeout or pool config, a new synchronous call, a logging level change, a library upgrade.
Mitigate first (roll back or scale), then do the RCA.
2CPU usage is normal, but API latency is very high — what could be happening? (thread pools exhausted while CPU stays normal)
Threads are waiting, not working. Typical causes:
- I/O waits: a slow DB or downstream service, a missing or too-long timeout.
- Connection pool exhaustion: threads waiting for a DB or HTTP connection (
HikariPool - Connection is not available). - Lock contention: synchronized blocks, DB row locks, distributed locks.
- Thread pool saturation: requests queued in Tomcat or executor queues.
- Synchronous logging to a slow appender, DNS resolution delays, GC safepoints (look at pause logs), network or TLS handshake problems, rate-limited third parties.
Diagnose: take thread dumps (jstack, or jcmd <pid> Thread.print, 3 dumps about 5s apart). Most threads in WAITING/BLOCKED at the same stack frame shows you the bottleneck. Also check pool metrics and traces.
3How do you tell whether a performance issue is in the application code, the database, network latency or a downstream service? (finding the bottleneck)
- Distributed tracing breaks the request into spans (controller, DB query, HTTP client call) and shows exactly where the time goes.
- The USE method (Utilization, Saturation, Errors) for each resource: CPU, memory/GC, threads, connection pools, disk, network.
- App code: high CPU → a profiler (async-profiler flame graphs, JFR) shows hot methods, excessive serialization or bad algorithms.
- DB: slow-query log,
pg_stat_statements, DB CPU and locks, pool wait time vs query time. - Network: compare client-side latency with server-side processing time (a big gap means network, LB or TLS);
tcpdump, retransmits, DNS. - Downstream: client metrics per dependency (Resilience4j/Micrometer
http.client.requests). - APM tools: Dynatrace, New Relic, Datadog, Elastic APM.
4An API works perfectly with 100 users but fails with 10,000 concurrent users — what do you check first?
Resource saturation and shared bottlenecks: Tomcat max threads (default 200) and the accept queue; DB connection pool (default 10 in Hikari) → timeouts; DB max connections and locks; HTTP client pool limits (per-route max connections); memory/GC (big per-request allocations); synchronized or lock contention; third-party rate limits; file descriptors and ephemeral port limits; non-scalable code (O(n²), unbounded caches); session or sticky state.
Approach: load test to reproduce (k6, Gatling); check error types (timeouts vs 5xx vs connection refused); take thread dumps under load; then tune pools, add caching, go async or use virtual threads, scale horizontally, rate limit, and optimize queries.
5Health checks pass, but users still report failures — why?
Health checks are too shallow: /health returns UP while a key dependency (DB, payment provider) is failing; or failures only affect specific users, regions, features or payloads (bad data, one shard, auth or token issues, a CDN/edge problem); errors are returned as 200 with an error body; the failing path is client-side (JS errors, mobile app version); only one pod out of many is bad; or there is an LB or DNS misrouting issue.
Fixes: meaningful readiness checks (without making liveness depend on downstream services, which causes restart loops); synthetic monitoring (scripted user journeys); SLO-based alerting on real user error rate and latency (RED metrics: Rate, Errors, Duration); RUM and client telemetry.
6APIs randomly return 500 errors without infrastructure alerts — how do you investigate?
Infrastructure metrics (CPU, memory) are fine, so the problem is application-level. Check error logs by correlation ID and trace the failing requests; group by endpoint, pod, version and customer to find a pattern (one bad pod? a specific input?); look at unhandled exceptions (NPEs on edge-case data); intermittent downstream timeouts that surface as 500s; connection resets (keep-alive mismatches between the LB idle timeout and the server timeout); race conditions; pool timeouts; a canary pod running a different version. Fix the gap: alert on the 5xx rate and error budget, not just infrastructure metrics.
7A deployment suddenly increases 4xx errors instead of 5xx — what does that tell you?
4xx means the server decided the client's request was wrong, so it's usually a contract or compatibility break: stricter validation (400), renamed or removed fields or endpoints (404), a changed path or version, auth/security config changes (401/403: a new filter, a changed scope or role mapping, CORS), content-type or serialization changes (415/406, Jackson config such as FAIL_ON_UNKNOWN_PROPERTIES), rate limiting newly enabled (429). Action: look at which endpoints and clients are affected, diff the API contract, roll back or add backward compatibility, and add contract tests.
8APIs work locally but fail in production (production-only issues) — what do you investigate? Issue happens only under heavy traffic and can't be reproduced locally?
Environment differences: config/profile and env vars, secrets, feature flags; network (firewalls, DNS, proxies, TLS certificates, mTLS); dependency versions and data (prod data volume and edge cases); resource limits (container memory/CPU limits, JVM inside a container); multiple instances (local state, caches, scheduled jobs, sticky sessions); timeouts at LBs and proxies; time zones and locales; permissions (IAM, DB grants).
Under heavy traffic only: concurrency bugs (race conditions, non-thread-safe singletons, SimpleDateFormat), pool exhaustion, GC pressure, lock contention, rate limits.
Approach: use observability rather than reproduction: logs with correlation IDs, traces, metrics around the failure window; thread and heap dumps captured during the incident; compare a good pod and a bad pod; reproduce with a prod-like load test in staging (production data shape, k6/Gatling); canary plus feature flags to isolate the change; add targeted logging; chaos testing.
9One slow database query starts affecting the whole application — why and how do you isolate it?
The slow query holds connections longer, so the shared pool drains, so every endpoint that needs the DB waits, so Tomcat threads pile up, so the whole app slows down (resource coupling). It may also hold locks that block other queries, or use up DB CPU and I/O.
Fix: optimize the query (index, rewrite); statement timeouts (jakarta.persistence.query.timeout, DB-side statement_timeout); a separate connection pool (bulkhead) for heavy or reporting queries; move reporting to a read replica; caching; pagination; async processing of heavy work.
10The cache improves performance at first, then starts returning stale data
TTL too long or no invalidation; a race in cache-aside (a read repopulates old data after a write's delete); multiple instances with local caches and no invalidation broadcast; writes that bypass the cache path (batch jobs or other services writing straight to the DB); replica lag (the cache is filled from a lagging read replica). Fixes: delete-on-write plus TTL with jitter, delayed double delete, CDC-based invalidation (Debezium), versioned keys, pub/sub invalidation for L1 caches, and reading from the primary right after writes.
11Your Kubernetes pod keeps restarting, but application logs show no clear error — how do you debug it?
kubectl describe pod <pod>: check Last State / Reason: OOMKilled (exit 137, container memory limit exceeded), liveness probe failed, Error (exit 1), or a Completed loop.kubectl logs <pod> --previous: logs from the crashed container.kubectl get events --sort-by=.lastTimestamp.- OOMKilled: the JVM heap plus non-heap memory (metaspace, threads, direct buffers) is over the container limit. Use
-XX:MaxRAMPercentage=75(not a fixed -Xmx larger than the limit) and size the limit with headroom. The JVM doesn't log this because the kernel kills it. - Liveness probe failing: the app is slow to start (add a
startupProbeorinitialDelaySeconds), the probe timeout is too short, GC pauses, or liveness is checking a downstream dependency (it shouldn't). - Also check: CrashLoopBackOff from a missing config or secret or a failed init container; node pressure evictions; image or entrypoint issues; a stuck
preStophook.
12Your application's memory keeps growing slowly and it crashes after several days — how do you investigate the memory leak?
Symptoms: heap usage after each GC keeps climbing (a sawtooth with a rising baseline), frequent Full GCs, longer GC pauses, CPU spikes from GC, and finally OutOfMemoryError.
- Monitor: JVM metrics (heap used after GC, old gen), GC logs (
-Xlog:gc*). - Heap dump:
jcmd <pid> GC.heap_dump /tmp/heap.hprof(orjmap -dump:live,format=b,file=heap.hprof <pid>); set-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumpsup front. Take two dumps hours apart and compare them. - Analyse: Eclipse MAT → Leak Suspects, Dominator Tree (largest retained sizes), Path to GC Roots to see what keeps the objects alive.
- Common culprits: unbounded static maps or caches (use a size bound plus TTL); ThreadLocals not removed in pooled threads; listeners or callbacks never deregistered; unclosed resources (streams, connections); growing collections in singletons; classloader leaks on redeploy (Metaspace); big session objects; String interning abuse; off-heap or direct buffer leaks (look at native memory tracking:
-XX:NativeMemoryTracking=summary, thenjcmd VM.native_memory). - Fix and verify: patch, load test, and confirm the heap baseline stays flat.
13How do you troubleshoot high CPU or memory usage in a Spring Boot application in production?
High CPU: top -H -p <pid> to find the hot thread → convert its TID to hex → find it in the jstack output (nid=0x...) to see what it's running. Or use async-profiler or JFR (jcmd <pid> JFR.start duration=60s filename=rec.jfr) to get flame graphs. Typical causes: infinite loops, regex backtracking, heavy serialization or logging, busy spinning, GC thrashing (high CPU in GC threads means memory pressure).
High memory: heap vs non-heap (Actuator jvm.memory.used), GC logs, heap dump → MAT; native memory with NMT.
Ask about the workload: a sudden traffic spike? An infinite loop or expensive algorithm? Heavy SQL? A memory leak? A batch or cron job running? A flood of API requests?
14How do you capture heap dumps and thread dumps? How do you check server memory?
- Thread dump:
jstack <pid>,jcmd <pid> Thread.print,kill -3 <pid>(written to stdout), Actuator/actuator/threaddump, JVisualVM. - Heap dump:
jcmd <pid> GC.heap_dump file.hprof,jmap -dump:live,format=b,file=heap.hprof <pid>,-XX:+HeapDumpOnOutOfMemoryError, Actuator/actuator/heapdump. In Kubernetes:kubectl execthenkubectl cpthe file out. Heap dumps pause the JVM and can be large. - Server memory:
free -h,top/htop,vmstat,/proc/meminfo; JVM:jstat -gc <pid> 1s,jcmd <pid> GC.heap_info; Kubernetes:kubectl top pod.
15What are the reasons a server crashes?
OutOfMemoryError (heap, metaspace, direct memory, "unable to create native thread"); container OOMKilled; StackOverflowError (deep recursion); the JVM crashing in native code (an hs_err_pid.log file is produced); disk full (logs filling it); exhausted file descriptors; CPU starvation or a deadlock making it unresponsive, so liveness kills it; a bad deployment or config; dependency failures at startup; hardware or node failure; a kernel OOM killer; and unhandled signals.
16How do you process a 5 GB CSV file without causing an OutOfMemoryError? (Large file processing)
Never load it entirely into memory. Stream it.
try (BufferedReader br = Files.newBufferedReader(path);
Stream<String> lines = br.lines()) {
Iterator<String> it = lines.skip(1).iterator();
List<Record> batch = new ArrayList<>(1000);
while (it.hasNext()) {
batch.add(parse(it.next()));
if (batch.size() == 1000) { repo.saveAllBatch(batch); batch.clear(); } // JDBC batch insert
}
if (!batch.isEmpty()) repo.saveAllBatch(batch);
}
- Use a streaming parser (OpenCSV/Univocity iterators, Jackson CSV
MappingIterator); chunked processing with bounded memory; JDBC batch inserts (rewriteBatchedStatements=true), or the DB's native bulk load (COPY,LOAD DATA). - Spring Batch:
FlatFileItemReader→ processor →JdbcBatchItemWriter, chunk size 1000, restartable (it tracks progress), skip and retry policies, and partitioning for parallelism. - Parallelism: split the file by byte ranges, or use a bounded producer-consumer queue with worker threads.
- Should users wait for large uploads? No. Upload straight to object storage (an S3 pre-signed URL, multipart and resumable), return 202 Accepted with a job ID, process it in a background job (queue-triggered), and let the user poll status or get a webhook or notification. Move processing to async jobs whenever it takes longer than a few seconds, uses heavy resources, or must survive restarts.
17How do you handle and analyze logs in a distributed microservices environment? How do you monitor logs?
Structured JSON logs (Logback + logstash-encoder) with traceId/spanId/userId in the MDC; ship them with Fluent Bit or Filebeat → ELK/OpenSearch + Kibana or Grafana Loki; centralized search by correlation ID; log levels per environment (changeable at runtime through Actuator /loggers); no sensitive data in logs (PII masking); retention policies; log-based alerts (error spikes). Combine with metrics and traces (the three pillars of observability).
18How do you monitor and alert on critical issues in a Spring Boot app? Which metrics do you monitor?
Stack: Actuator + Micrometer → Prometheus → Grafana dashboards → Alertmanager (PagerDuty, Slack); plus tracing (OTel → Jaeger/Tempo) and logs (ELK).
Metrics:
- RED: request Rate, Errors (5xx %), Duration (p50/p95/p99 latency) per endpoint.
- JVM: heap after GC, GC pause time and count, threads, CPU.
- Pools: Hikari active/pending/timeouts, Tomcat busy threads, executor queue sizes.
- Dependencies: downstream latency and errors, circuit breaker state, retry counts, cache hit ratio.
- Kafka: consumer lag, DLQ count. Business: orders per minute, payment success rate.
Alerting: on SLO burn rate and symptoms (user-facing error rate, latency) rather than every CPU blip; actionable alerts with runbooks.
19Distributed tracing disappears during production incidents — why?
The trace pipeline is overloaded exactly when you need it: the collector or agent is CPU- or memory-starved and drops spans, exporters time out, or sampling is cut under load. Or app threads are exhausted so the exporter can't run, context is lost in async thread pools or Kafka hops, pods get OOMKilled before flushing their buffers, or the network to the tracing backend is saturated. Fixes: give the collector its own resources, buffering and backpressure; tail-based sampling that keeps errors and slow traces; local agents (OTel Collector as a sidecar or DaemonSet); separate observability infrastructure from the app's failure domain.
20The application works for hours and then suddenly becomes unstable — what could be happening?
Something accumulates over time: a memory leak → GC thrashing → long pauses; connection or file-descriptor leaks; growing thread count (threads created without pools); cache growth without bounds; expiring credentials or tokens (OAuth tokens or certificates not refreshed); DNS cached forever (the JVM's networkaddress.cache.ttl) after an IP change; scheduled jobs piling up; log disks filling; DB connections killed by firewalls or an idle timeout (stale connections, so set maxLifetime/keepalive); clock drift. Investigate: trends over time (heap after GC, threads, open FDs, pool sizes), heap and thread dumps compared over time, and correlation with scheduled events.
21Retry logic creates duplicate transactions — how do you prevent it?
Idempotency keys (generated once per operation by the client, reused on every retry); unique constraints on the business reference; check status before retrying a timed-out call (a timeout doesn't mean failure); retry only idempotent operations and transient errors; one retry layer only (no nested retries); idempotent consumers for messages; the outbox pattern. See the idempotency answers in the Microservices section.
DevOps, Docker & Kubernetes
1Docker vs Kubernetes
Docker builds and runs containers (it packages the app and its dependencies into an image: "it works on my machine" becomes "it works everywhere"). Kubernetes orchestrates containers at scale across many machines: scheduling, self-healing, scaling, service discovery, rolling updates, config and secrets ("it works on one machine, but we need it on 100").
2How do you deploy microservices using Docker (and Kubernetes)?
# multi-stage build
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
USER 1000
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
The pipeline: CI (GitHub Actions/Jenkins) builds → tests → builds the image (or uses Jib or Buildpacks) → scans it (Trivy) → pushes it to a registry (ECR/ACR/Harbor) → deploys to Kubernetes with Helm charts or ArgoCD (GitOps) → a Deployment, Service, Ingress, ConfigMap/Secret, HPA, probes, and resource requests and limits. Use small base images, a non-root user, layered JARs for caching, and immutable tags (commit SHA).
3Kubernetes essentials for interviews
- Pod (the smallest unit), Deployment (ReplicaSets, rolling updates), StatefulSet (stable identity for DBs or Kafka), Service (ClusterIP/NodePort/LoadBalancer: a stable DNS name and load balancing), Ingress (HTTP routing and TLS), ConfigMap/Secret, HPA, PDB (disruption budget), namespaces, NetworkPolicy.
- Probes: liveness (restart if dead; don't check dependencies), readiness (remove from the Service when it can't serve), startup (protects slow-starting JVMs).
- Resources: requests (used for scheduling) vs limits (enforced: CPU is throttled, memory is OOMKilled).
- Infrastructure as Code: Helm (templated manifests, releases, rollback) and Terraform (cloud infrastructure: VPC, EKS, RDS) with state and plan/apply.
4CI/CD — what does a production pipeline look like?
On PR: build → unit tests → static analysis (SonarQube) → SCA/security scans (Snyk, OWASP) → integration tests (Testcontainers) → contract tests. On merge: build the image → push → deploy to dev → automated tests → promote to staging → performance and smoke tests → prod deploy (canary or blue-green) with automatic rollback on metric degradation. Tools: GitHub Actions or Jenkins (pipeline-as-code Jenkinsfile), ArgoCD, Helm, Terraform.
5How do you handle configuration management across dev/test/prod?
Spring profiles plus externalized config; Kubernetes ConfigMaps and Secrets mounted as env vars or files; Spring Cloud Config (git-backed, with refresh) or Consul; secrets in Vault or a cloud secret manager (never committed to git; use sealed or external secrets); Helm values files per environment; immutable images, with config injected at deploy time; feature flags for runtime toggles.
Search & Observability Stack (OpenSearch / Elasticsearch / Kibana)
1How do OpenSearch/Elasticsearch work, and how have you used them?
A distributed search engine built on Lucene. Documents (JSON) go into an index split into primary shards plus replicas spread across nodes. Text is analysed (tokenizers and filters) into an inverted index (term → list of docs), which makes full-text search fast; relevance is scored with BM25. Mappings define field types (text for full-text search vs keyword for exact match and aggregations). Queries use the Query DSL (bool, must, filter (cached, no scoring), match, term) plus aggregations for analytics. Data is near-real-time (refresh interval ~1s).
Use cases: product or log search, autocomplete (edge n-grams, completion suggester), log analytics (ELK/Kibana dashboards).
Production tips: size shards around 10–50GB, use index lifecycle management (hot-warm-cold, rollover), bulk indexing, avoid deep pagination (search_after), sync from the DB with CDC/Kafka (with the DB as source of truth), and use aliases for zero-downtime reindexing.
System Design (HLD)
1A framework that works for any design question
- Requirements: functional (3–5 core features) plus non-functional (scale, latency, availability vs consistency, durability).
- Estimates: QPS (read vs write), storage per year, bandwidth. 1M requests/day ≈ 12 QPS on average; plan for a peak of 5–10×.
- API design: the key endpoints and payloads.
- Data model and database choice (SQL for transactions and relations; NoSQL for scale and flexible schemas).
- High-level architecture: clients → CDN → LB/API Gateway → services → cache → DB/queue → workers.
- Deep dives: scaling (sharding, replication, caching), bottlenecks, failure handling, consistency.
- Trade-offs, plus monitoring and security.
2How do you design a high-traffic system?
Stateless services + horizontal scaling behind load balancers; CDN for static content; caching at several layers (browser, CDN, Redis, local); DB scaling (read replicas, sharding, partitioning, CQRS); async processing with queues to absorb spikes; rate limiting and backpressure; circuit breakers; multi-AZ/region deployment; connection pooling; denormalized read models; and observability plus load testing.
3Design a Payment / NEFT processing system (1M+ transactions per day)
Requirements: initiate transfers, validate them, debit and credit, settle in batches (NEFT runs in half-hourly batches through RBI), status tracking, notifications. NFRs: correctness over availability, exactly-once effect, auditability, about 12 TPS on average and around 100+ TPS at peak (modest, but strict).
Architecture: API Gateway (auth, rate limiting, idempotency key) → Payment Service (state machine: INITIATED → VALIDATED → DEBITED → SENT_TO_NETWORK → SETTLED / FAILED → REVERSED) → Ledger Service (double-entry, append-only) → Settlement/Batch Service (builds NEFT batches, talks to the clearing network through an adapter) → Notification. Kafka carries events; the Outbox pattern handles publishing.
Key points:
- Idempotency keys with unique constraints; double-entry ledger (every transaction is a debit row plus a credit row and the sum is 0).
- Validations: balance check, limits, fraud/AML screening, IFSC validation.
- Saga for the multi-step flow, with compensation (reversals).
- Reconciliation jobs against bank or RBI files; timeouts → status inquiry, not blind retry.
- PostgreSQL/Oracle with ACID transactions, partitioned by date; strong consistency on the balance (row lock or atomic conditional update).
- Security: encryption at rest and in transit, PCI-DSS, an audit trail, maker-checker for operations.
- Scaling to 1M+/day: partition by account ID, async processing, batch writes; the bottleneck is usually the external network, so queue and retry carefully.
4Saga Pattern vs 2PC
See Microservices → Distributed transactions. Short version: 2PC is atomic and strongly consistent but blocking, slow and coordinator-dependent, which makes it unsuitable across microservices. A Saga is a series of local transactions with compensations, eventually consistent, scalable, available; use orchestration for complex flows.
5Design a global transaction ledger (high throughput, strict auditability, immutability, fast balance reads under heavy writes)
- Immutable append-only journal: entries
(entry_id, txn_id, account_id, amount (+/-), currency, timestamp, prev_hash). Never UPDATE or DELETE; corrections are reversing entries. Optionally hash-chain entries per account (each entry includes the previous entry's hash) to make tampering evident. WORM storage for audit. - Double-entry invariant: every transaction's entries sum to zero, validated atomically.
- Write path: shard by
account_id(consistent hashing), so each account has a single writer (partition leader), which gives ordering without global locks. Kafka (partitioned by account) as the durable sequencer → ledger writers → DB (e.g. sharded PostgreSQL, CockroachDB/Spanner for global consistency, or TigerBeetle). - Fast balance reads (CQRS): keep a materialized balance per account, updated in the same transaction as the entry (
UPDATE balances SET bal = bal + ?, version = version + 1 WHERE account_id=? AND bal + ? >= 0), or built asynchronously from events into Redis. Periodic snapshots/checkpoints (balance as of entry N) mean a balance = snapshot + the delta since then. Reads go to replicas or cache (with read-your-writes where needed). - Hot accounts (e.g. a merchant receiving millions of credits): split the account into sub-accounts or buckets and aggregate on read, or batch credits.
- Cross-shard transactions: Saga, or a two-phase reservation with a transfer-intent record; idempotency on every entry (unique
txn_id). - Global: multi-region replication; home region per account; reconciliation and audit queries over the archive (data lake).
6How do you handle 1M+ transactions daily?
1M/day ≈ 12 TPS average, about 100 peak, which is easy for one well-tuned DB. Focus on correctness: idempotency, ACID, partitioned tables (by date), proper indexes, connection pooling, async non-critical paths (notifications, analytics through Kafka), batch processing for settlement, horizontal app scaling, read replicas for reporting, archiving, and monitoring. For 100M/day and beyond: sharding, event streaming, CQRS.
7Design a URL Shortener (+ Java implementation)
Requirements: shorten a URL → a 7-character code; redirect (302) fast; custom aliases; expiry; analytics. Read-heavy (100:1).
Code generation: a unique ID from a counter/ID generator → Base62 (62⁷ ≈ 3.5 trillion codes). Use a distributed ID source (Snowflake, or DB ranges where each instance reserves a block of 1000 IDs) to avoid a single counter bottleneck. Alternatives: a hash (MD5/SHA → take 7 characters, handle collisions) or pre-generated keys (a Key Generation Service).
Storage: a KV store (DynamoDB/Cassandra) or SQL: short_code PK, long_url, created_at, expires_at, user_id. Redis cache for hot codes; CDN/edge for redirects. Analytics events go to Kafka and then ClickHouse. 301 (cached by browsers, less load) vs 302 (every hit is tracked).
public class Base62 {
private static final String CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public static String encode(long num) {
if (num == 0) return "a";
StringBuilder sb = new StringBuilder();
while (num > 0) { sb.append(CHARSET.charAt((int) (num % 62))); num /= 62; }
return sb.reverse().toString();
}
}
public class UrlShortenerService {
private final ConcurrentHashMap<String, String> codeToUrl = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> urlToCode = new ConcurrentHashMap<>(); // dedupe
private final AtomicLong counter = new AtomicLong(100_000); // start offset -> longer codes
public String shortenUrl(String longUrl) {
return urlToCode.computeIfAbsent(longUrl, u -> {
String code = Base62.encode(counter.incrementAndGet());
codeToUrl.put(code, u);
return code;
});
}
public String getLongUrl(String code) { return codeToUrl.get(code); }
}
// Main: service.shortenUrl("https://www.google.com/search?q=java") -> "http://sho.rt/" + code
In production the maps become a DB plus a cache, and the AtomicLong becomes a distributed ID generator. Sequential codes are guessable, so if that matters, shuffle or obfuscate the ID (e.g. a bijective scramble) first.
8Design a Distributed ID Generator
- UUID v4: no coordination, but 128 bits, not sortable, and poor B-tree index locality (UUIDv7 is time-ordered and better).
- Twitter Snowflake (64-bit):
1 sign bit | 41 bits timestamp (ms since a custom epoch, ~69 years) | 10 bits machine ID (1024 nodes) | 12 bits sequence (4096 per ms per node). Roughly time-sortable, about 4M IDs/sec per node, no coordination at runtime. Problems: clock going backwards (wait or refuse), and machine ID assignment (ZooKeeper, or a K8s StatefulSet ordinal). - DB segment/range allocation (Leaf): each service reserves ranges of 1000 IDs from a DB row, and double-buffers the next range.
- Redis INCR (simple, but a single point unless clustered).
public class Snowflake {
private static final long EPOCH = 1700000000000L;
private final long machineId; private long lastTs = -1, seq = 0;
public Snowflake(long machineId) { this.machineId = machineId & 0x3FF; }
public synchronized long nextId() {
long ts = System.currentTimeMillis();
if (ts < lastTs) throw new IllegalStateException("Clock moved backwards");
if (ts == lastTs) { seq = (seq + 1) & 0xFFF; if (seq == 0) while ((ts = System.currentTimeMillis()) <= lastTs); }
else seq = 0;
lastTs = ts;
return ((ts - EPOCH) << 22) | (machineId << 12) | seq;
}
}
9Design an API Rate Limiter
See Microservices → Rate limiting (token bucket in Redis + Lua at the gateway, per-user/IP/API-key rules, 429 + Retry-After) and the LLD code there.
10Design a scalable Chat Application (WhatsApp/Slack)
- Connections: clients hold persistent WebSocket connections to a fleet of chat servers; a session registry (Redis: userId → server) tracks who is connected where.
- Send flow: sender → its chat server → persist the message (Cassandra/ScyllaDB, partitioned by
conversation_id, clustered by message ID/time, which suits write-heavy workloads) → look up the recipient's server → route through pub/sub (Redis or Kafka) → push to the recipient. If the recipient is offline, store it and send a push notification (APNs/FCM). - Ordering: per-conversation sequence numbers (or Snowflake IDs). Delivery receipts: sent, delivered, read (acks).
- Group chat: fan-out on write for small groups; for large channels (Slack), fan out on read or through pub/sub per channel.
- Presence: heartbeats with a TTL in Redis; publish presence changes only to contacts.
- Media: upload to object storage (S3) through a pre-signed URL and send the link; CDN for downloads.
- E2E encryption: the Signal protocol (the server stores only ciphertext). Multi-device sync, sync on reconnect (fetch everything after the last seen message ID). Search through Elasticsearch (Slack).
11Design a distributed Notification System
Requirements: multi-channel (push, SMS, email, in-app), templates, user preferences, scheduling, rate limits, high volume (millions per hour), at-least-once delivery without spamming duplicates.
Flow: producers (services) → Notification API (validate, idempotency key) → Kafka (topic per channel or priority) → workers per channel → providers (FCM/APNs, Twilio, SES/SendGrid). Plus: a preference and opt-out service, a template service (i18n), a deduplication store (Redis with a TTL on notification ID + user), per-user and per-provider rate limiting, retries with backoff and a DLQ, provider failover (a second SMS vendor), priority queues (OTPs before marketing), a scheduler for delayed sends, delivery tracking and analytics (callbacks/webhooks), and a notification log DB.
12How would you scale a Video Streaming platform (YouTube/Netflix)?
- Upload pipeline: a pre-signed upload to object storage → a queue → a transcoding worker farm (FFmpeg) → multiple resolutions and bitrates (240p–4K), codecs (H.264, H.265/AV1), chunked into 2–10s segments → HLS/DASH manifests → thumbnails, DRM, content moderation.
- Delivery: CDN (edge caches near users; Netflix's Open Connect puts boxes inside ISPs); adaptive bitrate streaming (the player picks the quality based on bandwidth); popular content pre-pushed to the edge; long tail served from origin.
- Metadata: SQL/NoSQL with a cache; search (Elasticsearch); recommendations (an offline ML pipeline + a feature store); view counts (Kafka → stream aggregation, approximate counts).
- Scale: DAU × watch time ≈ petabytes of egress, so the CDN is the main cost lever; multi-region; resumable uploads.
13Design Spotify (music streaming)
Similar to video, but smaller files. Audio files are encoded into several bitrates (Ogg/AAC: 96/160/320 kbps), stored in object storage and served through a CDN in chunks; the client prefetches the next track and caches it offline (encrypted, DRM). Services: catalog (artists, albums, tracks; read-heavy with heavy caching), playlists (user-generated, collaborative; NoSQL), user library, search (Elasticsearch with autocomplete), play events (Kafka → analytics, royalties per stream, recommendations), recommendations (Discover Weekly from collaborative filtering and audio embeddings, computed offline in batch), social/following. Royalty accounting needs accurate play counts (a play counts after about 30s), with idempotent event processing.
14Design a ride-sharing app (Uber/Ola) — including the schema
Core flows: a rider requests → matching with nearby drivers → the driver accepts → trip tracking → pricing (surge) → payment → ratings.
Location: drivers send GPS every ~4s → a location service keeps an in-memory geospatial index (Redis GEO / geohash / Uber's H3 hexagons / quadtree), sharded by city or region. Only the latest location is needed for matching; history goes through Kafka into cold storage.
Matching: find the K nearest available drivers within a radius, rank by ETA (from a routing service), offer to one driver at a time (with a timeout) or in batches; lock the driver (status AVAILABLE → RESERVED atomically) so they can't be double-booked.
Others: WebSockets or push for real-time updates; a surge pricing service (supply and demand per geo-cell); a payments Saga; ETA through graph routing (A/contraction hierarchies).
Schema:*
users(id PK, name, phone UNIQUE, email, created_at)
drivers(id PK, user_id FK, license_no, status ENUM('OFFLINE','AVAILABLE','ON_TRIP'), rating, current_vehicle_id FK)
vehicles(id PK, driver_id FK, type ENUM('MINI','SEDAN','SUV'), plate_no UNIQUE, model)
trips(id PK, rider_id FK, driver_id FK, vehicle_id FK, status ENUM('REQUESTED','ACCEPTED','ARRIVED','IN_PROGRESS','COMPLETED','CANCELLED'),
pickup_lat, pickup_lng, drop_lat, drop_lng, requested_at, started_at, ended_at,
distance_km, fare_amount, surge_multiplier, version)
payments(id PK, trip_id FK UNIQUE, amount, method, status, provider_ref, idempotency_key UNIQUE)
ratings(id PK, trip_id FK, from_user_id, to_user_id, stars, comment)
driver_locations(driver_id PK, lat, lng, geohash, updated_at) -- hot data lives in Redis GEO
-- indexes: trips(rider_id, requested_at), trips(driver_id, requested_at), trips(status)
15Design a fraud detection system for transactions
- Real-time path (< 100ms): transaction → rules engine (velocity: > 5 transactions in a minute, amount thresholds, blacklists, geo-impossibility like Delhi and London 10 minutes apart, new device) + an ML model score (gradient boosting such as XGBoost/LightGBM, or neural networks) using features from a feature store (Redis: the user's average spend, transaction count in the last hour, merchant risk, device fingerprint) → decision: approve, decline, or step-up auth (OTP) / manual review.
- Streaming features: Kafka → Flink computes windowed aggregates in real time.
- Offline: a data lake → feature engineering → model training (labels from chargebacks and analyst decisions; heavily imbalanced data, so use SMOTE, class weights, precision/recall and PR-AUC) → model registry → shadow and A/B deployment.
- Graph analysis for fraud rings (shared devices or cards across accounts).
- Feedback loop: case management, analyst labels, retraining, monitoring for drift.
- Trade-off: false positives (customer friction) vs false negatives (losses). Explainability (SHAP) for regulators.
16Design a data warehouse for an online retailer
- Sources: OLTP DBs (orders, customers, inventory), clickstream, marketing, payments → ingestion via CDC (Debezium → Kafka) and batch ELT (Airflow/Fivetran) → a data lake (S3, Parquet; bronze/silver/gold layers) → a warehouse (Snowflake, BigQuery, Redshift).
- Modeling: a star schema (Kimball). Fact tables:
fact_sales(order_id, date_key, customer_key, product_key, store_key, quantity, revenue, discount),fact_inventory_snapshot,fact_web_events. Dimensions:dim_date,dim_customer(SCD Type 2 for address and segment history:valid_from,valid_to,is_current),dim_product(category hierarchy),dim_store/channel,dim_promotion. - Transformations with dbt; data quality tests; partitioning and clustering by date; aggregates or materialized views for dashboards (Looker/Tableau/Power BI); governance (catalog, lineage, PII masking, access control).
- Use cases: sales trends, cohort/retention, basket analysis, inventory forecasting, CLV.
17Design a news aggregator (Google News)
- Ingestion: crawlers or RSS/API fetchers on a schedule (politeness, per-source rate limits) → Kafka → parse and clean → deduplicate (URL canonicalization plus near-duplicate detection with SimHash/MinHash) → NLP: categorize, extract entities, cluster related stories (embeddings + clustering) → store (articles in a DB, search index in Elasticsearch).
- Serving: a personalized feed = candidate generation (topics followed, trending clusters, locale) → ranking (freshness decay + source authority + personalization + diversity) → cache top stories per region and category (Redis/CDN).
- Trending detection: windowed counts of entities and clusters.
- Freshness is key: push breaking news through notifications.
- Scale: read-heavy, with heavily cached feeds and precomputation for anonymous users.
18Design a Metric Collection System (Prometheus/Datadog-like)
- Collection: agents on hosts or pull scraping (Prometheus) or push (StatsD/OTLP) → an ingestion gateway → Kafka (buffer) → stream processors (pre-aggregation, downsampling).
- Storage: a time-series DB (Prometheus TSDB, InfluxDB, M3, VictoriaMetrics, Thanos/Cortex for long-term storage): each series is identified by metric name + labels; data is compressed with delta-of-delta timestamps + XOR (Gorilla) values; partitioned by time blocks.
- Retention and downsampling: raw data for 15 days, 1-minute rollups for 90 days, 1-hour rollups for years.
- Query: PromQL-like aggregation; dashboards (Grafana). Alerting: a rule evaluator → Alertmanager (dedup, grouping, routing).
- Challenges: high cardinality (a userId label explodes the number of series, so limit and police labels), write throughput (millions of samples per second), HA (replication), and late or out-of-order data.
19Design an E-commerce Checkout System
Cart (Redis/DB) → checkout service (orchestrates a Saga): validate the cart and prices → reserve inventory (atomic decrement with a reservation TTL to prevent overselling: UPDATE stock SET available = available - ? WHERE sku=? AND available >= ?) → apply coupons and tax → create the order (PENDING) → payment (redirect to the PSP; idempotency key; handle async webhooks) → on success, confirm the order and commit the inventory; on failure or timeout, release the reservation (compensation). Then events → fulfilment, notifications, analytics. Flash sales: pre-load stock into Redis (decrement with Lua), a queue or virtual waiting room, rate limiting. Outbox for events; reconciliation of payments vs orders.
20Design a Search Autocomplete System (Google typeahead)
- Data: query logs → an offline aggregation job (hourly/daily) computes the top queries by frequency (with time decay) → builds a trie where each node stores its top-k completions (precomputed, so a lookup is O(prefix length)).
- Serving: the trie is sharded by prefix range and held in memory (or Redis sorted sets per prefix); responses are cached at the CDN and in the browser; the client debounces keystrokes (~100ms) and fetches only after 1–2 characters; target latency under 100ms.
- Freshness: trending queries through a streaming path (Kafka → Flink → hot updates merged in).
- Extras: personalization (blending in the user's history), spell correction (edit distance), filtering offensive terms, multi-language support.
21Design a Distributed Cache (like Redis)
- Partitioning: consistent hashing with virtual nodes (adding or removing a node moves only ~1/N of the keys), or hash slots (Redis Cluster: 16384 slots).
- Replication: a leader plus replicas per shard (async replication, so recent writes can be lost on failover); failover through Sentinel or Raft-based leader election.
- Eviction: LRU/LFU (approximate sampling like Redis), TTL expiry (lazy on access + active sampling).
- Node internals: an in-memory hash table, a single-threaded event loop (no locks; I/O multiplexing with epoll) or sharded multi-threading; persistence (RDB snapshots, AOF logs).
- Clients: smart clients that know the slot map, or a proxy (Twemproxy/Envoy).
- Problems to handle: hot keys (replicate them or add a local cache), big keys, cache stampede, network partitions (CAP trade-off: usually AP).
22Design a Content Delivery Network (CDN)
Edge PoPs around the world cache content close to users. Routing: GeoDNS or Anycast sends the user to the nearest healthy PoP. Cache hierarchy: edge → regional/mid-tier (origin shield) → origin, which cuts origin load. Caching rules: Cache-Control, TTL, ETag revalidation, cache keys (URL + selected headers). Invalidation: purge APIs, versioned URLs (app.v123.js) as the best practice. Also: TLS termination at the edge, DDoS protection/WAF, compression (gzip/brotli), HTTP/2 and HTTP/3, edge compute, video segment caching, health checks and failover, and log aggregation for analytics.
23How would you design Google Docs (collaborative editing)?
- Concurrency control: Operational Transformation (OT) (Google Docs uses it: the server orders operations and transforms concurrent ops against each other, e.g. an insert at position 5 is shifted by a concurrent insert before it) or CRDTs (Yjs, Automerge: commutative data structures with no central transform, which suits offline/P2P use).
- Architecture: clients keep a WebSocket to a document session server. All editors of one doc are routed to the same server (consistent hashing on the doc ID), which serializes operations, assigns revision numbers, transforms them, broadcasts them, and persists them to an operation log; periodic snapshots make loading fast (snapshot + replay of later ops).
- Presence and cursors: ephemeral, broadcast only (not persisted). Offline: buffer local ops and rebase on reconnect.
- Others: version history (from the op log), comments, permissions/ACLs, autosave, document storage (Spanner/Bigtable), access through the share service.
24Design a Distributed Job Scheduler
Requirements: one-off and cron jobs, millions of jobs, at-least-once execution (ideally effectively-once), retries, priorities, visibility into job status.
- Job store: a DB
jobs(id, cron/next_run_at, payload, status, retries, owner, lease_until), indexed onnext_run_at, and partitioned/sharded. - Schedulers (several instances): poll for due jobs claiming them atomically:
SELECT ... WHERE next_run_at <= now() AND status='SCHEDULED' FOR UPDATE SKIP LOCKED LIMIT 100, or partition ownership (each scheduler owns shards through leader election/ZooKeeper). Push due jobs to a queue (Kafka/SQS) by priority. - Workers: consume, execute, heartbeat or extend the lease (if a worker dies, the lease expires and the job is re-queued); idempotent jobs; retries with backoff → DLQ; record executions in a history table.
- Time wheel / delay queue for precise timers. Exactly-once is hard, so use idempotency plus fencing tokens.
- Examples: Quartz (clustered), Airflow (DAGs), Temporal.
25Design a Social Media Feed (Twitter/Instagram)
- Fan-out on write (push): when a user posts, write the post ID into each follower's feed cache (Redis list/sorted set). Reads are very fast; writes are expensive for celebrities.
- Fan-out on read (pull): at read time, merge recent posts from everyone the user follows. Writes are cheap, reads are expensive.
- Hybrid (Twitter): push for normal users; pull for celebrities (> 100k followers) and merge their posts in at read time.
- Storage: posts in Cassandra/sharded MySQL by post ID (Snowflake IDs give time ordering); social graph in a graph store or adjacency tables; media in S3 + CDN; feed caches in Redis holding only IDs (posts hydrated from a cache).
- Ranking: a candidate pool → ML ranking (engagement prediction) → diversity rules. Pagination with cursors. Counters (likes) via sharded counters or async aggregation.
26Design a File Storage System (Dropbox/Google Drive)
- Chunking: split files into ~4MB chunks, each identified by its content hash (SHA-256), which gives deduplication (identical chunks are stored once) and delta sync (only changed chunks get uploaded).
- Upload: the client asks the metadata service which chunks are missing → uploads them straight to object storage (S3) via pre-signed URLs → commits a new file version (an ordered list of chunk hashes) in the metadata DB.
- Metadata DB (SQL, sharded by user/namespace): files, folders, versions, chunks, sharing ACLs.
- Sync: a notification service (long polling/WebSocket) tells the user's other devices; they fetch the changes since their cursor. Conflicts: last writer wins, plus a "conflicted copy" file.
- Also: resumable uploads, compression, encryption, versioning/trash, sharing links, a CDN for downloads, quotas, and cold storage tiers for old versions.
Low-Level Design (LLD)
1Design a Parking Lot (LLD)
Entities: ParkingLot (singleton) → Floors → ParkingSpots (types: COMPACT, LARGE, BIKE, EV); Vehicle (Car, Bike, Truck); Ticket; EntryGate/ExitGate; PaymentStrategy; PricingStrategy; SpotAllocationStrategy.
enum VehicleType { BIKE, CAR, TRUCK }
enum SpotType { BIKE, COMPACT, LARGE }
abstract class Vehicle { final String plate; final VehicleType type; Vehicle(String p, VehicleType t) { plate = p; type = t; } }
class Car extends Vehicle { Car(String p) { super(p, VehicleType.CAR); } }
class ParkingSpot {
final String id; final SpotType type; private Vehicle vehicle;
ParkingSpot(String id, SpotType type) { this.id = id; this.type = type; }
synchronized boolean park(Vehicle v) { if (vehicle != null) return false; vehicle = v; return true; }
synchronized void free() { vehicle = null; }
boolean isFree() { return vehicle == null; }
boolean fits(VehicleType vt) {
return switch (vt) { case BIKE -> type == SpotType.BIKE; case CAR -> type == SpotType.COMPACT || type == SpotType.LARGE; case TRUCK -> type == SpotType.LARGE; };
}
}
record Ticket(String id, Vehicle vehicle, ParkingSpot spot, Instant entryTime) {}
interface SpotAllocationStrategy { Optional<ParkingSpot> find(List<ParkingSpot> spots, Vehicle v); }
class NearestFirst implements SpotAllocationStrategy {
public Optional<ParkingSpot> find(List<ParkingSpot> spots, Vehicle v) {
return spots.stream().filter(s -> s.isFree() && s.fits(v.type)).findFirst();
}
}
interface PricingStrategy { double fee(Ticket t, Instant exit); }
class HourlyPricing implements PricingStrategy {
private final Map<VehicleType, Double> rate = Map.of(VehicleType.BIKE, 10.0, VehicleType.CAR, 30.0, VehicleType.TRUCK, 60.0);
public double fee(Ticket t, Instant exit) {
long hours = Math.max(1, (long) Math.ceil(Duration.between(t.entryTime(), exit).toMinutes() / 60.0));
return hours * rate.get(t.vehicle().type);
}
}
class ParkingLot {
private final List<ParkingSpot> spots;
private final Map<String, Ticket> active = new ConcurrentHashMap<>();
private final SpotAllocationStrategy allocator; private final PricingStrategy pricing;
ParkingLot(List<ParkingSpot> spots, SpotAllocationStrategy a, PricingStrategy p) { this.spots = spots; allocator = a; pricing = p; }
public Ticket enter(Vehicle v) {
while (true) {
ParkingSpot s = allocator.find(spots, v).orElseThrow(() -> new IllegalStateException("Lot full"));
if (s.park(v)) { // atomic claim; retry if someone else took it
Ticket t = new Ticket(UUID.randomUUID().toString(), v, s, Instant.now());
active.put(t.id(), t); return t;
}
}
}
public double exit(String ticketId) {
Ticket t = active.remove(ticketId);
if (t == null) throw new IllegalArgumentException("Invalid ticket");
t.spot().free();
return pricing.fee(t, Instant.now());
}
}
Patterns to mention: Strategy (pricing, allocation, payment), Singleton (lot), Factory (vehicles), Observer (a display board showing free spots per floor). Concurrency: an atomic spot claim. Extensions: EV charging, reservations, multiple gates.
2Design a File System (LLD) — e.g. LeetCode "Design File System" / in-memory FS
Composite pattern: FileSystemEntry (abstract: name, parent, createdAt) → File (content, size) and Directory (Map<String, FileSystemEntry> children).
abstract class Entry {
final String name; Directory parent;
Entry(String name) { this.name = name; }
abstract long size();
}
class File extends Entry {
private final StringBuilder content = new StringBuilder();
File(String n) { super(n); }
void append(String s) { content.append(s); }
String read() { return content.toString(); }
long size() { return content.length(); }
}
class Directory extends Entry {
final Map<String, Entry> children = new TreeMap<>(); // sorted ls
Directory(String n) { super(n); }
long size() { return children.values().stream().mapToLong(Entry::size).sum(); }
}
class FileSystem {
private final Directory root = new Directory("");
public List<String> ls(String path) {
Entry e = resolve(path);
if (e instanceof File f) return List.of(f.name);
return new ArrayList<>(((Directory) e).children.keySet());
}
public void mkdir(String path) { walk(path, true); }
public void addContentToFile(String path, String content) {
int idx = path.lastIndexOf('/');
Directory dir = (Directory) walk(path.substring(0, idx), true);
String name = path.substring(idx + 1);
File f = (File) dir.children.computeIfAbsent(name, n -> { File nf = new File(n); nf.parent = dir; return nf; });
f.append(content);
}
public String readContentFromFile(String path) { return ((File) resolve(path)).read(); }
private Entry resolve(String path) { return walk(path, false); }
private Entry walk(String path, boolean create) {
Entry cur = root;
for (String part : path.split("/")) {
if (part.isEmpty()) continue;
Directory d = (Directory) cur;
Entry next = d.children.get(part);
if (next == null) {
if (!create) throw new IllegalArgumentException("No such path: " + path);
Directory nd = new Directory(part); nd.parent = d; d.children.put(part, nd); next = nd;
}
cur = next;
}
return cur;
}
}
Variant (LeetCode 1166 "createPath(path, value)/get(path)"): a HashMap<String,Integer> of paths, checking that the parent path exists before creating. Extensions: permissions, rm/mv, symlinks, a thread-safe version (a ReadWriteLock per directory), and search (Visitor pattern).
3Design a Task Scheduler / LRU / LFU / Rate Limiter (LLD)
See the code in the DSA sections (Heap → Task Scheduler, Linked List → LRU and the pluggable LRU/LFU cache) and Microservices → Rate limiter LLD.
Project, Behavioral & HR
1Explain your current project architecture / the microservices architecture you worked on
Structure (2–3 minutes): (1) the business domain and scale (users, TPS, data volume); (2) a diagram in words: clients → API Gateway (auth) → N Spring Boot services on Kubernetes → Kafka for events → PostgreSQL/Oracle + Redis + OpenSearch → observability (Prometheus/Grafana/ELK); (3) your services and your ownership; (4) key design decisions and trade-offs (why Kafka, why Saga, the caching strategy); (5) a production challenge you solved with measurable impact (e.g. "cut p99 from 2s to 300ms by fixing N+1 and adding a Redis cache"). Expect follow-up questions on every box you mention.
2Walk me through a feature you built from requirement to production. What was your role in LLD? How did you test it before QA?
Use STAR: requirement and stakeholders → clarifying NFRs → design (an HLD/LLD doc, class diagrams, API contract in OpenAPI, DB schema, sequence diagrams, design review) → implementation (patterns used, feature flag) → testing: unit tests (JUnit 5, Mockito), integration tests with Testcontainers (real Postgres or Kafka), contract tests, local docker-compose, load tests for critical paths, code review, Sonar quality gate → deployment (canary, dashboards, alerts) → post-release metrics and learnings.
3What production challenges have you solved? What's the biggest lesson you've learned?
Have 2–3 real stories ready with metrics, e.g. DB pool exhaustion (a remote call inside @Transactional), a memory leak (an unbounded cache or ThreadLocal), duplicate payments (added idempotency keys), Kafka lag (batching plus more partitions), a cascading failure (added timeouts and a circuit breaker). Lesson examples: "Every remote call needs a timeout"; "Observability first: you can't fix what you can't see"; "Design for failure and idempotency from day one"; "Small, reversible deployments".
4What does your Integration Team do?
Explain it as owning system-to-system integration: building and maintaining APIs, adapters and connectors between internal services and external partners (banks, payment networks, vendors); message flows (Kafka/MQ); data transformation and mapping; protocol handling (REST, SOAP, SFTP files, ISO 20022/8583 for payments); error handling, retries and reconciliation; contracts, versioning and backward compatibility; monitoring of integration SLAs. Tailor this to your real work.
5Why Goldman Sachs (or the target company)?
Connect their engineering (scale, low-latency and high-reliability financial systems, strong engineering culture, open source like GS Collections/Legend) with your experience (payments, distributed systems, Java) and your growth goals (building critical systems, learning from strong engineers). Be specific: mention a business area or tech initiative you researched.
6Managing high-pressure deadlines; collaborating with global teams; long-term aspirations
- Deadlines: prioritize ruthlessly (MVP scope), communicate risks early, break the work into milestones, never compromise on testing for critical paths, and give a concrete STAR example.
- Global teams: async communication (clear written specs, documented decisions, recorded demos), overlap hours, respect for cultural differences, clear ownership and handoffs, and a time you resolved a disagreement.
- Aspirations: grow into a Staff/Principal or tech lead role owning architecture across systems, mentoring engineers, and deepening distributed-systems expertise, aligned with the role you're applying for.
No matching questions.