Ad

Real Adobe Interview Experience

4 Rounds: String Algorithms, Async JavaScript, System Basics & HR

4
Rounds
8-10
Weeks Prep
80-95 LPA
Expected Package
βœ“
SELECTED
Vasanth

By Vasanth Bhat

Staff Software Engineer @ Walmart Global Tech

Successfully guided 100+ developers to top company offers (Microsoft, Uber, Adobe, Atlassian)

Round 1: LeetCode String Problem (60 minutes)

Medium difficulty string manipulation problem. Focus on optimal solution and handling edge cases.

🎯 Can Form String from Words

Difficulty: Medium | Time: 50 minutes | Topic: String/HashMap

Problem Statement:
Given a string s and a list of words words, determine if s can be formed by concatenating words from the list. Each word in the list can be used at most once. The order of words in the list is not important, but each word can only be used as many times as it appears in the list. πŸ“‹ Constraints:
β€’ 1 ≀ s.length ≀ 10⁡
β€’ 1 ≀ words.length ≀ 10⁴
β€’ 1 ≀ words[i].length ≀ 15
β€’ s and words[i] contain only lowercase English letters
β€’ Multiple occurrences of same word allowed
πŸ“ Examples:
β€’ Input: s = "abcdefghijklmnop", words = ["ab","cd","ef","gh","ij","kl","mn","op"] β†’ Output: true
β€’ Input: s = "abcdefghijklmnopqrst", words = ["ab","cd","ef","gh","ij","kl","mn","op"] β†’ Output: false (missing "qrst")
β€’ Input: s = "abab", words = ["ab"] β†’ Output: false (only 1 "ab" available, need 2)
β€’ Input: s = "aaab", words = ["aa","ab"] β†’ Output: true
πŸ”„ Step-by-Step Approach:
Key Insight:
Count the frequency of each word. Then using DFS/Backtracking, try to match characters of `s` with available words, decrementing count as we use them.
🎯 Optimal Solution (Backtracking + HashMap - O(n*m) time):
function canFormString(s, words) {
  // Count frequency of each word
  const wordCount = {};
  for (const word of words) {
    wordCount[word] = (wordCount[word] || 0) + 1;
  }
  
  // Backtracking function
  function backtrack(index) {
    // If we've matched entire string, success!
    if (index === s.length) {
      return true;
    }
    
    // Try each available word
    for (const word in wordCount) {
      if (wordCount[word] > 0) {
        // Check if current position can start with this word
        if (s.substring(index, index + word.length) === word) {
          // Use this word
          wordCount[word]--;
          
          // Recursively try to match rest of string
          if (backtrack(index + word.length)) {
            return true;
          }
          
          // Backtrack - restore the word
          wordCount[word]++;
        }
      }
    }
    
    return false;
  }
  
  return backtrack(0);
}

// Test cases
console.log(canFormString("abcdefghijklmnop", 
  ["ab","cd","ef","gh","ij","kl","mn","op"]));      // true
console.log(canFormString("abcdefghijklmnopqrst", 
  ["ab","cd","ef","gh","ij","kl","mn","op"]));      // false
console.log(canFormString("abab", ["ab"]));         // false
console.log(canFormString("aaab", ["aa","ab"]));    // true
⚑ Trace Through Example:
s = "abab", words = ["ab", "ab"]
wordCount = {ab: 2}

backtrack(0):
  - Try word "ab"
  - s[0:2] = "ab" matches "ab" βœ“
  - wordCount[ab] = 1
  - backtrack(2):
    - Try word "ab"
    - s[2:4] = "ab" matches "ab" βœ“
    - wordCount[ab] = 0
    - backtrack(4):
      - index === s.length (4 === 4) β†’ return true βœ“

Result: true
⚠️ Common Mistakes:
❌ Forgetting to decrement word count when using a word
❌ Not properly backtracking (restoring) word counts
❌ Using array.join() instead of substring() for efficiency
❌ Not handling repeated words correctly
πŸ’‘ Interview Tips:

