Adobe engineers work on a stack almost no other company has: thirty-year-old C++ desktop applications like Photoshop and Illustrator, a modern web layer in Adobe Express and Firefly, a document engine in Acrobat that has to parse every malformed PDF ever produced, and Adobe Experience Platform ingesting billions of customer events a day. That range is what makes the interview unpredictable. Two candidates for the same job title can get completely different loops — one a React and Node.js assessment, another a deep dive on smart pointers and object lifetimes, another a conversation about serving generative AI inference under load. This question bank prepares you for the breadth rather than betting on one track.
Most candidates report three to five rounds spread over four to ten weeks. It opens with a recruiter screen, then an online assessment on HackerRank or CodeSignal, then two or three technical interviews, then a hiring manager round.
The technical rounds vary sharply by team. Creative Cloud desktop teams push on C++ fundamentals, memory ownership and performance. Express and web teams run frontend-heavy problems in JavaScript, React and Node. Document Cloud teams care about parsing, correctness and security. Newer Firefly and Sensei teams increasingly bring generative AI infrastructure into the conversation. The hiring manager round is a genuine fit conversation, not a formality.
The consistent signal across all of them: interviewers want your reasoning out loud. Candidates who reach a correct answer silently score worse than candidates who reason to a slightly weaker answer while narrating the trade-offs.
A designer in London and a designer in Singapore both have a 4 GB Photoshop file open from the same Creative Cloud folder. Both are offline for two hours. Both make edits. Both come back online within the same minute. Today the product silently keeps the last write and the London designer loses an hour of work — we get support tickets about this every week. Design the sync and conflict-resolution layer. How do you detect the conflict, what do you do about the 4 GB payload, and what does the user actually see? Assume you cannot change the PSD file format.
Why interviewers ask this
This tests distributed systems judgment in a setting where the naive answer — last write wins — is exactly what is already broken. Weak candidates reach for a generic CRDT or operational transform answer without asking whether a binary blob can be merged at all. Strong candidates separate the three distinct problems hiding here: detecting divergence, moving bytes efficiently, and choosing a user-facing resolution that does not destroy work.
Example strong answer
I would start by separating detection from resolution, because they have different constraints.
For detection, I would not rely on timestamps — clock skew across regions makes them unreliable, and that is likely part of why last-write-wins is failing today. I would attach a version vector to each file: a small map of client ID to a monotonic counter, stored in metadata alongside the file rather than inside the PSD, since I cannot change the format. When a client uploads, it sends the version vector it started from. The server compares: if the incoming vector dominates the stored one, it is a clean fast-forward. If neither dominates, the edits are concurrent and we have a genuine conflict. This gives correct detection without any dependency on wall-clock time.
For the payload, uploading 4 GB twice is unacceptable on a Singapore link. I would use content-defined chunking — a rolling hash such as Rabin fingerprinting to split the file into variable-size chunks averaging maybe 4 MB, then upload only chunks whose hashes the server does not already hold. PSD files are layered, so a designer editing one layer typically rewrites a bounded region; in practice this often means transferring tens of megabytes instead of four gigabytes. Chunks are content-addressed, so deduplication across versions and across users is free. I would keep the chunk index small enough to fit comfortably in memory on the client.
For resolution, I would refuse to auto-merge. A PSD is a binary composite; a byte-level merge produces a corrupt file, which is worse than losing an hour of work because it fails silently and later. Instead I would preserve both: the server keeps both versions as siblings, and the client surfaces them as Poster.psd and Poster (London, 14:20).psd, with both users notified. This is the Dropbox conflicted-copy model and it is the right call because the cost of being wrong is asymmetric — a duplicate file is annoying, a corrupt or silently discarded file is a lost afternoon.
I would then add one thing beyond conflict handling: presence. Most of these tickets exist because neither designer knew the other was in the file. A lightweight lock or an ambient indicator showing who else has the document open prevents far more conflicts than any resolution strategy resolves. I would measure success by conflicted-copy rate per thousand sessions and by support ticket volume on this theme, and I would expect presence to move that number more than the merge logic does.
The honest trade-off is storage cost. Keeping both sides plus chunk history grows the store. I would cap version retention at thirty days for conflicted copies and garbage-collect unreferenced chunks on a background sweep.
Follow-up questions
Firefly image generation runs on a fixed pool of GPU workers. Median generation takes about eight seconds. Adobe announces a free tier at MAX and traffic goes up eleven times within twenty minutes. The GPU pool cannot scale for at least forty minutes because capacity has to be provisioned. Right now requests queue until the load balancer times them out at thirty seconds, so users see a spinner and then a generic error, and many immediately retry — which makes it worse. Design the degradation strategy. What do you shed, what do you queue, and what does a user on the free tier see?
Why interviewers ask this
This is a load-shedding and backpressure question dressed in a product story, and it is increasingly common on Firefly and Sensei teams. Weak candidates say "autoscale" and stop, ignoring the explicit forty-minute constraint. Strong candidates recognise the retry storm as the real amplifier and design for honesty to the client rather than silent queueing.
Example strong answer
The first thing I would fix is not capacity, it is the retry amplification. At eleven times load with a thirty-second timeout and client retries, effective load is well above eleven times and climbing. Every second the system stays in that state it gets further behind. So my first move is to make failure fast, explicit, and non-retryable from the client's perspective.
Concretely, I would put an admission controller in front of the queue that knows the current queue depth and the measured service rate. With a pool that drains at roughly the pool size divided by eight seconds, I can compute the expected wait for a request joining the queue. If that expected wait exceeds a threshold — say sixty seconds — I reject at admission with a 429 and a Retry-After header carrying a real number, not a generic 503. That header matters: it converts an uncoordinated retry storm into a scheduled, spread-out return. I would add jitter to the value so clients do not synchronise into a thundering herd at the same second.
Second, I would make the queue bounded and prioritised rather than unbounded and FIFO. Unbounded queues under overload are a trap — they convert a capacity problem into a latency problem and every request eventually times out having consumed real GPU time. I would size the queue to roughly the work the pool can clear in sixty seconds and drop at the tail beyond that. For priority, paid Creative Cloud subscribers go into a higher-priority class than free tier. This is a business decision but it is defensible and it is what the free tier launch implicitly signed up for: paid users should not experience the launch as an outage.
Third, degrade the work itself rather than only the admission. Firefly generation has quality knobs — inference steps, output resolution. Under sustained overload I would serve free-tier requests at reduced steps and a smaller default resolution, which cuts per-request GPU time meaningfully and raises effective throughput without adding hardware. The user gets a real image, faster, and I would label it honestly with an option to regenerate at full quality later.
Fourth, the user experience during the wait. A spinner that ends in a generic error is the worst possible outcome because it wastes the user's time and teaches them the product is broken. If I admit a request, I show a real queue position and estimated time derived from the same admission-controller maths. If I reject, I say so immediately and offer to notify when capacity frees up, which converts a failure into an email capture rather than an abandonment.
Finally, I would make sure this whole path is exercised before MAX rather than during it. I would run a load test at twelve times projected traffic against a scaled-down pool to validate that the admission controller, the priority classes and the degraded quality path all behave, and I would put the shed rate and p99 admitted latency on a dashboard the on-call actually watches.
Follow-up questions
Photoshop loads third-party plugins as dynamic libraries into its own process. A plugin can hold a reference to a layer object, and the user can delete that layer while the plugin still holds it. We currently hand plugins a raw pointer. We are seeing crash reports where the plugin dereferences a layer that the host has already freed, and because the plugin is third-party code we cannot fix it in their binary. Redesign the ownership model at the host boundary. You cannot break the existing plugin API in a way that requires every plugin to be recompiled.
Why interviewers ask this
This is the classic Adobe desktop question and it is a genuine problem the Creative Cloud teams live with. Weak candidates answer "use shared_ptr" without thinking about what shared ownership means when the other owner is untrusted code you cannot audit. Strong candidates recognise that the host must retain authority over lifetime while still handing out something safe, and that this points at a handle or weak-reference design rather than shared ownership.
Example strong answer
The core constraint is that the plugin is untrusted and unfixable, so any design where the plugin's behaviour determines whether the host is correct is the wrong design. That rules out plain shared_ptr handed across the boundary: if a plugin stores one and never releases it, the layer leaks and, worse, the document's own notion of what layers exist diverges from what memory says. The host must stay the sole owner.
What the plugin actually needs is a way to ask "is this thing still alive, and if so give me access for the duration of this call." That is a weak_ptr in spirit, but I would not put a C++ weak_ptr in the ABI, because standard library types across a DLL boundary compiled with a different toolchain is how you get subtle ABI breakage. Instead I would expose an opaque integer handle.
The host keeps a handle table: a slot array where each slot holds a generation counter and a pointer to the layer. A handle is the slot index packed with the generation counter, say 32 bits each in a 64-bit value. When a layer is destroyed the host clears the slot and increments its generation. When a plugin passes a handle back, the host decomposes it, checks that the slot's current generation matches the handle's generation, and if it does not, returns a clean error. A stale handle is now a detectable, non-crashing condition rather than a dangling pointer. Slot reuse is safe because the generation counter changes on every reuse.
For the access window, I would give plugins an explicit scoped acquire and release pair, where acquire returns a raw pointer valid only until release, and the host pins the layer for that window. Pinning means deletion during the window is deferred rather than blocked, so the UI does not hang if a plugin is slow. If a plugin never calls release, I would time-bound the pin and log it rather than leak forever.
On not breaking existing plugins: the current API hands out raw pointers, so I would keep that entry point and make it a thin shim that acquires a handle, resolves it, and returns the pointer as before. Old plugins keep working with exactly today's semantics, including today's risk. New and recompiled plugins get the handle API. I would then instrument the shim so we can see which plugins still use it and how often, and use that data to drive deprecation with the partner ecosystem team rather than by fiat.
The trade-off is indirection cost on every access. In practice the handle lookup is an array index and a comparison, which is negligible next to any real layer operation, but I would benchmark it in a tight filter loop before committing, because Photoshop performance work is unforgiving and a per-pixel path would be a different conversation entirely.
Follow-up questions
Adobe Express runs in the browser. A user has a design with roughly two hundred elements — text, shapes, images, a few grouped layers. When they drag one element, the whole canvas stutters and we measure frame times around forty milliseconds. The React component tree re-renders broadly on every pointer move. Product says dragging must feel native. You have the existing React codebase and cannot rewrite it into a game engine. Get the drag interaction to a consistent sixteen milliseconds and explain how you would know you succeeded.
Why interviewers ask this
Express and web teams run frontend performance problems like this instead of abstract algorithm puzzles. Weak candidates list generic optimisations — memoise everything, use useCallback — without diagnosing. Strong candidates profile first, identify that the fix is to take the dragged element out of React's render path entirely during the gesture, and can talk about compositing versus layout.
Example strong answer
I would not optimise anything before I have a profile, because forty milliseconds could be scripting, layout, paint or compositing and each has a different fix. I would record a performance trace during a drag and look at where the frame budget goes. With two hundred elements and broad React re-renders, my prior is that most of it is scripting — React reconciliation across the whole tree — with a secondary cost in layout if elements are positioned with top and left.
Assuming the trace confirms that, the central insight is that during a drag, exactly one element's transform is changing. Everything else on the canvas is static. So the goal is to make the drag not go through React at all.
I would hoist the dragged element into its own compositor layer at gesture start and drive it with a CSS transform: translate3d() written directly to the DOM node via a ref, updated inside a requestAnimationFrame callback fed by pointer events. transform and opacity are the only two properties the browser can animate on the compositor without layout or paint, so this turns each frame into a compositor-only update, which is well inside budget. React state is updated once, on pointer up, with the final position. During the gesture React renders zero times.
For the pointer input itself I would coalesce. Pointer events can fire more often than the display refreshes, so I would store the latest event position in a ref and read it once per animation frame rather than doing work per event. On browsers that support it I would also use getCoalescedEvents to keep the path accurate for anything that needs the full input trace.
Beyond the dragged element, two hundred static elements still cost paint if they are all in one layer that gets invalidated. I would check whether the drag causes repaints of the static content — the paint flashing tool makes this obvious — and if so, put the drag layer above the static canvas so the static content is never invalidated. I would avoid promoting all two hundred elements to their own layers, because layer explosion costs GPU memory and can make things worse; the rule is promote the thing that moves, not everything.
If scripting is still hot after that, I would look at the selection and snapping logic. Snapping often does an O(n) pass over all elements per frame to find alignment guides. At two hundred elements that is survivable, but I would precompute the candidate edge positions once at gesture start into sorted arrays and binary search per frame, since the static elements cannot move mid-drag.
For verification I would not trust a subjective "feels smoother." I would add a performance mark around the drag gesture and record the frame time distribution, then report p95 and p99 frame time rather than the mean, because stutter is a tail phenomenon. I would set the bar as p99 under sixteen milliseconds on a mid-tier laptop, not a developer machine, and add a regression test in CI that fails if the p95 for a scripted two-hundred-element drag exceeds the threshold.
Follow-up questions
Acrobat has to open PDFs produced by thousands of generators over thirty years, many of which do not follow the spec. A customer sends us a file where the cross-reference table points to byte offsets that are wrong, one object claims a length that runs past the end of the file, and there is a recursive reference where object A's page tree contains object B which contains object A. Today the parser either crashes or hangs. Design the parsing strategy. How do you recover a readable document without becoming a security liability?
Why interviewers ask this
Document Cloud interviews probe correctness and security thinking under adversarial input, because a PDF parser is an attack surface reachable by anyone who can email a file. Weak candidates treat it as a validation exercise and reject the file. Strong candidates recognise that rejecting is not an option commercially — competitors open the file — and design for recovery plus containment.
Example strong answer
I would frame this as three separate obligations: never crash, never hang, and never let a malformed file cause the process to do something it should not. Opening the document successfully is a fourth goal, and it is subordinate to the first three.
For the broken cross-reference table, the spec-compliant path reads the trailer, finds the xref offset and jumps directly to objects. When those offsets are wrong, I would fall back to reconstruction: scan the file linearly for N M obj patterns and rebuild the xref from what is actually present. This is what robust readers do and it handles the common case of a file that was appended to or truncated by a bad tool. I would treat the reconstructed table as authoritative and log that reconstruction happened, because that telemetry tells us which generators in the wild are producing bad files.
For the object whose declared length runs past end of file, I would never trust a declared length as a read bound. Every read is clamped to the actual remaining bytes in the buffer. For a stream, I would additionally scan forward for the endstream keyword and take the shorter of the declared length and the scanned length. If the stream is compressed and the truncated data fails to inflate, I decode as much as inflate gives me and mark the object partial rather than discarding it — a page that renders with the bottom third missing is more useful than an error dialog.
For the recursive reference, the fix is cycle detection during traversal. I would carry a set of object numbers on the current resolution path and return a null object if an object number reappears, rather than recursing. I would also cap total resolution depth, because a deeply nested but acyclic structure is equally capable of blowing the stack, and a stack overflow in a parser is a crash at best and exploitable at worst. Both limits get a clear internal error, not an exception that unwinds through half the renderer.
On containment, this is the part I would push hardest in a real design review. Parsing untrusted input should not run with the privileges of the application. I would parse and render in a sandboxed child process with no filesystem write access, no network, and a restricted syscall policy, communicating with the host over a narrow IPC surface that only carries rendered tiles and structured document metadata. If a malformed file does find a memory-safety bug we have not caught, the blast radius is a crashed sandbox and a retry, not code execution on the user's machine. I would also put hard resource ceilings on that process — wall-clock, memory and object count — so a decompression bomb or a pathological object graph is a bounded failure.
On confidence, I would not rely on the three cases in this ticket. I would stand up continuous fuzzing with a coverage-guided fuzzer seeded from a corpus of real-world PDFs, run under address and undefined-behaviour sanitisers. Every crash becomes a regression test. That is the only way to get ahead of a format this old and this widely abused.
The user-facing outcome I would aim for is: the file opens, a non-blocking banner says the document had errors and was repaired, and nothing about the failure mode is silent to us in telemetry.
Follow-up questions
N M obj finds two objects with the same number. Which one wins, and why?Experience Platform ingests customer behaviour events from client SDKs — web, iOS, Android. A retail customer sends about four hundred thousand events per second at peak. Mobile SDKs retry on network failure, so duplicates arrive, sometimes minutes apart. Events can also arrive late, occasionally hours late, because a device was offline. Downstream, marketers build audience segments off these events, and a duplicated purchase event means someone gets counted twice and receives the wrong campaign. Design the deduplication. What are you willing to be wrong about?
Why interviewers ask this
This is a streaming systems question with an explicit correctness cost, and the closing line is deliberate: no dedup scheme is exact at this scale and horizon, so the interviewer is testing whether you will name the trade-off rather than claim a perfect solution.
Example strong answer
The first question I would ask is whether the SDK sends a stable event ID. If it does — a UUID generated once at event creation and reused across retries — then dedup is an idempotency-key problem and everything downstream gets simpler. If it does not, I would push to add it, because doing dedup by content hashing is strictly worse: two genuine page views of the same page seconds apart are legitimately distinct events and content hashing cannot tell them from a retry. So my first move is to make the producer part of the solution rather than trying to fix it entirely at the consumer.
Given a stable event ID, the design is a keyed state store with a bounded retention window. I would partition the stream by the ID so that all copies of an event land on the same worker, then keep a set of seen IDs per partition. Retention is the crux: keeping every ID forever at four hundred thousand per second is not viable, so I choose a window. A twenty-four-hour window covers the overwhelming majority of retry and offline cases. Anything later than that is admitted as a new event.
That is the thing I am willing to be wrong about, and I would say so explicitly: an event that arrives more than twenty-four hours after its original will be double-counted. I would size that window from the data rather than by intuition — measure the distribution of arrival delay minus event timestamp across the customer's actual traffic and pick a window that covers, say, the 99.9th percentile, then revisit it quarterly.
For the state store itself, an exact set of IDs over twenty-four hours is large but tractable if I shard it and use RocksDB-backed state with the ID as key and the event timestamp as value, expired by TTL. I would resist a Bloom filter as the primary mechanism here even though it is memory-cheap, because a false positive means silently dropping a real purchase event, and dropping revenue data is a worse failure than storing it twice. If memory pressure forced my hand I would use the Bloom filter only as a negative cache — a definite "not seen" fast path — and fall through to the exact store on a possible hit, which gives the memory win without the false-drop risk.
For late data, I would use event-time processing with watermarks rather than processing-time windows, so a segment computed for Tuesday reflects events that happened on Tuesday regardless of when they landed. Late events past the watermark go to a side output that triggers a recomputation of affected segments rather than being dropped. Marketers care more about eventual correctness of a segment than about it being final within a second.
Finally, I would make the duplication rate itself an observable metric per data source, broken out by SDK version. In my experience the largest single win in problems like this is not the dedup algorithm — it is discovering that one SDK version on one platform has a retry bug producing most of the duplicates, and fixing that at the source.
Follow-up questions
Lightroom mobile has a bug: about two percent of users report that exporting an edited photo produces a corrupted JPEG. It only happens on Android. It does not reproduce on any device in our lab. Crash reporting shows nothing, because the app does not crash — it writes a bad file and reports success. You have access to production telemetry and can ship a build. Walk me through how you find this.
Why interviewers ask this
Adobe hiring managers use debugging questions to see whether a candidate has actually shipped software to a large, diverse device population. Weak candidates propose adding logging and stop. Strong candidates narrow the population statistically before touching code, and think about how to get evidence without creating a privacy problem.
Example strong answer
Two percent and Android-only is a strong hint that this correlates with device, OS version or storage configuration rather than being a logic bug in the export path, because a logic bug would generally reproduce everywhere. So my first move is analysis, not instrumentation.
I would pull the population of users who reported corruption and compare it against a matched control of users who exported successfully in the same period, then look for dimensions where the distributions diverge: OEM and model, Android version, available free storage, whether the export target is internal storage or an SD card, file size, whether the source was RAW or JPEG, and whether the app was backgrounded during export. In my experience one of those separates cleanly, and when it does the hypothesis space collapses immediately. My prior here is SD card writes or a backgrounding interaction, because both are Android-specific and both are underrepresented in a lab full of flagship phones with plenty of internal storage.
While that analysis runs, I would add cheap integrity evidence rather than verbose logging. After writing the JPEG I would read the file back and validate it: check the SOI and EOI markers, confirm the byte length matches what we believe we wrote, and compare a hash of the buffer before write against a hash of the file after write. If those hashes differ, the corruption is in the write path. If they match but the file is still bad downstream, the corruption is upstream in encoding. That single bit of information splits the problem in half and costs one extra read of a file we just wrote.
I would ship that as a staged rollout to a small percentage, reporting only a compact structured event: a failure flag, the divergence point, device model, OS version, storage type and free bytes. No image content, no filenames, nothing that would make this a privacy review problem.
If the evidence points at the write path, the usual culprits on Android are failing to flush and close before reporting success, the process being killed mid-write when backgrounded, or MediaStore and scoped storage semantics differing across OEM implementations. The fix pattern is to write to a temporary file, flush and fsync, verify, then atomically rename into place, so a killed process leaves a temp file rather than a half-written JPEG the user believes is good. That also fixes the reporting-success-on-failure problem, which is arguably the worse bug — the user trusted us and deleted the original.
If the evidence points upstream at encoding, I would check whether we are using a hardware encoder on affected devices and fall back to a software path on the identified models. Unglamorous, but it is how this class of bug is usually resolved.
Throughout, I would keep the corruption rate as a tracked metric with device dimensions attached, so the fix is validated by the rate going to zero in the affected population rather than by the absence of new complaints.
Follow-up questions
We have a public REST API that partners use to list a user's Creative Cloud assets. It returns a flat array of assets with a type field. We now need nested folders, per-asset permissions, and pagination, because one partner has a customer with four hundred thousand assets and the endpoint times out. Several thousand integrations depend on the current shape and we have no way to force them to update. Design the change.
Why interviewers ask this
API evolution is a judgment question with no clever trick, and it separates engineers who have maintained something with external consumers from those who have not. Weak candidates propose a v2 and move on. Strong candidates think about migration mechanics, how to identify who is actually affected, and how to avoid maintaining two systems forever.
Example strong answer
I would start from the constraint that I cannot force migration, which means the current shape has to keep working correctly and performantly for as long as anyone uses it. That rules out changing the existing response in place, even in ways that look additive, because clients in the wild do strict schema validation and positional parsing.
For the new capabilities I would introduce a versioned endpoint rather than overloading the old one with query parameters, because the response shape genuinely changes — nested folders are not expressible as a flat array without lying about the hierarchy. I would put the version in the path so it is visible in logs and cacheable, and design the response as an envelope with a data array plus a pagination object carrying an opaque cursor. Cursors rather than offsets, because offset pagination over a mutating asset list produces skipped and duplicated items, and at four hundred thousand assets that is guaranteed to happen.
Permissions I would model as an explicit object per asset rather than a boolean, because permission models always grow and a boolean cannot. I would include only the effective permission for the calling user, which keeps the payload small and avoids leaking collaborator information the caller has no right to see.
The interesting problem is the old endpoint's timeout, which does not go away just because a new endpoint exists. That customer is broken today. I would fix the old endpoint's performance without changing its contract: paginate the query internally, stream the response, and apply a documented, clearly signalled ceiling on the number of assets returned. Truncating is a contract change of a kind, but the current behaviour is a timeout that returns nothing at all, so a truncated but successful response is strictly better for every caller.
For migration mechanics, I would instrument the old endpoint by API key so I know exactly which partners call it, how often, and whether they are the ones hitting scale problems. That turns several thousand integrations from a scary abstraction into a ranked list, and the distribution will be heavily skewed. I would work the top partners directly with developer relations and leave the long tail alone.
I would publish a deprecation policy with a real date rather than an open-ended one, emit a Sunset header and a deprecation warning on old-endpoint responses, and email the keys still calling it as the date approaches. I would also implement the old endpoint on top of the new one internally — a thin adapter that flattens the tree and drops the envelope — so I maintain one query path, not two. That is usually the difference between a deprecation that finishes and one that lives forever.
The trade-off I would name up front is that the flattening adapter cannot represent folders, so old clients see a flat view of a hierarchical world. That is acceptable, because it is exactly what they see today.
Follow-up questions
The single strongest predictor of an Adobe engineering offer is narration. Interviewers consistently report that they want to hear reasoning, not just answers — candidates who reach correct solutions silently rate below candidates who reason aloud to slightly weaker ones. Practise stating your assumptions, naming the trade-off you are accepting, and saying what you would measure to know you were right. Second: find out which team you are interviewing with before the loop. The difference between a Creative Cloud desktop team and an Express web team is the difference between smart pointers and compositor layers, and recruiters will usually tell you if you ask.