Skip to main content

Backend engineer backend interview questions

30 questions on backend for backend engineer candidates. Expect prompts such as “Implement cursor-based pagination” and “Explain offset versus cursor pagination”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 25 of 30 backend questions (page 1 of 2).

As asked

Implement cursor-based pagination for a feed API in TypeScript. Explain why cursor beats offset for this case.

Sample answer outline

Offset pagination is O(offset) per page on the database (the query still scans the skipped rows) and breaks when items are inserted or deleted mid-paging. Cursor pagination uses an opaque token encoding the last-seen sort key (e.g. created_at + id for uniqueness). The next query is WHERE (created_at, id) < (last_created_at, last_id) ORDER BY created_at DESC, id DESC LIMIT N. Constant time per page, stable under inserts. Encode the cursor as base64 of a small JSON object so clients treat it as opaque. Discuss edge cases: ties on the sort key, bidirectional paging.

Reference implementation (typescript)

TypeScript
type Cursor = { createdAt: string; id: string };

export function decodeCursor(token: string | null): Cursor | null {
  if (!token) return null;
  return JSON.parse(Buffer.from(token, "base64url").toString());
}

export function encodeCursor(c: Cursor): string {
  return Buffer.from(JSON.stringify(c)).toString("base64url");
}

export async function fetchFeed(after: string | null, limit = 20) {
  const cursor = decodeCursor(after);
  const rows = await db.query(
    `SELECT id, created_at, body FROM posts
     WHERE ($1::timestamptz IS NULL OR (created_at, id) < ($1, $2))
     ORDER BY created_at DESC, id DESC LIMIT $3`,
    [cursor?.createdAt ?? null, cursor?.id ?? null, limit],
  );
  const last = rows[rows.length - 1];
  return {
    items: rows,
    nextCursor: last ? encodeCursor({ createdAt: last.created_at, id: last.id }) : null,
  };
}

Expect these follow-ups

  • How do you implement bidirectional pagination (back and forth)?
  • What if the sort field is a non-unique field like score?
  • When is offset still the right choice?
paginationapi-designsql

As asked

Explain the difference between offset-based pagination and cursor-based pagination for a list API. When would you reach for each, and what is the simplest cursor you could use?

Sample answer outline

Offset pagination uses page and size, translating to LIMIT and OFFSET in SQL. It is simple and great for a fixed dataset where users jump to a specific page. Its two weaknesses are that the database still scans the skipped rows, which gets slow deep into a large list, and that items shifting position while paging can cause skipped or repeated rows. Cursor pagination instead remembers the last item seen and asks for rows after it, ordered by a stable sort key. The simplest cursor is the sort value of the last row, such as its id or created_at. The early-career answer should land the core tradeoff clearly: offset for small or jump-to-page cases, cursor for large or live feeds.

Reference implementation (sql)

SQL
-- Offset: simple, but scans skipped rows
SELECT id, body FROM posts ORDER BY id DESC LIMIT 20 OFFSET 100;

-- Cursor: ask for rows after the last id seen
SELECT id, body FROM posts WHERE id < $1 ORDER BY id DESC LIMIT 20;

Expect these follow-ups

  • Why does offset get slower as you page deeper?
  • What problem do inserts cause for offset pagination?
  • What is the simplest value you can use as a cursor?
paginationsqlfundamentals

As asked

A client calls your charge endpoint, the network drops, and it retries. Now you risk charging the customer twice. Design the API so a retry never results in a double charge, and tell me where the idempotency state lives.

Sample answer outline

The core idea is an idempotency key chosen by the client and sent on every attempt of the same logical charge. On the first request you record the key with the request fingerprint and the result; on a retry you recognise the key and return the stored result instead of charging again. The state belongs in durable storage, not memory, because retries can land on a different instance, and the write that records the key must be in the same transaction as the charge so a crash between them cannot leave you charged with no record. Use a unique constraint on the key so two concurrent retries race into the database rather than both succeeding. Cover the awkward cases: a reused key with a different payload should be rejected, not silently served, and stored results need a sensible retention window. The honest point is that idempotency is a database concurrency problem wearing an API hat.