How to Approach This:

  • Clarify: "Can I use each word multiple times?" β†’ Answer: Only as many times as it appears
  • Identify: This is a backtracking problem, not greedy
  • Optimize: HashMap for O(1) word lookups
  • Test: Edge cases: empty string, single word, repeated words
  • Communicate: Explain the backtracking logic as you code

String manipulation is foundational. What Adobe really tests is problem decomposition under pressureβ€”can you think clearly when syntax isn't the bottleneck? That's the real skill. Learn to solve like the best β†’

Round 2: Async Task Queue in JavaScript (90 minutes)

Build a system to manage and execute async tasks sequentially with callbacks.

βš™οΈ Async Task Queue Manager

Difficulty: Hard | Time: 80 minutes | Topic: Async/Promises/Callbacks

Problem Statement:
Design and implement an AsyncTaskQueue class that:
  • Takes an array of async tasks during initialization
  • Processes tasks sequentially (one at a time)
  • Provides onAdd(task) - callback when a task is added
  • Provides onCompute(result) - callback when a task completes with result
  • Provides onComplete() - callback when ALL tasks are finished
πŸ“‹ Requirements:
β€’ Tasks execute sequentially (wait for previous to complete)
β€’ Handle Promise-based async tasks
β€’ Implement error handling (continue on error or stop)
β€’ Support dynamic task addition during execution
β€’ Track task progress and results
🎯 Complete Solution:
class AsyncTaskQueue {
  constructor(tasks = []) {
    this.tasks = [...tasks];      // Queue of tasks to execute
    this.isRunning = false;       // Flag to track if queue is processing
    this.currentTaskIndex = 0;    // Track which task we're on
    this.results = [];            // Store results of completed tasks
    
    // Callbacks
    this.onAddCallback = null;
    this.onComputeCallback = null;
    this.onCompleteCallback = null;
  }
  
  // Register callback for when task is added
  onAdd(callback) {
    this.onAddCallback = callback;
  }
  
  // Register callback for when task completes
  onCompute(callback) {
    this.onComputeCallback = callback;
  }
  
  // Register callback for when all tasks are done
  onComplete(callback) {
    this.onCompleteCallback = callback;
  }
  
  // Add a new task to the queue
  addTask(task) {
    this.tasks.push(task);
    
    // Trigger onAdd callback
    if (this.onAddCallback) {
      this.onAddCallback(task);
    }
    
    // If not currently running, start processing
    if (!this.isRunning) {
      this.process();
    }
  }
  
  // Process tasks sequentially
  async process() {
    if (this.isRunning) return; // Prevent multiple simultaneous processes
    
    this.isRunning = true;
    
    while (this.currentTaskIndex < this.tasks.length) {
      try {
        const task = this.tasks[this.currentTaskIndex];
        
        // Execute the async task
        const result = await Promise.resolve(task());
        
        // Store result
        this.results.push(result);
        
        // Trigger onCompute callback with result
        if (this.onComputeCallback) {
          this.onComputeCallback(result);
        }
        
        this.currentTaskIndex++;
      } catch (error) {
        console.error(`Task ${this.currentTaskIndex} failed:`, error);
        // Optionally continue or break on error
        this.currentTaskIndex++;
      }
    }
    
    // All tasks completed
    this.isRunning = false;
    
    // Trigger onComplete callback
    if (this.onCompleteCallback) {
      this.onCompleteCallback(this.results);
    }
  }
  
  // Get all results
  getResults() {
    return this.results;
  }
}

// Example Usage:
const queue = new AsyncTaskQueue();

// Register callbacks
queue.onAdd((task) => {
  console.log("πŸ“Œ Task added to queue");
});

queue.onCompute((result) => {
  console.log(`βœ… Task computed with result:`, result);
});

queue.onComplete((results) => {
  console.log("πŸŽ‰ All tasks completed!", results);
});

// Add tasks
queue.addTask(() => 
  new Promise(resolve => {
    setTimeout(() => resolve("Task 1 done"), 1000);
  })
);

queue.addTask(() => 
  new Promise(resolve => {
    setTimeout(() => resolve("Task 2 done"), 500);
  })
);

queue.addTask(() => {
  return "Task 3 done (synchronous)";
});
πŸ“ Real-World Example - Processing API Requests:
const apiQueue = new AsyncTaskQueue();

