Real Adobe Interview Experience
4 Rounds: String Algorithms, Async JavaScript, System Basics & HR
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
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 β€ words.length β€ 10β΄
β’ 1 β€ words[i].length β€ 15
β’ s and words[i] contain only lowercase English letters
β’ Multiple occurrences of same word allowed
β’ 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:
trueCount the frequency of each word. Then using DFS/Backtracking, try to match characters of `s` with available words, decrementing count as we use them.
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:β Not properly backtracking (restoring) word counts
β Using array.join() instead of substring() for efficiency
β Not handling repeated words correctly
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
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
β’ Handle Promise-based async tasks
β’ Implement error handling (continue on error or stop)
β’ Support dynamic task addition during execution
β’ Track task progress and results
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
β 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
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: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
Explain the difference between these three useEffect patterns and when to use each:
- useEffect(() => { ... }, [])
- useEffect(() => { ... }, [dependency])
- useEffect(() => { ... }) with no dependency array
// 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:β 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
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: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
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):
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-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: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
// 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:β’ 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
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
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."
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."
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]."
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."
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
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
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.