Expect these follow-ups

  • What happens if two retries with the same key arrive at the same instant?
  • Should a client reuse a key for a genuinely new charge, and how do you stop it?
  • How long do you keep idempotency records, and why not forever?
idempotencypaymentsapi-designconcurrency

As asked

Design the OTP supervision tree for a Phoenix service that handles chat rooms, user presence, and outbound webhooks. What processes exist and what restarts when something fails?

Sample answer outline

A strong answer starts with process boundaries, not modules. Put room processes under a DynamicSupervisor so rooms can start and stop independently, and isolate webhook delivery workers so a bad third-party endpoint cannot take down chat state. Choose restart strategies deliberately: one_for_one for independent room workers, rest_for_one only where later children depend on earlier ones. Presence should be treated as derived state that can be rebuilt, while durable chat messages belong in the database. Candidates often trip up by making one GenServer own too much state or by restarting a whole subtree for a local failure.

Expect these follow-ups

  • When would you use Registry versus pg for finding room processes?
  • How do you avoid a thundering restart loop after a deploy?
  • What state is safe to keep only in a process?
otpsupervisionphoenix

As asked

ByteDance's backend services are mostly written in Go. Describe how a goroutine leak happens, how you would detect it in a production service, and how you fix the most common patterns you have seen.

Sample answer outline

A strong answer explains that goroutine leaks happen when goroutines block indefinitely, typically on channel sends or receives that never complete, on a context that is never cancelled, or on a mutex that is never released. Detection involves monitoring runtime.NumGoroutine over time or exposing /debug/pprof/goroutine. The most common fixes are always cancelling contexts on the return path, using select with a done channel or ctx.Done(), and setting read/write timeouts on network connections.

Expect these follow-ups

  • How do you use pprof to get a goroutine profile from a live production service?
  • How would you design a worker pool in Go that cleans up correctly on shutdown?
company:bytedancegogoroutinesconcurrencypprofdebugging

As asked

You have a Spark job on Databricks that is taking three times longer than expected. The Spark UI shows most of the time is in the shuffle read stage. Walk me through how you diagnose what is causing the slow shuffle, and what changes you would make to the job or cluster configuration to fix it.

Sample answer outline

A strong answer checks for data skew first by looking at task duration variance in the Spark UI stage view. If one or a few tasks take 100x longer, skew is the likely cause and salting the join key or using skew hints in Databricks SQL is the fix. If skew is not the problem, the candidate should consider shuffle partition count (spark.sql.shuffle.partitions defaults to 200, which can be too low or too high for the data volume), disk spill to executor local storage, and network bandwidth. They should also mention Adaptive Query Execution in Spark 3 which can coalesce partitions automatically.

Expect these follow-ups

  • How would you use Adaptive Query Execution in Databricks Runtime to reduce shuffle overhead automatically?
  • Explain the difference between a sort-merge join and a broadcast hash join and when each is appropriate.
company:databrickssparkshuffleperformance-tuningdatabricks-runtimedistributed-systems

As asked

You are building a streaming pipeline in Databricks that aggregates events by 5-minute windows, but events can arrive up to 10 minutes late. How do you configure Spark Structured Streaming to handle late data correctly without keeping state forever?

Sample answer outline

The candidate should explain withWatermark(eventTimeColumn, delayThreshold), which tells Structured Streaming to wait up to the specified delay beyond the maximum event time seen before finalizing a window. State for windows older than the watermark threshold is dropped, preventing unbounded state growth. The candidate should clarify that results for a window are only emitted as final after the watermark passes, which means results arrive 10 minutes after the window closes in this example. They should mention the output modes (append, update, complete) and which ones are valid with watermarks.

Reference implementation (python)

Python
df = spark.readStream.format("kafka") \
    .option("kafka.bootstrap.servers", "host:9092") \
    .option("subscribe", "events") \
    .load()

windowed = (
    df
    .withWatermark("event_time", "10 minutes")
    .groupBy(
        window("event_time", "5 minutes"),
        "user_id"
    )
    .count()
)

windowed.writeStream \
    .format("delta") \
    .outputMode("append") \
    .option("checkpointLocation", "/chk/events") \
    .start("/delta/event_counts")

Expect these follow-ups

  • What output mode would you use if you want to see partial window results updated in real time?
  • How does Delta Lake's ability to merge help when you need to correct late-arriving data that already landed in your output table?