// Setup callbacks
apiQueue.onCompute((data) => {
  console.log("API Response:", data);
});

apiQueue.onComplete((allResults) => {
  console.log("All API calls completed:", allResults);
});

// Add API calls as tasks
apiQueue.addTask(() => 
  fetch('https://api.example.com/users/1').then(r => r.json())
);

apiQueue.addTask(() => 
  fetch('https://api.example.com/posts/1').then(r => r.json())
);

apiQueue.addTask(() => 
  fetch('https://api.example.com/comments/1').then(r => r.json())
);
πŸ’‘ Interview Approach:

Key Points to Discuss:

  • Sequential Processing: Tasks execute one after another, not in parallel
  • State Management: Track current index, results, and running status
  • Error Handling: Decide whether to continue or stop on error
  • Dynamic Addition: Support adding tasks while queue is running
  • Callback Pattern: Implement clean callback mechanism
  • Promise Handling: Use async/await for cleaner code
⚠️ Common Issues:
❌ Processing tasks in parallel instead of sequentially
❌ Not properly handling Promise.resolve() for sync tasks
❌ Forgetting to increment currentTaskIndex
❌ Not preventing multiple concurrent process() calls
❌ Losing results or not exposing them properly

Async code separates the safe coders from the ones who understand systems. Adobe isn't looking for someone who knows async/await syntaxβ€”they want engineers who can reason about concurrency and callbacks under real-world constraints. Build systems with confidence β†’

Round 3: React & JavaScript Fundamentals (70 minutes)

Core concepts testing and practical React patterns. Expect deep questions on rendering, hooks, and optimization.

βš›οΈ Component Rendering Patterns in React

Difficulty: Medium | Time: 20 minutes

Question:
You need to render a list of items recursively (components within components, nested lists, tree structures). How would you build this in React? What are the pitfalls? Explain the importance of the `key` prop and show how using the array index as a key can break your component. 🎯 Model Answer:
// ❌ WRONG: Using array index as key
function RecursiveListBad({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          {item.name}
          {item.children && item.children.length > 0 && (
            <RecursiveListBad items={item.children} />
          )}
        </li>
      ))}
    </ul>
  );
}

// Problem: If you reorder items or filter them, React gets confused
// It uses index 0, 1, 2... which doesn't map to actual items

// βœ… CORRECT: Using unique identifier as key
function RecursiveListGood({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}
          {item.children && item.children.length > 0 && (
            <RecursiveListGood items={item.children} />
          )}
        </li>
      ))}
    </ul>
  );
}

// Real-world example with dynamic list
const TreeNode = ({ node }) => {
  const [expanded, setExpanded] = useState(false);
  
  return (
    <div style={{ marginLeft: '20px' }}>
      <div onClick={() => setExpanded(!expanded)}>
        {expanded ? 'β–Ό' : 'β–Ά'} {node.name}
      </div>
      {expanded && node.children && (
        <div>
          {node.children.map(child => (
            <TreeNode key={child.id} node={child} />
          ))}
        </div>
      )}
    </div>
  );
};

// Usage
const fileTree = {
  id: '1',
  name: 'root',
  children: [
    { id: '2', name: 'folder1', children: [
      { id: '3', name: 'file1.js' }
    ]}
  ]
};

export default function FileExplorer() {
  return <TreeNode node={fileTree} />;
}
πŸ”‘ Key Insights:
Why the key prop matters: React uses keys to match elements between renders. Without proper keys, when you filter or reorder a list, React might reuse the wrong component instance, causing state bugs and performance issues.

Index as key: Works ONLY if list is static and never filtered/reordered. Using index breaks when items are added/removed.

Recursive components: Must have unique keys at each level of recursion to prevent React reconciliation bugs.

🎣 useEffect Cleanup & Dependency Arrays

Difficulty: Medium | Time: 20 minutes

Question:
Explain the difference between these three useEffect patterns and when to use each:
  1. useEffect(() => { ... }, [])
  2. useEffect(() => { ... }, [dependency])
  3. useEffect(() => { ... }) with no dependency array
