I Interviewed for a Microsoft SDE I Role. Here's Everything That Happened.
#microsoft
#sde-1
#programming-pattern
#dsa-pattern
#problem-solving-strategy
Four rounds. Four coding problems. One system design question that made me genuinely enjoy an interview for once. And a blank code editor with zero autocomplete that I was NOT mentally prepared for. I got selected at the end of this I've seen enough vague "it went well, prepare DSA" posts to know they're basically useless. So here's the real thing the actual questions, what I said, where I fumbled before getting it right code I actually tested rather than just typed and hoped for the best. Whole process took about a week and a half, start to offer.
One honesty flag before I get going: I'm writing this a little after the fact, so a couple of exchanges are reconstructed from memory rather than word-for-word transcripts. The problems, the code what actually happened are all real.
| Company | Microsoft |
| Role | SDE I |
| Rounds | 4 (2 DSA, 1 system design, 1 behavioral) |
| Format | Round 1 remote, Rounds 2–4 on-site |
| Timeline | ~1–2 weeks, start to offer |
| Difficulty | Hard nothing exotic, but every problem had a twist |
| Result | Selected |
Quick Roadmap
Round 1 was two DSA problems on a bare-bones online editor. Round 2 was two more DSA problems fired at me rapid-style by someone clearly enjoying making me think fast. Round 3 was a full hour of "design WhatsApp," no code at all. Round 4 was just... a conversation. Let's go through each one.
Round 1: The Editor From Hell
So I click the link I'm sent for round 1 it's just a bare collaborative text box. No syntax highlighting. No autocomplete. Not even bracket matching. If you've spent years letting your IDE quietly save you from your own typos, this is a genuinely different experience I caught myself triple-checking every closing brace like a paranoid person.
Anyway. First question: build a logger where the same message can't get printed again if it already printed too recently some other message coming through is fine, it's specifically about repeats of the same message getting rate-limited.
My gut reaction was the dumb-but-correct version: keep a full history of every timestamp every message has ever printed at on each call, scan back through that specific message's history looking for anything too recent.
// Brute force: keep the full print history per message, scan it on every call.
static class BruteForceLogger {
Map<String, List<Integer>> log = new HashMap<>();
int windowSize;
BruteForceLogger(int windowSize) { this.windowSize = windowSize; }
boolean shouldPrintMessage(int timestamp, String message) {
List<Integer> times = log.computeIfAbsent(message, k -> new ArrayList<>());
for (int t : times) {
if (timestamp - t < windowSize) return false;
}
times.add(timestamp);
return true;
}
}
Works fine. Also pointless, because I only ever care about the most recent time each message printed not its whole life story. So really I just need a hashtable pointing each message straight at its last-seen timestamp.
// Optimal: hashtable of message -> last printed timestamp. O(1) per call.
static class Logger {
Map<String, Integer> lastPrinted = new HashMap<>();
int windowSize;
Logger(int windowSize) { this.windowSize = windowSize; }
boolean shouldPrintMessage(int timestamp, String message) {
if (!lastPrinted.containsKey(message) || timestamp - lastPrinted.get(message) >= windowSize) {
lastPrinted.put(message, timestamp);
return true;
}
return false;
}
}
Ran both against each other across 200 randomized call sequences just to be sure matched every time. Ten-second window, shouldPrintMessage(1, "foo") is true, hit it again at t=3 and it's false, wait until t=11 and it flips back to true. Exactly what you'd expect.
Then came the twist question, which I actually liked: "cool, now what if this thing had to run across a bunch of servers behind a load balancer instead of one process?" I said the in-memory hashtable dies instantly there server B has zero clue what server A just printed. Swap it for something shared, Redis is the obvious pick, same message-to-timestamp mapping just centralized. Then I caught myself and added the race condition angle unprompted: two servers could both read "safe to print" in the same instant before either writes back, so you'd want an atomic check-and-set, not a separate read then write. That seemed to land well.
Second problem
This one actually got me for a second: Imagine a row of buildings, a fixed number of bricks, and a fixed number of ladders. To move from one building to the next, you only need to spend resources when the next building is taller. You can either use a ladder, which covers any height difference regardless of its size, or use bricks, where you need one brick for every unit you climb.
The question is simple: how far can you get?
Example
heights = [4, 12, 2, 7, 3, 18, 20, 3, 19]
bricks = 10
ladders = 2
Explanation:
Starting from building 0, we need to decide how to use our 10 bricks and 2 ladders while moving to taller buildings.
- 4 → 12 = 8
- 2 → 7 = 5
- 3 → 18 = 15
- 18 → 20 = 2
- 3 → 19 = 16
Moving down or staying at the same height costs nothing.
At first, it may seem like we should immediately decide whether to use bricks or a ladder for every climb. But that creates many possible combinations. For example, should we use a ladder for the climb of 8? What if a much larger climb appears later? Should we save bricks now or save the ladder?
Eventually, we discover that with 10 bricks and 2 ladders, we can reach building index 7, but we cannot afford the final climb from 3 → 19.
Output: 7
My first approach is brute force, For every upward climb, we have two possible choices:
- Use bricks, if we have enough.
- Use a ladder, if one is available.
So we can recursively try both possibilities and return the furthest building reachable.
// Brute force: try both choices (ladder or bricks) at every climb, recursively.
static int helper(int[] heights, int i, int bricks, int ladders) {
if (i == heights.length - 1) return i;
int diff = heights[i + 1] - heights[i];
// Moving down or staying at the same height costs nothing.
if (diff <= 0) {
return helper(heights, i + 1, bricks, ladders);
}
int best = i;
// Option 1: Use bricks.
if (bricks >= diff) {
best = Math.max(
best,
helper(heights, i + 1, bricks - diff, ladders)
);
}
// Option 2: Use a ladder.
if (ladders >= 1) {
best = Math.max(
best,
helper(heights, i + 1, bricks, ladders - 1)
);
}
return best;
}
This brute-force solution is useful because it clearly shows the decision we need to make at every climb: bricks or ladder? But the number of combinations grows quickly. If there are many upward climbs, trying every possible resource allocation becomes expensive.
That's where the important greedy observation comes in: since a ladder can cover any height difference, we should ideally use ladders for the largest climbs and bricks for the smaller ones.
The tricky part is that we don't know the future climbs in advance. So, for every upward climb, we temporarily assume that we're using a ladder and store the climb in a min-heap. If the number of climbs exceeds the ladders available, we remove the smallest climb from the heap and pay for it using bricks instead. This ensures that our ladders are always reserved for the largest climbs encountered so far.
import java.util.PriorityQueue;
static int optimal(int[] heights, int bricks, int ladders) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int i = 0; i < heights.length - 1; i++) {
int diff = heights[i + 1] - heights[i];
// No resources are needed when moving down or staying at the same height.
if (diff <= 0) {
continue;
}
// Temporarily assign a ladder to this climb.
minHeap.offer(diff);
// If we need more ladders than available,
// use bricks for the smallest climb.
if (minHeap.size() > ladders) {
bricks -= minHeap.poll();
// Cannot afford this climb.
if (bricks < 0) {
return i;
}
}
}
return heights.length - 1;
}
The key idea is simple: whenever we have more climbs than ladders, the smallest climb is the cheapest one to pay for with bricks, leaving the ladders available for larger climbs.
Quick follow-up before we moved on: "What's the complexity here?"
The time complexity is O(n log L), where n is the number of buildings and L is the number of ladders. Each upward climb is added to the min-heap, and when necessary, the smallest climb is removed, both taking O(log L) time. The space complexity is O(L) because the heap stores at most L + 1 climbs.
Round 2 : DSA Round
Round 2 had a completely different feel from the first one. The interviewer moved very quickly. I would barely finish two sentences explaining my approach before the next follow-up question came in. At first, I found the pace a little difficult to adjust to, but after a few minutes I stopped trying to predict the next question and just went with the conversation. In fact, this ended up being the round I enjoyed the most because it felt more like a technical discussion than a typical question-and-answer interview.
Question 1: Maximum Sum Subarray of Exactly K Elements
The first question was a variation of the classic maximum subarray problem. Instead of finding a subarray of any length, the interviewer asked me to find the maximum sum of a subarray containing exactly K elements.
For example, given [2, 1, 5, 1, 3, 2] and K = 3, the possible windows are [2, 1, 5], [1, 5, 1], [5, 1, 3], and [1, 3, 2]. Their sums are 8, 7, 9, and 6, so the answer is 9, coming from [5, 1, 3].
I started with the straightforward brute-force approach. For every possible starting position, I would calculate the sum of the next K elements and keep track of the maximum. This works, but it takes O(N * K) time because we calculate the sum of every window from scratch.
static int bruteForce(int[] nums, int k) {
int n = nums.length;
int best = Integer.MIN_VALUE;
for (int i = 0; i + k <= n; i++) {
int sum = 0;
for (int j = i; j < i + k; j++) {
sum += nums[j];
}
best = Math.max(best, sum);
}
return best;
}
The optimization was fairly natural once I looked at what happens between two consecutive windows. If the first window is [2, 1, 5], the next window is [1, 5, 1]. We don't need to calculate the entire sum again. We can simply remove the element that left the window and add the element that entered it.
So if the current sum is 8, we remove 2 and add 1, giving us 7. This is the fixed-size sliding window pattern.
static int optimal(int[] nums, int k) {
int n = nums.length;
int windowSum = 0;
for (int i = 0; i < k; i++) {
windowSum += nums[i];
}
int best = windowSum;
for (int i = k; i < n; i++) {
windowSum += nums[i] - nums[i - k];
best = Math.max(best, windowSum);
}
return best;
}
This brings the time complexity down to O(N) and uses O(1) extra space. The important idea is that we don't need to recalculate information that can be updated as the window moves.
The interviewer then asked whether the same solution would work if the array contained negative numbers. It does. Nothing in the sliding-window logic depends on the numbers being positive. We are simply maintaining the sum of the current window and comparing it with the best sum found so far.
For example, if the input is [-5, -2, -8, -1] and K = 2, the window sums are -7, -10, and -9. The correct answer is -7. This is also why initializing best with Integer.MIN_VALUE is important. If we initialized it with 0, the solution would fail when every possible window had a negative sum.
I had also tested the solution with around 30 random test cases, including negative numbers, and compared the optimized solution against the brute-force version. There were no mismatches. I like doing this kind of comparison when testing an optimized solution because the brute-force version gives you a simple reference implementation to validate against.
Then came another follow-up almost immediately: what if the requirement changed from exactly K elements to at most K elements?
This is where the problem changes more than it initially appears. With exactly K elements, every valid window has the same size, so a fixed-size sliding window works perfectly. With at most K elements, the window can have different sizes. A shorter window can sometimes produce a better answer, especially when negative numbers are present.
For example, consider [5, -10, 4] with K = 2. The single-element subarray [5] has a sum of 5, while [5, -10] has a sum of -5. So simply taking windows of size K would miss the actual answer.
I told the interviewer that this was a different problem and that I would need to reconsider the approach rather than blindly modifying the fixed-size sliding window. I mentioned that I would first look at the constraints and the effect of negative numbers before deciding on the right technique. I had not practiced that exact variant enough to confidently write the optimal code on the spot, so I said that rather than guessing.
That turned out to be a useful moment in the interview. Sometimes it is better to clearly explain what you know and what you need to reconsider than to write code that you are not confident about.
Question 2: Palindrome Partitioning with Minimum Length
The second question was more interesting. The interviewer asked me to partition a string into the minimum possible number of palindromic pieces, with one additional condition: every piece had to have at least a given minimum length.
For example, if the string is "aabb" and the minimum length is 2, we can split it as "aa" + "bb", giving us two pieces. On the other hand, for "abc" with a minimum length of 2, there is no valid partition because there is no palindrome of length at least two that can be used to cover the entire string.
I started with the recursive brute-force approach. At every position, I would try every possible ending position. If the substring was a palindrome and its length was at least the required minimum, I would recursively solve the remaining part of the string and take the minimum number of pieces.
static int bruteForce(String s, int start, int minLen) {
if (start == s.length()) {
return 0;
}
int best = INF;
for (int end = start + minLen; end <= s.length(); end++) {
if (isPalindrome(s, start, end - 1)) {
int rest = bruteForce(s, end, minLen);
if (rest != INF) {
best = Math.min(best, 1 + rest);
}
}
}
return best;
}
The problem with this approach is that we repeatedly check the same substrings and explore many of the same states again. This is where dynamic programming becomes useful.
I used one table to precompute whether every substring is a palindrome. The idea is simple: a substring is a palindrome if its first and last characters are equal and the substring between them is also a palindrome. Once this table is available, checking whether any substring is a palindrome becomes an O(1) operation.
I then used another DP array to keep track of the minimum number of valid pieces needed to build the string up to each position. When considering a split between positions j and i, I only accepted it if the substring had the required minimum length and pal[j][i - 1] was true.
static int optimal(String s, int minLen) {
int n = s.length();
boolean[][] pal = new boolean[n][n];
for (int len = 1; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
if (s.charAt(l) == s.charAt(r)
&& (len <= 2 || pal[l + 1][r - 1])) {
pal[l][r] = true;
}
}
}
int[] dp = new int[n + 1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= i - minLen; j++) {
if (dp[j] == INF) {
continue;
}
if (pal[j][i - 1]) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
}
return dp[n];
}
Both the palindrome table and the partition DP take O(N²) time, with O(N²) space for the palindrome table.
I tested this one against the brute-force solution as well. I used around 3,000 random strings and intentionally limited the strings to two characters. That produced a lot of palindromes and therefore created many possible partitions, which made the tests more useful. The optimized solution matched the brute-force result in all the tests, including cases where no valid partition existed.
For example, "aabbaa" with a minimum length of 2 returns 1 because the entire string is already a palindrome. "aabb" returns 2 because it can be split into "aa" and "bb". "abc" correctly returns an impossible result because there is no valid way to partition the string.
The final follow-up was to return the actual palindrome pieces instead of just the number of pieces.
That requires only a small change to the DP. Along with the minimum count, we can store the previous position that produced the best result. For example, if parent[4] = 2, it means the optimal solution reached position 4 from position 2, so the last piece is the substring from 2 to 3.
Once the DP is complete, we start from the end of the string and follow the parent pointers backward until we reach the beginning. This gives the pieces in reverse order, so we reverse the result at the end.
For "aabb", the positions might look like:
4 → 2 → 0
which gives "bb" followed by "aa" while walking backward. Reversing that list gives:
["aa", "bb"]
I didn't fully code the reconstruction part live because we were already moving quickly through the round, but I explained the approach and the interviewer was happy to move on.
Looking back, the most useful part of this round wasn't either of the two algorithms. It was the way the interviewer kept changing the requirements. The first problem started with a fixed-size window, then introduced negative numbers, and then changed the requirement to "at most K". The second problem started with minimum palindrome partitioning, added a minimum length constraint, and finally asked for the actual partition instead of just the count.
That is something worth preparing for in DSA interviews. Don't just practice getting the accepted answer. After solving a problem, ask yourself what would happen if one of the main conditions changed. What if negative numbers were allowed? What if the window size became variable? What if the interviewer wanted the actual elements instead of the maximum value? What information would you need to store to reconstruct the answer?
Those follow-up questions are often where the real interview starts.
And one more thing I took away from this round: if you don't know the best solution to a variation, don't bluff. Explain what changes, identify the part of your current approach that no longer works, and think through the new constraints. A clear and honest explanation is much better than confidently writing code that you cannot justify.
Round 3: Design WhatsApp (No Code, Just Me and a Whiteboard)
This was the round I'd actually call fun. Build a real-time messaging platform 1:1 chats, group chats, message ordering, delivery guarantees, notifications, syncing across devices scaling to millions of people. Zero code, just talk it through.
Here's roughly how I broke it down.
Connections first: This needs to feel instant, so persistent WebSocket connections between client and server, not polling. Polling is either too slow or too wasteful depending on the interval neither is good enough for a chat app.
Sending a message: It hits a connection server, which doesn't try to deliver it directly it drops it onto a message queue (Kafka's the obvious pick) instead. That decoupling matters a lot, because the recipient might be offline, on a bad connection, or sitting on a totally different connection server than the sender.
Ordering: Give every message an increasing sequence number, but scoped per conversation, not globally. Network timing can make messages arrive slightly out of order at the delivery layer, but the client can always rebuild the true order by sorting on that number instead of trusting whatever order things showed up in.
Delivery states: sent, delivered, read three separate events each one needs to route back to the original sender, which is basically the same delivery problem all over again, just carrying a status update instead of a chat bubble.
Persistence and syncing across devices: Every message gets durably written (Cassandra fits the access pattern well heavy writes, simple reads scoped to one conversation) before it counts as truly sent. Each device just remembers the last sequence number it's seen per conversation on reconnecting, asks for everything newer. Same trick used to catch up any client that's been offline.
Notifications: If someone's app isn't actively connected, fall back to a push notification (APNs, FCM) triggered off that same underlying message event one event, two possible delivery paths depending on whether the client's actually listening.
Group chats: same idea, fanned out to every member at the queue level. I flagged the real danger spot on my own a broadcast to a group with hundreds of thousands of members needs to fan out asynchronously in the background, not block the sender's request until every single person's been individually notified.
Scaling: shard the connection servers so no single box is holding millions of live sockets, shard the message store by conversation ID since almost every query is already scoped to one conversation keep presence (who's online right now) as its own separate, lightweight service it churns way faster than message content and shouldn't be tangled up with the rest of the system.
Client A Client B
| |
| WebSocket WebSocket |
v v
+----------+ +----------+
| Conn Svr |<---- sharded, many instances ------> | Conn Svr |
+----+-----+ +----+-----+
| |
+---------------------+ +-------------------+
v v
+------------------+
| Message Queue | (Kafka -- decouples send from deliver)
+---------+--------+
|
+--------------+----------------+
v v
+----------------+ +----------------+
| Message Store | | Notification |
| (sharded by | | Service (push |
| conversation) | | via APNs/FCM |
+----------------+ | if offline) |
+----------------+
I made a point of stating my tradeoffs out loud before being asked Kafka over a direct-delivery path costs a bit of latency but buys durability, per-conversation sequence numbers over one global clock avoids a single choke point. That seemed to be exactly the kind of thing this interviewer wanted to dig into the round genuinely felt like two engineers arguing about a real system, not an exam.
Round 4: Just... a Conversation
By this point I was fried from three straight hours of hard technical rounds round 4 was a genuine relief. My journey, how I work with people, what actually gets me excited about the work, why Microsoft. That kind of thing.
Two questions stuck with me. "Tell me about a time you disagreed with a teammate on something technical" I picked a real, specific example instead of a made-up-sounding one, walked through what each of us actually wanted made sure the ending was about what we both learned rather than "and I was right." And "what motivates you day to day" I kept it concrete and personal instead of reciting something that sounded like a company values page.
Didn't feel adversarial at all. Felt like they were figuring out whether they'd want to sit next to me in a standup, which, fair.
What I'd Tell Someone Prepping for This
Get the fundamentals genuinely solid arrays, strings, graphs, sliding window, DP, greedy-with-a-heap because every single problem I got was one of those with a twist bolted on. Rate limiting instead of a plain hashmap lookup. Exactly-K instead of any-length. A minimum-length rule bolted onto palindrome partitioning. If you only ever drill the clean textbook version of a pattern, the twist is exactly what's going to catch you flat-footed.
Practice coding somewhere with zero autocomplete at least once before you walk in. I was not ready for how naked that felt.
If you land a fast-paced interviewer, don't panic when they interrupt your explanation with the next follow-up that usually means they're satisfied and moving faster, not that something's wrong.
For system design, say your tradeoffs out loud before anyone has to drag them out of you. That one change in approach seemed to shift the whole tone of my round for the better. And for the behavioral round have two or three real stories ready ahead of time. Not perfect ones. Real ones. Specific beats polished, every time.
Questions People Keep Asking Me
How many rounds does Microsoft's SDE I process have?
Four for me two DSA, one system design, one behavioral. Took about 1 to 2 weeks total.
What kind of coding questions came up?
All four were familiar patterns with a twist: a rate-limited logger (hashtable), a greedy climbing problem with bricks and ladders (min-heap), a fixed-window subarray sum palindrome partitioning with a minimum length rule.
Is there a system design round for SDE I?
There was for me a full hour designing something like WhatsApp, covering ordering, delivery guarantees, persistence, device syncing scale.
How long did the whole thing take?
Roughly 1 to 2 weeks, first round to offer.