company:databricksstructured-streamingwatermarksstateful-streamingsparkdatabricks

As asked

After a deploy, you notice that the p99 latency for GET /repos/{owner}/{repo}/pulls has increased from 200ms to 800ms. You have access to Datadog metrics, distributed traces, slow query logs, and error logs. Walk me through your systematic debugging approach from the alert to a fix.

Sample answer outline

Start with the deployment diff to narrow the blast radius to what changed. Check distributed traces for where time is spent: is the regression in the database, a downstream service, or the Ruby application layer? If in the DB, pull slow query logs and check for missing or dropped indexes or a new query introduced by the deploy. If in the app, look for N+1 queries or a new synchronous external call in the hot path. Confirm with a rollback or a targeted fix and watch the p99 return to baseline.

Expect these follow-ups

  • The traces show the slow time is in an external call to a third-party check provider, not your code. What do you do?
  • How do you distinguish between a regression caused by this deploy versus a gradual data growth issue that happened to cross a threshold after this deploy?
company:githubobservabilitydebuggingperformancetracingdatabases

As asked

GitLab's Rails app is large and N+1 queries are a real production pain point. Given this code that loads a list of merge requests and their authors, identify the N+1 and rewrite it to eliminate it. What GitLab tooling would you use to catch this in CI before it ships?

Sample answer outline

A strong answer identifies the missing eager load, rewrites using includes or preload with the correct association name, and explains why select on top of includes avoids over-fetching. Crucially, they should mention GitLab's QueryRecorder spec helper (in spec/support/query_recorder.rb) that records the number of SQL queries and lets tests assert an exact query count, which is how N+1 regressions are caught in CI.

Reference implementation (ruby)

ruby
# Controller action
def index
  @merge_requests = MergeRequest.where(project_id: params[:project_id])
end

# View
<% @merge_requests.each do |mr| %>
  <p><%= mr.author.name %> opened <%= mr.title %></p>
<% end %>

Expect these follow-ups

  • When would you choose preload over eager_load and why does it matter for joins?
  • How would you handle an N+1 that only appears under certain feature flags?
company:gitlabruby-on-railsn-plus-onedatabaseperformancegitlab-codebase

As asked

A manufacturing client needs a REST API to manage their product inventory: creating products, updating stock levels, querying by category, and soft-deleting items. Design the endpoint structure, HTTP verbs, status codes, and pagination approach. Then tell me what versioning strategy you would use and why.

Sample answer outline

A strong answer uses noun-based resource paths (/products, /products/{id}), appropriate verbs (POST to create, PATCH for partial update of stock, GET with query params for category filter, DELETE for soft-delete returning 204), cursor-based or offset pagination for large result sets, and 422 for validation errors versus 400 for malformed requests. For versioning it should discuss URL versioning (/v1/products) versus header versioning and the tradeoffs for an enterprise client who might have long-lived integrations.

Expect these follow-ups

  • How do you handle a client that sends a PATCH with a field you don't recognize?
  • What OpenAPI tooling would you use in an IBM engagement to keep the API contract in sync with the implementation?
company:ibmrest-apiapi-designbackendversioninghttp

As asked

Your microservice has a P50 latency of 20ms and a P99 of 800ms. The P99 has been spiking to 3 seconds every few minutes since a deploy last week. Nothing obvious shows up in your application logs. Walk me through your debugging process from the first tool you open to a root cause hypothesis.

Sample answer outline

A strong answer is systematic: start with distributed traces (Zipkin or Jaeger) to find which span is slow in the high-latency requests, check whether the spike correlates with GC pauses, thread pool saturation, connection pool exhaustion, or a downstream dependency timing out. The candidate should narrow the diff to last week's deploy (feature flags, dependency version bumps, new database queries), check slow query logs if a database is involved, and look at OS-level metrics like file descriptors and TCP TIME_WAIT for connection leak signs.

Expect these follow-ups

  • Your traces show the latency is inside a single service, not in a downstream call. What do you look at next?
  • How do you reproduce a once-every-few-minutes latency spike in a staging environment?
company:netflixdebugginglatencydistributed-tracingbackendperformance

As asked