Also explain what cleanup functions do and give an example where improper cleanup causes a memory leak. 🎯 Model Answer:
// Pattern 1: Run once on mount only
useEffect(() => {
  console.log('Component mounted');
  // Perfect for: API calls, subscriptions, event listeners
}, []);
// Cleanup runs on unmount


// Pattern 2: Run when dependency changes
useEffect(() => {
  console.log('UserId changed:', userId);
  // Perfect for: Reacting to prop/state changes
}, [userId]);
// Cleanup runs before effect runs again + on unmount


// Pattern 3: Run after EVERY render (DANGEROUS!)
useEffect(() => {
  console.log('Runs after every render!');
  // Avoid this! Can cause infinite loops and performance issues
});


// ❌ MEMORY LEAK: Event listener not cleaned up
function ChatWindow({ userId }) {
  useEffect(() => {
    const handleNewMessage = (message) => {
      console.log('New message:', message);
    };
    
    // Subscribe to WebSocket
    socket.on('message', handleNewMessage);
    
    // Missing cleanup! listener stays registered even after unmount
  }, [userId]);
  
  return <div>Chat</div>;
}

// βœ… CORRECT: Cleanup function unsubscribes
function ChatWindow({ userId }) {
  useEffect(() => {
    const handleNewMessage = (message) => {
      console.log('New message:', message);
    };
    
    // Subscribe
    socket.on('message', handleNewMessage);
    
    // Cleanup function removes listener
    return () => {
      socket.off('message', handleNewMessage);
    };
  }, [userId]);
  
  return <div>Chat</div>;
}


// Real-world example: API call with proper cleanup
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchUser = async () => {
      const data = await fetch(`/api/users/${userId}`);
      const user = await data.json();
      
      // Only update state if component is still mounted
      if (isMounted) {
        setUser(user);
        setLoading(false);
      }
    };
    
    fetchUser();
    
    // Cleanup: mark component as unmounted
    return () => {
      isMounted = false;
    };
  }, [userId]);
  
  return loading ? <div>Loading...</div> : <div>{user.name}</div>;
}
πŸ”‘ Common Mistakes:
❌ Missing dependency in array β†’ stale closures
❌ Including all dependencies β†’ unnecessary re-runs
❌ Not cleaning up subscriptions β†’ memory leaks
❌ Putting objects in dependency array β†’ infinite loops

πŸ”„ React's Reconciliation Algorithm & Re-renders

Difficulty: Hard | Time: 20 minutes

Question:
Explain when a React component re-renders. Why does this code cause an infinite loop? How would you fix it?
// ❌ CAUSES INFINITE LOOP
function BadExample() {
  const [count, setCount] = useState(0);
  
  // Problem: onUserAction is recreated every render
  // Even though the function body is the same, it's a new reference
  const onUserAction = () => setCount(count + 1);
  
  useEffect(() => {
    // Problem: onUserAction is in dependency array
    // It changes every render β†’ effect runs every render
    // Effect creates new onUserAction β†’ infinite loop!
    element.addEventListener('click', onUserAction);
    
    return () => element.removeEventListener('click', onUserAction);
  }, [onUserAction]); // ❌ Bad dependency
  
  return <button>Count: {count}</button>;
}
βœ… Solutions:
// Solution 1: Use useCallback to memoize the function
function GoodExample1() {
  const [count, setCount] = useState(0);
  
  const onUserAction = useCallback(() => {
    setCount(c => c + 1); // Use functional update
  }, []); // No dependencies = function never changes
  
  useEffect(() => {
    element.addEventListener('click', onUserAction);
    return () => element.removeEventListener('click', onUserAction);
  }, [onUserAction]); // Safe dependency
  
  return <button>Count: {count}</button>;
}

// Solution 2: Use functional setState (recommended)
function GoodExample2() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const onUserAction = () => setCount(c => c + 1);
    element.addEventListener('click', onUserAction);
    
    return () => element.removeEventListener('click', onUserAction);
  }, []); // No dependencies needed!
  
  return <button>Count: {count}</button>;
}
πŸ’‘ Why Re-renders Happen:
1. Parent component re-renders (props passed down)
2. State changes (setState called)
3. Context value changes
4. Key prop changes

