Amazon SDE-2 Interview Experience (3 Years Experience)
#amazon
#interview-experience
Company: Amazon Role: Software Development Engineer II (SDE-2) Experience: 3 Years Interview Mode: Virtual Result: Rejected
Last Month, I interviewed with Amazon for an SDE-2 Backend position. The entire process started with an Online Assessment and was followed by three technical rounds. Overall, the interviews were challenging but well structured. As expected, Amazon placed a strong emphasis on both coding skills and its Leadership Principles (LPs) throughout the process.
Even though I didn't receive an offer, I learned a lot from the interviews and wanted to share my experience in case it helps someone preparing for a similar role.
Online Assessment (OA)
The Online Assessment consisted of two different sections.
The first section included standard DSA problems. Unfortunately, I don't remember the exact first coding question, but it was fairly typical of Amazon's online assessments.
The second section was different from a traditional coding challenge. It was a developer-focused exercise where HackerRank provided access to a remote development environment. Interestingly, the editor had an integrated AI assistant available in the sidebar, similar to tools like Cursor, which could be used during the exercise.
The role was open for Hyderabad and Bengaluru and after clearing the assessment, I was invited to the interview rounds. Amazon's SDE II hiring process commonly combines coding, development exercises and behavioral assessments centered on Leadership Principles.
Round 1 : DSA + Leadership Principles
The first interview started with a few Leadership Principle questions before moving into coding.
Question 1 – Distance Between Two Nodes in a Binary Tree
The interviewer asked me to find the distance between two nodes in a binary tree. The catch was that the nodes didn't have parent pointers. The inputs were only:
- Root node
- Source node
- Target node
I discussed my approach first and then implemented the solution. The interviewer seemed satisfied and we spent some time discussing the complexity and a few edge cases.
My Approach
Since the tree doesn't contain parent pointers, we cannot move upward from a node. The optimal solution is to first compute the Lowest Common Ancestor (LCA) of the two nodes. The shortest path between the nodes always passes through their LCA. Once the LCA is known, I calculate the distance from the LCA to each node using DFS and add the two distances. I also verify that both nodes exist in the tree to correctly handle invalid inputs. This visits each node at most a constant number of times, resulting in O(N) time and O(H) auxiliary space.
public class DistanceBetweenNodes {
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) {
this.val = val;
}
}
static class Result {
TreeNode lca;
boolean foundSource;
boolean foundTarget;
Result(TreeNode lca, boolean foundSource, boolean foundTarget) {
this.lca = lca;
this.foundSource = foundSource;
this.foundTarget = foundTarget;
}
}
/**
* Returns distance between source and target.
* Returns -1 if either node is not present.
*/
public static int distance(TreeNode root, TreeNode source, TreeNode target) {
if (root == null || source == null || target == null)
return -1;
if (source == target) {
return exists(root, source) ? 0 : -1;
}
Result result = findLCA(root, source, target);
if (!result.foundSource || !result.foundTarget)
return -1;
int d1 = findDistance(result.lca, source, 0);
int d2 = findDistance(result.lca, target, 0);
return d1 + d2;
}
/**
* Finds LCA while also verifying whether both nodes exist.
*/
private static Result findLCA(TreeNode root, TreeNode source, TreeNode target) {
if (root == null)
return new Result(null, false, false);
Result left = findLCA(root.left, source, target);
Result right = findLCA(root.right, source, target);
boolean foundSource = left.foundSource || right.foundSource || root == source;
boolean foundTarget = left.foundTarget || right.foundTarget || root == target;
// If current node is source or target
if (root == source || root == target) {
return new Result(root, foundSource, foundTarget);
}
// Nodes found in different subtrees
if (left.lca != null && right.lca != null) {
return new Result(root, foundSource, foundTarget);
}
TreeNode lca = left.lca != null ? left.lca : right.lca;
return new Result(lca, foundSource, foundTarget);
}
/**
* Finds distance from root to target.
*/
private static int findDistance(TreeNode root, TreeNode target, int distance) {
if (root == null)
return -1;
if (root == target)
return distance;
int left = findDistance(root.left, target, distance + 1);
if (left != -1)
return left;
return findDistance(root.right, target, distance + 1);
}
// Checks whether a node exists in tree.
private static boolean exists(TreeNode root, TreeNode target) {
if (root == null)
return false;
if (root == target)
return true;
return exists(root.left, target) || exists(root.right, target);
}
public static void main(String[] args) {
/*
1
/ \
2 3
/ \ / \
4 5 6 7
/
8
*/
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
TreeNode n6 = new TreeNode(6);
TreeNode n7 = new TreeNode(7);
TreeNode n8 = new TreeNode(8);
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
n3.left = n6;
n3.right = n7;
n5.left = n8;
System.out.println(distance(n1, n4, n8)); // 3
System.out.println(distance(n1, n4, n6)); // 4
System.out.println(distance(n1, n2, n8)); // 2
System.out.println(distance(n1, n3, n7)); // 1
System.out.println(distance(n1, n6, n6)); // 0
TreeNode fake = new TreeNode(100);
System.out.println(distance(n1, n4, fake)); // -1
}
}
Question 2 – Design a Stack with O(1) Middle Element Operations
The second problem was more interesting.
I had to design a stack that supported the following operations in O(1) time:
- Push
- Pop
- Top
- Get Middle
I first discussed the data structure I wanted to use and explained why a doubly linked list combined with a middle pointer would work efficiently.
I was able to implement the core data structure and complete the logic for maintaining the middle element whenever items were pushed or popped. I didn't finish follow up implementation before time ran out, but the interviewer appeared more interested in my design decisions than in typing every line of code.
Overall, I walked out of this round feeling fairly confident.
public class StackWithMiddle {
static class Node {
int data;
Node prev, next;
Node(int data) {
this.data = data;
}
}
private Node head; // Top of stack
private Node middle; // Middle node
private int count;
public void push(int data) {
Node node = new Node(data);
if (head != null)
head.prev = node;
head = node;
count++;
if (count == 1) {
middle = node;
} else if (count % 2 == 0) {
// Move middle one step backward
middle = middle.prev;
}
}
public int pop() {
if (count == 0)
throw new RuntimeException("Stack Underflow");
int value = head.data;
head = head.next;
if (head != null)
head.prev = null;
count--;
if (count == 0) {
middle = null;
} else if (count % 2 == 1) {
// Move middle one step forward
middle = middle.next;
}
return value;
}
public int top() {
if (count == 0)
throw new RuntimeException("Stack is Empty");
return head.data;
}
public int getMiddle() {
if (count == 0)
throw new RuntimeException("Stack is Empty");
return middle.data;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public static void main(String[] args) {
StackWithMiddle stack = new StackWithMiddle();
stack.push(10);
System.out.println("Middle: " + stack.getMiddle()); // 10
stack.push(20);
System.out.println("Middle: " + stack.getMiddle()); // 20
stack.push(30);
System.out.println("Middle: " + stack.getMiddle()); // 20
stack.push(40);
System.out.println("Middle: " + stack.getMiddle()); // 30
stack.push(50);
System.out.println("Middle: " + stack.getMiddle()); // 30
System.out.println("Top: " + stack.top()); // 50
System.out.println("Pop: " + stack.pop()); // 50
System.out.println("Middle: " + stack.getMiddle()); // 30
System.out.println("Pop: " + stack.pop()); // 40
System.out.println("Middle: " + stack.getMiddle()); // 20
System.out.println("Pop: " + stack.pop()); // 30
System.out.println("Middle: " + stack.getMiddle()); // 20
}
}
Follow-up: Delete Middle in O(1)
Interviewers often extend this problem by asking: Can you also delete the middle element in O(1)?
Since each node has prev and next pointers and we already maintain a pointer to the middle node, it can also be removed in O(1):
public int deleteMiddle() {
if (count == 0)
throw new RuntimeException("Stack is Empty");
int value = middle.data;
if (count == 1) {
head = middle = null;
count = 0;
return value;
}
Node prev = middle.prev;
Node next = middle.next;
if (prev != null)
prev.next = next;
if (next != null)
next.prev = prev;
// If deleting the top element
if (middle == head)
head = next;
count--;
if (count % 2 == 0)
middle = prev;
else
middle = next;
return value;
}
Round 2 : Coding + Object-Oriented Design
Like the previous round, this interview also began with a couple of Leadership Principle questions before moving to the technical discussion.
The coding problem was based on LeetCode's Basic Calculator. The expression contains:
- +, -, (, ), Multi-digit numbers and Spaces
- No * or /.
I first solved the parsing problem and explained my reasoning as I progressed. The optimal solution uses a stack to save the previous result and sign whenever we encounter an opening parenthesis.
import java.util.Stack;
public class BasicCalculator {
public static int calculate(String s) {
Stack<Integer> stack = new Stack<>();
int number = 0;
int sign = 1; // +1 or -1
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (Character.isDigit(ch)) {
number = 0;
while (i < s.length() && Character.isDigit(s.charAt(i))) {
number = number * 10 + (s.charAt(i) - '0');
i++;
}
i--;
result += sign * number;
}
else if (ch == '+') {
sign = 1;
}
else if (ch == '-') {
sign = -1;
}
else if (ch == '(') {
// Save current result
stack.push(result);
// Save current sign
stack.push(sign);
// Reset for sub-expression
result = 0;
sign = 1;
}
else if (ch == ')') {
int prevSign = stack.pop();
int prevResult = stack.pop();
result = prevResult + prevSign * result;
}
}
return result;
}
public static void main(String[] args) {
System.out.println(calculate("1 + 1")); // 2
System.out.println(calculate("2-1+2")); // 3
System.out.println(calculate("(1+(4+5+2)-3)+(6+8)")); // 23
System.out.println(calculate("1-(2+3)")); // -4
System.out.println(calculate("10-(3-(2+1))")); // 10
System.out.println(calculate("2147483647")); // 2147483647
}
}
Once that was done, the interviewer extended the discussion and asked how I would redesign the solution using object-oriented principles so that it could easily support additional operators and future extensions.
Instead of treating it as just another coding question, the interviewer wanted to evaluate software design skills. This is a classic SDE-2 follow-up. The interviewer is no longer testing whether I can parse an expression they're evaluating whether I can design a calculator that is open for extension but closed for modification (Open/Closed Principle).
I proposed a modular object-oriented approach by separating parsing and evaluation responsibilities. I apply the Strategy Pattern so each operator encapsulates its own behavior.
Instead of writing:
if (ch == '+') {
...
} else if (ch == '-') {
...
} else if (ch == '*') {
...
}
every operator becomes its own class.
+----------------------+
| Calculator |
+----------------------+
|
|
+----------------------+
| Parser |
+----------------------+
|
Generates Tokens
|
+----------------------------+
| |
NumberToken OperatorToken
|
Operator Strategy
|
-------------------------------------------------
| | | | |
AddOperator SubOperator MulOperator DivOperator PowerOperator
When a new operator is introduced, such as % or ^, no existing code needs to change.
Step 1: Create the Operator Interface
Define a common contract for all mathematical operators. Each operator specifies:
- Its symbol (
+,-,*, etc.) - Its precedence
- How it performs the calculation
public interface Operator {
char symbol();
int precedence();
int apply(int left, int right);
}
Step 2: Implement Individual Operators
Each operator is implemented as a separate class. This follows the Open/Closed Principle because adding new operators doesn't require modifying existing code.
Addition Operator
public class AddOperator implements Operator {
@Override
public char symbol() {
return '+';
}
@Override
public int precedence() {
return 1;
}
@Override
public int apply(int left, int right) {
return left + right;
}
}
Subtraction Operator
public class SubtractOperator implements Operator {
@Override
public char symbol() {
return '-';
}
@Override
public int precedence() {
return 1;
}
@Override
public int apply(int left, int right) {
return left - right;
}
}
Adding a New Operator (Multiplication)
To support multiplication, simply add another implementation.
public class MultiplyOperator implements Operator {
@Override
public char symbol() {
return '*';
}
@Override
public int precedence() {
return 2;
}
@Override
public int apply(int left, int right) {
return left * right;
}
}
Notice that no existing classes need to be modified. We only add a new implementation.
Step 3: Create the Operator Registry
The registry stores all available operators and provides them based on their symbol.
public class OperatorRegistry {
private final Map<Character, Operator> operators = new HashMap<>();
public OperatorRegistry() {
register(new AddOperator());
register(new SubtractOperator());
register(new MultiplyOperator());
}
public void register(Operator operator) {
operators.put(operator.symbol(), operator);
}
public Operator get(char symbol) {
return operators.get(symbol);
}
}
Whenever the evaluator encounters an operator, it simply looks it up from the registry.
Step 4: Parse the Expression
The parser has only one responsibility: convert the input string into tokens. For example:
10 + 20 * 3
becomes
[10] [+] [20] [*] [3]
The parser does not know how addition, subtraction or multiplication works. It only identifies numbers and operator symbols.
Step 5: Evaluate the Expression
The evaluator processes the parsed tokens and delegates the computation to the appropriate operator.
Operator op = registry.get('*');
int result = op.apply(20, 3);
The evaluator doesn't know how multiplication is implemented. It simply retrieves the operator and invokes its apply() method.
This keeps the evaluator independent of individual operator implementations and makes the design easy to extend with new operators like division, modulus or exponentiation.
Due to time constraints, I wasn't able to implement the complete design.
Looking back, I think this round wasn't really about getting the correct answer. It was about demonstrating clean software design and showing that I could think beyond solving a single coding problem.
Round 3 : Hiring Manager Discussion
The final interview felt very different from the earlier technical rounds. Instead of jumping directly into coding, the interviewer spent the first few minutes getting to know my background.
We talked about:
- My previous experience
- The projects I'd worked on
- My career progression
- Why I wanted to switch companies again
Since I had already changed jobs once before, the interviewer spent quite a bit of time trying to understand my motivation for looking for another opportunity. It wasn't an aggressive conversation, but there were several follow-up questions that required thoughtful answers.
Deep Dive into Previous Project
After that, the discussion shifted to the most impactful project I had worked on in my current organization. The interviewer wanted to understand not only what I built but also why certain technical decisions were made.
Some of the questions included:
- Why did your team choose this architecture?
- Why not use an alternative approach?
- What were the trade-offs?
- What challenges did you face?
- How did your solution scale?
Because the project involved confidential work, I had to be careful not to disclose anything sensitive while still explaining the overall design and technical decisions. The interviewer kept digging deeper into almost every answer, so the discussion felt more like an architectural review than a traditional interview.
Design Follow-up
Toward the end of the interview, I was asked only one technical question.
The interviewer proposed adding a completely new feature to the system I'd just explained. Instead of expecting one correct answer, he wanted to understand my decision-making process.
I came up with two different design approaches. For each approach, I explained:
- Advantages
- Disadvantages
- Complexity
- Maintainability
- Scalability
He then asked me to write down the pros and cons of both approaches and we spent the remaining time discussing the trade-offs.
Result
A few days later, I received a rejection email.
I reached out to the recruiter hoping to get more detailed feedback. The only information shared was that the feedback from Rounds 2 and 3 wasn't positive enough for the panel to move forward.
Unfortunately, no additional details were provided, even after I specifically asked for actionable feedback. Naturally, it was disappointing, especially because I felt the interviews had gone reasonably well. But that's part of interviewing at companies like Amazon. Sometimes even a decent performance isn't enough because the hiring bar is extremely high and the evaluation covers coding ability, software design, communication and Leadership Principles together. Amazon also advises candidates to prepare thoroughly for behavioral interviews using the STAR method and Leadership Principles, as they are evaluated throughout the process.
My Takeaways
Looking back, there are a few things I would do differently.
-
First, for coding rounds, solving the problem correctly is only one part of the evaluation. Interviewers also pay close attention to how you communicate your thought process, discuss trade-offs and improve your solution.
-
Second, object-oriented design requires more than wrapping existing logic inside classes. You need to demonstrate extensibility, modularity and clean abstractions.
-
Finally, Amazon's Leadership Principles aren't just an HR formality. They influence every round, including technical discussions. Preparing solid STAR stories and being able to explain your technical decisions clearly is just as important as practicing DSA.
Although I didn't get the offer this time, the experience helped me identify areas where I can improve and I'll definitely be better prepared for future interviews.
Questions Asked
| Round | Questions |
|---|---|
| Online Assessment | DSA Problem, Developer Environment Challenge |
| Round 1 | Distance Between Two Nodes in a Binary Tree, Design Stack with O(1) Middle Operation |
| Round 2 | Basic Calculator, Extend the Solution Using Object-Oriented Design |
| Round 3 | Leadership Principles, Project Deep Dive, System Design Feature Extension, Design Trade-offs |