A Salesforce customer reports that their Apex trigger on the Opportunity object is throwing a 'Too many SOQL queries: 101' exception in production. Walk me through how you would diagnose and fix this, and explain what caused it in the first place.

Sample answer outline

A strong answer explains that Salesforce enforces per-transaction governor limits and that 101 SOQL queries means a query is running inside a loop. The candidate should describe pulling SOQL out of the loop and bulkifying using collections, explain how to use the Apex debug log to find the offending line, and mention writing a unit test that fires the trigger with 200 records to confirm the fix. Mention lazy loading patterns or using static maps for lookup caching.

Reference implementation (java)

Java
// Problematic: SOQL inside loop
trigger OpportunityTrigger on Opportunity (before insert) {
  for (Opportunity opp : Trigger.new) {
    Account acc = [SELECT Id, Name FROM Account WHERE Id = :opp.AccountId];
    // ... process acc
  }
}

// Fixed: bulkify with a map
trigger OpportunityTrigger on Opportunity (before insert) {
  Set<Id> accountIds = new Set<Id>();
  for (Opportunity opp : Trigger.new) {
    accountIds.add(opp.AccountId);
  }
  Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
  );
  for (Opportunity opp : Trigger.new) {
    Account acc = accountMap.get(opp.AccountId);
  }
}

Expect these follow-ups

  • How would you write a test to reproduce this in a sandbox before deploying the fix?
  • What other governor limits are commonly hit in Apex triggers?
company:salesforceapexsalesforce-platformgovernor-limitsdebuggingtriggers

As asked

Shopify runs one of the largest Ruby on Rails monoliths in the world, with millions of shops on a shared database cluster. Given that context, explain how N+1 query problems arise in Rails ActiveRecord, how you would detect them in a production codebase at Shopify's scale, and what your remediation strategy would be.

Sample answer outline

A strong answer covers how N+1 originates from lazy loading associations in a loop, tooling for detection (Bullet gem, query logging, Datadog APM traces, Rack::MiniProfiler in dev), and remediation options: eager-loading with includes vs preload vs eager_load, selecting only needed columns, and when to switch to a raw SQL join instead. At Shopify's scale, the candidate should mention the multiplier effect: a 10-query N+1 with 10k shops in a batch is 100k queries.

Expect these follow-ups

  • If eager-loading still produces a single query that returns too much data, what would you do next?
  • How does this problem change when you add database sharding per shop?
company:shopifyruby-on-railsactiverecorddatabaseperformanceshopify-scale

As asked

A merchant clicks Install on your Shopify app listing. Walk me through every step of the OAuth 2.0 flow from that click to your app receiving a permanent access token. What security validations must you perform at each step, and what are the most common implementation bugs that lead to token theft or session fixation?

Sample answer outline

A strong answer covers the full flow: redirect to Shopify's OAuth endpoint with client_id, requested scopes, redirect_uri, state, and nonce; Shopify redirects back with code and HMAC signature; validate the HMAC using the shared secret before doing anything else; exchange the code for an access token; store the token scoped to the shop domain. Security validations: HMAC validation (most common miss), state parameter round-trip to prevent CSRF, validate the shop parameter is a myshopify.com domain to prevent open-redirect injection.

Expect these follow-ups

  • What happens if a merchant uninstalls and reinstalls your app? How do you handle token invalidation?
  • What is the difference between online and offline access tokens in Shopify's OAuth model?
company:shopifyoauthsecurityshopify-appauthenticationbackend

As asked

Shopify delivers webhooks with at-least-once semantics, meaning your endpoint may receive the same event more than once. You are building a fulfillment service that processes orders/create webhooks. How do you make the processing idempotent, and what happens if you fail to acknowledge the webhook within Shopify's 5-second timeout?

Sample answer outline

A strong answer uses the webhook's event ID as an idempotency key: on receipt, insert the event ID into a processed_webhooks table with a unique constraint; if the insert fails with a duplicate key error, the event was already handled, so return 200 immediately. The 5-second timeout: if processing takes longer, the endpoint should immediately enqueue the work to a background job (Sidekiq) and return 200. Shopify will retry on non-200 responses with exponential backoff. The candidate should also mention HMAC validation of the webhook payload before processing.