Performance tip: Use React.memo to prevent re-renders of child components when parent re-renders unnecessarily

Interview Tips for React/JS Round:

  • Understand the "why": Don't just know the syntax, explain the reasoning behind React's design decisions
  • Discuss performance: Show awareness of useCallback, useMemo, React.memo, code splitting
  • Handle edge cases: Mention stale closures, memory leaks, race conditions in async operations
  • Write production-ready code: Error handling, null checks, cleanup functions
  • Connect to real projects: Share examples from your own experience with large codebases

React is a tool, JavaScript is the craft. What Adobe values is engineers who deeply understand why React exists, not just how to use itβ€”the patterns, the philosophy, the performance thinking. Master the philosophy β†’

Round 4: Computer System Fundamentals (45 minutes)

Deep dive into system-level concepts and their impact on application performance.

πŸ–₯️ CPU Cache and Memory Hierarchy Impact

Difficulty: Hard | Time: 40 minutes

Question:
Explain CPU cache (L1, L2, L3) and memory hierarchy. How does it affect application performance? Write pseudo-code showing the performance difference between cache-friendly and cache-unfriendly memory access patterns. πŸ—οΈ Memory Hierarchy (from fastest to slowest):
1. CPU Registers - ~1 cycle, ~32KB (per core)
2. L1 Cache - ~4 cycles, ~32KB (per core)
3. L2 Cache - ~10 cycles, ~256KB (per core)
4. L3 Cache - ~40 cycles, ~8MB (shared by cores)
5. RAM (Main Memory) - ~200 cycles, ~8GB
6. Disk (SSD/HDD) - ~10,000,000 cycles, ~500GB+
πŸ’‘ Cache-Friendly vs Cache-Unfriendly:
// ❌ CACHE-UNFRIENDLY (Row-major vs Column-major mismatch)
function sumMatrixUnfriendly(matrix) {
  let sum = 0;
  const rows = matrix.length;
  const cols = matrix[0].length;
  
  // Accessing column by column in row-major stored matrix
  for (let col = 0; col < cols; col++) {
    for (let row = 0; row < rows; row++) {
      sum += matrix[row][col];  // Huge cache misses!
    }
  }
  return sum;
}

// βœ… CACHE-FRIENDLY (Sequential memory access)
function sumMatrixFriendly(matrix) {
  let sum = 0;
  const rows = matrix.length;
  const cols = matrix[0].length;
  
  // Accessing row by row (sequential memory access)
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      sum += matrix[row][col];  // Cache hits!
    }
  }
  return sum;
}

// Performance Test
const matrix = Array(10000).fill(0).map(() => 
  Array(10000).fill(Math.random())
);

console.time("Unfriendly");
sumMatrixUnfriendly(matrix);
console.timeEnd("Unfriendly");  // ~500ms

console.time("Friendly");
sumMatrixFriendly(matrix);
console.timeEnd("Friendly");    // ~100ms (5x faster!)
🎯 Key Concepts for Frontend Engineers:
1. Spatial Locality: Access memory sequentially (cache lines ~64 bytes)
2. Temporal Locality: Reuse recently accessed data before moving to new data
3. Cache Line Utilization: One memory fetch gets 64 bytes, use all of it
4. Branch Prediction: CPU predicts conditional branches, misprediction = wasted cycles
πŸ“Š Practical Impact on Web Performance:
// DOM Operations - Cache misses with scattered DOM nodes
// ❌ Unfriendly: Touching scattered DOM nodes
for (let i = 0; i < 1000; i++) {
  document.getElementById(`item-${i}`).style.color = 'red';
  // Each query walks the DOM tree (cache unfriendly)
}

// βœ… Friendly: Batch DOM reads, then batch DOM writes
const nodes = [];
for (let i = 0; i < 1000; i++) {
  nodes.push(document.getElementById(`item-${i}`));
}
for (const node of nodes) {
  node.style.color = 'red';
}

// or better yet: Use DocumentFragment or classList batch operations
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const item = document.getElementById(`item-${i}`);
  item.classList.add('highlight');
}
// Triggers single reflow instead of 1000
πŸš€ Real-World Optimization:
For a Frontend Developer:
β€’ Avoid layout thrashing (alternating reads/writes)
β€’ Batch DOM updates using DocumentFragment
β€’ Cache DOM node references instead of querying repeatedly
β€’ Use requestAnimationFrame to batch visual updates
β€’ Minimize reflows by grouping style changes
πŸ“ Sample Answer Structure:

How to Answer This Interview Question:

  • Explain Hierarchy: Start with why caches exist (speed gap between CPU and RAM)
  • Define Levels: Talk about L1, L2, L3 latencies and sizes
  • Spatial Locality: Sequential access is faster than random
  • Show Code: Matrix example clearly demonstrates the impact
  • Connect to Frontend: Relate back to DOM operations and rendering performance
  • Mention Trade-offs: Sometimes less optimal code is more readable (engineering decision)

System thinking isn't about memorizing architecture patterns. It's about understanding trade-offsβ€”latency vs throughput, consistency vs availability, simplicity vs scalability. That's where Adobe separates the engineers who code from those who architect. Think like an architect β†’

Round 5: HR Discussion (30 minutes)

General HR conversation about experience, growth, and fit.

πŸ‘₯ HR Round Overview

Typical HR Questions & Approach:
Q: Tell me about yourself
A: (30 seconds) "I'm a frontend engineer with 5 years experience, primarily working with React and system design. Most recently at [Company], I led initiatives in performance optimization, resulting in 40% improvement in page load times. I'm passionate about building scalable, user-centric applications and mentoring junior developers."
Q: Why Adobe?
A: "Adobe's tools shape how creatives work globally. I'm particularly interested in your work on [specific product/technology]. The opportunity to work on systems used by millions appeals to me, and I see great alignment with my background in performance optimization and user experience."
Q: Why are you changing jobs?
A: "While I've grown significantly at [current company], I'm looking for the next challenge. Adobe's scale, tech stack, and commitment to innovation excite me. I see an opportunity to expand my expertise in [specific area]."
Q: What are your salary expectations?
A: "Based on my experience and market research for senior roles in this region, I'm expecting β‚Ή80-95 LPA. I'm flexible based on the overall compensation package including equity and benefits."
🎯 Interview Verdict: βœ“ SELECTED
πŸŽ‰ Result: OFFER EXTENDED

Position: Senior Software Engineer (Frontend)
Package: β‚Ή87 LPA (base) + equity + benefits
Location: Bangalore (with hybrid flexibility)
Start Date: June 15, 2026

Performance Summary:
βœ… Round 1 (String Algorithm): Optimal solution, good explanation
βœ… Round 2 (Async Queue): Clean implementation, handled edge cases
βœ… Round 3 (System Fundamentals): Deep understanding, practical knowledge
βœ… Round 4 (HR): Confident, clear communication, good cultural fit
πŸ’‘ Key Takeaways:

What Went Well in This Interview:

  • Technical Depth: Demonstrated system-level thinking, not just surface-level coding
  • Communication: Explained reasoning clearly during each round
  • Problem-Solving: Optimized solutions without leaving at first approach
  • Real-World Connection: Related technical concepts back to practical applications
  • HR Fit: Clear about career goals and alignment with company values
Adobe

You Now Know What Adobe Expects πŸ‘†

From string algorithms to system caching strategiesβ€”this is the complete Adobe interview roadmap. But knowing what to expect and being able to deliver under pressure are two different things.

Our cohort has helped developers crack interviews at Adobe, Microsoft, Uber, Atlassian, and Amazon. They didn't just learn the conceptsβ€”they learned to think like an Adobe engineer.

What our cohort members get:

  • βœ… Structured 8-week curriculum covering all 5 rounds
  • βœ… 50+ mock interviews (real-time, recorded, feedback)
  • βœ… 1-on-1 mentoring with engineers from Adobe, Microsoft, Uber, etc.
  • βœ… Private community (25 dedicated developers)
  • βœ… Lifetime access to recordings & materials
  • βœ… 100% money-back if you don't get offers

Cohort 3 starts October 2026. Limited to 25 seats. Join the waitlist now.