Expect these follow-ups

  • How do you clean up the processed_webhooks table as it grows over time?
  • What if the background job succeeds but crashes before marking the webhook as processed? How do you prevent double processing?
company:shopifywebhooksidempotencyat-least-oncebackendreliability

As asked

Shopify has two main APIs: the Admin API (REST and GraphQL) and the Storefront API (GraphQL only). Walk me through the key differences in authentication, data access, rate limits, and use cases. When would you specifically choose the Storefront API, and what cannot you do with it that you can do with the Admin API?

Sample answer outline

A strong answer covers: the Storefront API uses a public token safe to embed in client-side code, exposes only customer-facing data (products, collections, cart, checkout), and is designed for headless storefronts and mobile apps. The Admin API uses a private access token that must stay server-side, can access all merchant data including orders, customers, and financials, and must not be exposed to browsers. Key Storefront limitations: cannot access order history without a customer token, cannot modify fulfillment, cannot access raw metafields that are private. Rate limits are also different: Storefront has per-IP limits, Admin has per-shop bucket limits.

Expect these follow-ups

  • A headless storefront needs to show a logged-in customer their order history. Which API do you use and how do you authenticate?
  • You are building a mobile app for merchants to manage their shop. Which API do you use and how do you keep the token secure on a mobile device?
company:shopifyshopify-apistorefront-apiadmin-apiauthenticationdomain-knowledge

As asked

A backend microservice that your squad owns, responsible for serving track metadata to the client apps, suddenly shows a p99 latency increase from 50ms to 800ms. No deployment happened in the last 24 hours. Walk me through your debugging process step by step.

Sample answer outline

A strong answer follows a structured approach: check dashboards for error rates, traffic volume, and resource utilization first. Then look at dependency health (downstream services, the database, caches). Common culprits to name: a Redis cache eviction forcing expensive DB queries, a slow query triggered by a skewed data distribution, a GC pause on a JVM service, or a dependency service that started timing out causing thread pool exhaustion. The candidate talks through how to narrow it down with distributed tracing.

Expect these follow-ups

  • You find that the issue is a cache that started evicting frequently. What caused that and how do you fix it without restarting?
  • How would you prevent this kind of issue from being a surprise next time?
company:spotifydebugginglatencymicroservicesdistributed-tracingobservability

As asked

You are building a POST /v1/charges endpoint. A client might retry the request if the network drops. How do you guarantee that a card is never charged twice even if the same request arrives multiple times? Walk me through your schema, your deduplication logic, and how you handle the race where two identical requests arrive at the same millisecond.

Sample answer outline

A strong answer introduces an idempotency key passed by the caller, stored in a table with request hash, result, and creation timestamp. The candidate should describe taking a database-level unique constraint on the key, locking the row on the first request, and returning the cached response on retries. Edge cases to cover: key expiry policy, what to do if the first attempt failed mid-flight, and the difference between idempotency and exactly-once delivery at the downstream processor.

Reference implementation (sql)

SQL
CREATE TABLE idempotency_keys (
  key         TEXT PRIMARY KEY,
  request_hash TEXT NOT NULL,
  response_status INT,
  response_body   JSONB,
  locked_at   TIMESTAMPTZ,
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- On each request:
-- 1. INSERT ... ON CONFLICT DO NOTHING
-- 2. SELECT ... FOR UPDATE
-- 3. If response_body IS NOT NULL, return it
-- 4. Otherwise, process and UPDATE

Expect these follow-ups

  • What happens if the idempotency key collides between two different customers?
  • How long do you retain idempotency keys, and what drives that TTL?
company:stripeidempotencypaymentsdistributed-systemsdatabase-design

As asked

A trip completes and the payment service is called. Due to a network timeout, the caller retries. Walk me through the complete mechanism you would design to ensure the rider is charged exactly once, even if the payment endpoint is called multiple times with the same trip ID.

Sample answer outline

A strong answer introduces an idempotency key (the trip ID), shows how the payment service records the outcome of the first call in a database row keyed on trip ID with a unique constraint, and returns the stored result on subsequent calls rather than re-executing the charge. The candidate should handle the in-flight case (first call is still processing when the retry arrives) using a status field (PENDING, COMPLETED, FAILED), and discuss how this interacts with the payment provider's own idempotency key support (Stripe, Braintree).

Expect these follow-ups

  • The first payment attempt is in PENDING state and has been there for 10 minutes because the payment provider is slow. The retry comes in. What does your service return?
  • How do you handle the case where a charge succeeded at the payment provider but the response was lost before your service recorded it?
company:uberbackendidempotencypaymentsdistributed-systemsuber-specific

As asked

Implement cursor pagination over a feed sorted by a non-unique field such as a relevance score, supporting both forward and backward paging, stable results under concurrent inserts, and a deterministic order. Show the query and explain how the cursor is encoded.

Sample answer outline

The hard part is that a non-unique sort key alone cannot produce a stable, gap-free cursor, so you must form a composite key of the sort field plus a unique tiebreak such as the primary key, and order by both consistently. Forward paging uses a strict comparison on the tuple, WHERE (score, id) < (lastScore, lastId) for descending order; backward paging reverses the comparison and the ORDER BY, then reverses the returned rows in application code so the page reads naturally. Encode the cursor as a base64 token wrapping both the score and the id, kept opaque so clients cannot tamper with the ordering. Stability under inserts holds because the comparison is anchored to concrete values rather than a positional offset. Call out the traps: mismatched comparison and order-by direction silently returns wrong pages, and forgetting the tiebreak causes rows on a shared score to be skipped or duplicated.

Reference implementation (sql)

SQL
-- Composite cursor: (score, id) with a strict tuple comparison
-- Forward page (descending by score, then id):
SELECT id, score, body
FROM feed
WHERE (score, id) < ($1, $2)   -- ($1,$2) = last (score, id) from prior page
ORDER BY score DESC, id DESC
LIMIT 20;

-- Backward page: flip comparison and order, then reverse rows in code
SELECT id, score, body
FROM feed
WHERE (score, id) > ($1, $2)
ORDER BY score ASC, id ASC
LIMIT 20;

Expect these follow-ups

  • Why is a tiebreak column mandatory when the sort key is non-unique?
  • How does the query differ for backward paging, and why reverse in app code?
  • What breaks if the comparison direction and the ORDER BY disagree?
paginationsqledge-casesapi-design

As asked

You need to read every row of a 500 million row table to export it to the warehouse, without holding one giant transaction or hammering the source database. As a data engineer, how do you chunk through it reliably and resumably?

Sample answer outline

Reframe cursor pagination as keyset-based batch extraction. Do not use OFFSET, which rescans from the start each batch and degrades badly over hundreds of millions of rows. Instead, order by the primary key and read fixed-size batches with WHERE id > lastId ORDER BY id LIMIT N, carrying lastId forward. This keeps each query cheap on the index, avoids one long-lived transaction that would bloat the source database, and is naturally resumable: persist the last id processed so a failed job restarts from there rather than from zero. Add throttling between batches to protect the source, and decide how to handle rows that change mid-export, often by snapshotting or by tolerating eventual consistency in the warehouse. The data-engineering signal is reliability and resumability at scale, not just correct pagination semantics.

Reference implementation (sql)

SQL
-- Keyset batch extraction, resumable via persisted lastId
SELECT id, payload
FROM events
WHERE id > $1            -- $1 = last id from the previous batch (0 to start)
ORDER BY id ASC
LIMIT 10000;

Expect these follow-ups

  • Why is OFFSET a poor choice for a full-table export?
  • How do you make the export resumable after a mid-run failure?
  • How do you handle rows that change while the export is running?
paginationbatchdata-engineeringresumability

As asked

Your API runs on a fleet of servers behind a load balancer and you need a per-user limit of 100 requests per minute that holds across the whole fleet, not per server. How do you build it, and what is the sliding-window subtlety people get wrong?

Sample answer outline

A purely local counter on each server fails because a user spread across ten servers gets ten times the limit, so the counter must be shared, typically in Redis keyed by user and window. The naive fixed window resets on a clock boundary, which lets a user fire 100 requests at the end of one minute and 100 at the start of the next, 200 in a couple of seconds. The fix is a sliding window: either a log of timestamps trimmed to the last minute, which is exact but heavier, or a sliding-window counter that weights the previous window by how much of it still overlaps, which is cheap and close enough. The shared increment-and-check must be atomic, so use a Lua script or an atomic counter rather than read-then-write, otherwise concurrent requests undercount. Mention what happens when Redis itself is unavailable: you choose fail-open or fail-closed deliberately, because each has a real cost.

Reference implementation (lua)

lua
-- Atomic fixed-window check in Redis (Lua): increment, set TTL on first hit
local current = redis.call("INCR", KEYS[1])
if current == 1 then
  redis.call("EXPIRE", KEYS[1], ARGV[1])  -- ARGV[1] = window seconds
end
if current > tonumber(ARGV[2]) then       -- ARGV[2] = limit
  return 0                                 -- reject
end
return 1                                    -- allow

Expect these follow-ups

  • Why does a fixed window allow a burst of double the limit at the boundary?
  • Why must the increment and check be atomic?
  • Do you fail open or fail closed if the shared store is down?
rate-limitingredisdistributed-systems

As asked

A Node service is fine on deploy but its memory climbs steadily over a few days until it gets killed and restarts. You cannot reproduce it locally. How do you find the leak in production without taking the service down?

Sample answer outline

Start by confirming it is actually a leak and not just a large but stable working set: watch heap used over time and check whether it plateaus or grows without bound after garbage collection. Once confirmed, capture heap snapshots from a live instance at intervals and diff them; the objects that keep growing between snapshots point at the retained set. Common culprits in Node are closures held by long-lived timers or event listeners that are never removed, an ever-growing module-level cache or Map with no eviction, and listeners added per request to a shared emitter. Do this on one instance pulled from rotation if snapshots are heavy, so users are not affected. The disciplined answer is to follow the evidence from the snapshot diff to a specific retainer rather than guessing, then fix the root cause, for example bounding the cache or removing the listener, and confirm the curve flattens after deploy.

Expect these follow-ups

  • How do you tell a real leak apart from a large steady-state heap?
  • Which Node patterns most often retain memory by accident?
  • How do you profile without affecting live traffic?
debuggingnodememoryproduction

As asked

Third-party apps integrate with Creative Cloud via OAuth 2.0. Walk me through the full lifecycle of an access token: how it is issued, how an app uses it, how it is refreshed, and what happens when a user revokes access from the Creative Cloud dashboard. What are the security risks at each step and how do you mitigate them?

Sample answer outline

A strong answer covers the authorization code flow with PKCE, short-lived access tokens (15 to 60 minutes), long-lived refresh tokens stored server-side with rotation on each use, and the revocation endpoint that immediately invalidates the refresh token family. Security risks include: auth code interception (mitigated by PKCE and short code TTL), stolen access tokens (mitigated by short TTL and binding to redirect_uri), refresh token theft (mitigated by rotation, refresh token binding to client IP or device fingerprint), and open redirect attacks on the callback URL.

Expect these follow-ups

  • How do you detect and respond to a refresh token being used twice, which could indicate theft?
  • A user resets their password on Adobe ID. Which tokens should be invalidated and how quickly?
company:adobeoauth2securityauthenticationcreative-cloudtokens

As asked

Adobe's public APIs for Creative Cloud integrations need rate limiting. Design a rate limiter that enforces 1000 requests per minute per API key, handles traffic spikes gracefully, and is accurate enough that a burst of 1001 requests in 60 seconds reliably fails the last request rather than the first 1000.

Sample answer outline

The token bucket algorithm allows bursting up to the bucket size while bounding the sustained rate, which is a good fit here. A fixed window counter is simpler but has a 2x burst problem at window boundaries. The sliding window log is accurate but memory-expensive. The sliding window counter approximates the log with much less memory. Implement in Redis using atomic Lua scripts or the INCR+EXPIRE pattern for fixed windows, or sorted sets for sliding windows. A strong answer discusses distributed rate limiting across multiple API gateway nodes using a shared Redis cluster and the trade-off between accuracy and the Redis round-trip latency.

Expect these follow-ups

  • A third-party app is legitimately bursting at 2x their limit during a product launch. How do you give them a temporary increase without a code change?
  • How do you communicate the rate limit status to the API consumer in the HTTP response?
company:adoberate-limitingredisbackendapi-designdistributed-systems