cswork / libraryOpen practice workspace

Interview guides · Art of Problem Solving

Art of Problem Solving Software Engineer Interview Guide

Art of Problem Solving Software Engineer interview guide: JavaScript async coding, math-puzzle checking, code optimization, pedagogy and a 7-day plan.

The 60-second brief

Art of Problem Solving (AoPS) runs a website and learning platform built around math problems and curriculum for students. Candidates describe the engineering role as writing and reviewing JavaScript for the interactive tools students use: pages that present lessons, take student input and return feedback. The work is reported to involve curriculum developers and educators, so engineers translate teaching goals into working front-end features, debug problems that affect the learning experience, and take part in code reviews that cover both the code and the student-facing interface.

This guide is for candidates interviewing for a Software Engineer position at Art of Problem Solving, especially front-end and JavaScript-heavy profiles. It covers the five coding and algorithm questions candidates report (array and concurrency work, explaining a puzzle approach, a JavaScript task queue, a math-answer checker, and optimizing existing code), the math-puzzle reasoning candidates describe, and the pedagogy questions that can appear in a software loop.

No round-by-round sequence is reported. Candidates describe a multi-stage process that runs from initial screens to technical and pedagogical deep dives, with a long final stage in some loops.

Focus areas

  • JavaScript
  • Concurrency (JavaScript)
  • Pedagogy (Teaching Methodology)

Overview

The reported emphasis is JavaScript. Candidates describe being asked to write code live, explain their reasoning as they go, and work on topics such as asynchronous programming, event handling, DOM manipulation and browser-side performance. The requirements candidates list include JavaScript, HTML/CSS and modern web practices, with React.js or a similar framework as a plus. Prepare to write plain JavaScript without a framework or documentation open: promises, async/await, the event loop, array methods, and DOM events.

The second thread is mathematics. Candidates report logic and math puzzles as part of the technical evaluation, centred on pattern recognition and logical deduction, and one reported question asks for a function that verifies a student's answer to a math puzzle. That question is more interesting than it looks: equivalent answers can be written differently (1/2, 0.5, 2/4), floating-point comparison is unreliable, and a good checker has to decide what counts as correct. Practise modelling a puzzle with exact arithmetic and clear invariants before you write any code.

The third thread is teaching. Candidates report that a software loop can include questions about teaching philosophy, student-teacher interactions, and reviewing lesson plans or student work, and that the platform's curriculum and problem style are worth studying beforehand. You cannot predict the exact questions, but you can prepare concrete examples of how a technical decision (feedback timing, error messages, input handling, performance on slow devices) changes what a learner experiences.

Because the loop is reported as multi-stage and sometimes long, plan for stamina as well as content. Candidates suggest asking whether a full-day or multi-hour session can be split in two. Practise thinking aloud while you code, since candidates report being asked to explain reasoning during the exercise itself, and rehearse stating a trade-off in one sentence before you commit to an approach.

Where to focus your preparation

No named rounds are reported, so this guide uses a single preparation block that covers the areas candidates describe: live JavaScript coding, math-puzzle reasoning, optimizing code, and pedagogy discussion.

Preparation focus

Candidates describe a multi-stage process that moves from initial screens to deeper technical and pedagogical sessions, often involving people from different departments, and in some loops a full-day or multi-hour final stage. The reported technical content is live JavaScript coding with spoken reasoning, math and logic puzzles, and tasks such as optimizing a given piece of code. Candidates also report questions about teaching philosophy, student-teacher interactions and reviewing lesson plans or student work. Since no round order is reported, ask your recruiter which stages you will have, whether each includes code, and whether the long sessions can be split. Then work the five reported coding questions out loud, in plain JavaScript, with no documentation open, and prepare a few examples that connect your technical choices to a learner's experience.

What to demonstrate

  • Fluent JavaScript written live, including asynchronous code and event handling
  • Breaking a puzzle into parts and stating trade-offs before coding
  • Explaining your reasoning clearly while you work
  • How you discuss technical decisions in terms of the student using the software

How to prepare

  • Write from memory, without documentation, a promise-based task queue with a concurrency limit, and an answer checker for a small math puzzle that uses exact arithmetic
  • Practise narrating a solution aloud on array manipulation and optimization exercises, giving the brute-force version and its cost first
  • Skim the AoPS website and note the style of the math problems and lesson pages, so you can talk about how you would build an interface for them
  • Ask the recruiter for the stage list and for permission to split a long session, then write down the answers

Where candidates lose points

Coding silently, then explaining the solution afterwards

Candidates report being asked to explain their reasoning while they code. Practise a fixed rhythm: restate the problem, give the brute-force idea and its cost, name the improvement, then type. Say each trade-off in a sentence as you meet it, and record yourself once to hear where you go quiet.

Writing the answer checker with string equality or floating-point comparison

Decide what counts as a correct answer before coding: a normalized form (reduced fraction, sorted set of roots, trimmed text), exact integer or rational arithmetic, and a tolerance only when the problem is genuinely numeric. List edge cases such as 1/2 against 0.5, negative zero, whitespace, empty input and answers with several valid forms.

Building a JavaScript task queue that runs everything at once, swallows rejections, or gets the ordering of callbacks wrong

Be able to explain the event loop (call stack, microtask queue for promise callbacks, then timers and other tasks) and use it to predict output. Write a queue with an explicit concurrency limit, a result or error for every task, and a way to know when everything has finished. Test it with tasks that reject and tasks that finish out of order.

Optimizing the supplied code without saying what is slow or proving behaviour is unchanged

Read the code first, state what it does, and name the costly part (nested loops, repeated DOM reads and writes, work inside a handler that fires constantly). Make one change at a time, keep the outputs identical, and say plainly when a change is for readability rather than speed.

Treating the teaching and curriculum questions as soft and answering with generalities

Candidates report questions on teaching philosophy, student-teacher interactions, and reviewing lesson plans or student work. Prepare two or three concrete cases: a time you explained something technical to a beginner, feedback you would show a student who gave a wrong answer, and what you would check in a lesson page before shipping it. Use the same structured reasoning you use for code.

Questions & practice

Implement a solution for a specific array manipulation or concurrency problem.

Medium · Technical and Algorithmic Proficiency

Approach

  1. Ask which half it is before coding. For an array problem, settle whether you may mutate the input, whether it can be empty or hold duplicates, and how large n is. Then pick the pattern: two pointers on sorted data, a sliding window for contiguous ranges, prefix sums for range totals, or a Map/Set for O(1) lookups. That turns an O(n^2) pair scan into O(n).
  2. Know the JavaScript array costs you rely on. push and pop are amortised O(1). shift, unshift and splice are O(n) because they re-index the elements. includes and indexOf are O(n). The default sort compares values as strings, so [10, 9, 1].sort() returns [1, 10, 9]: pass (a, b) => a - b for numbers. Sort has been required to be stable since ES2019.
  3. For the concurrency version (for example, run N async jobs with at most K in flight), start K workers that each loop on a shared next-index counter and await one job at a time. JavaScript runs one thread per event loop, so const i = next++ cannot interleave with another worker between awaits. Write results[i] so the output keeps input order.
  4. Say what happens on failure. Promise.all rejects on the first rejection, but the other promises keep running because nothing cancels them. Promise.allSettled waits for all of them and reports each outcome. Pick one deliberately and say whether one failed job should stop the rest.
  5. Test the edges: an empty array, one element, all duplicates, K larger than the number of jobs, and a job that throws synchronously inside an async function (that becomes a rejection, not an uncaught throw).

Follow-up

  • Can you do the array version in place with O(1) extra space, and what does that cost in readability?
  • How would you add a per-job timeout or cancellation to the concurrency limiter, for example with AbortController?
  • If results must be returned in completion order rather than input order, what changes?

Explain the logic behind your approach to a given coding puzzle.

Medium · Technical and Algorithmic Proficiency

Approach

  1. Restate the puzzle with one small concrete example and its expected output before giving any approach. A wrong reading of the problem usually shows up here.
  2. State the brute force and its complexity first (for example, try every subset in O(2^n) or every pair in O(n^2)). Then name the specific observation that lets you do better: sortedness, a recurrence for dynamic programming, a greedy choice, or a mathematical pattern such as parity or a closed-form sum like n(n+1)/2.
  3. Justify correctness, not just mechanics. Give the loop invariant your code maintains, or for a greedy choice an exchange argument (swapping in the greedy pick never makes an optimal solution worse). For a recurrence, state the base case and what each subproblem means.
  4. Trace the algorithm by hand on your example, then on an edge case (empty input, a single element, the largest allowed value), and give final time and space complexity with the reason for each.
  5. Narrate decisions as you make them ("I use a Map here because I need lookups by value") so your reasoning is visible without a separate explanation at the end.

Follow-up

  • Why does your greedy step never miss the optimal answer? Can you give an input where a slightly different greedy rule fails?
  • How would you change the solution if the input were streamed and you could only see each element once?
  • Can you reduce the memory from O(n) to O(1), and what would you give up?

How would you structure a task queue or handle asynchronous events in JavaScript?

Medium · Technical and Algorithmic Proficiency

Approach

  1. Start with the event loop. Synchronous code runs to completion on the call stack. Then the whole microtask queue (promise reactions, queueMicrotask, await continuations) drains. Only then does the next macrotask (setTimeout, I/O, UI events) run. So console.log, then Promise.resolve().then, then setTimeout(fn, 0) print in that order.
  2. Outline a task queue as a class with enqueue(fn) that returns a promise for that task's result, a FIFO array of pending entries, a running count and a concurrency limit. When a task settles, decrement the count and start the next one. Use a head index or a linked list instead of shift() if the queue can grow large, because shift is O(n).
  3. Isolate failures. Wrap each call as Promise.resolve().then(fn) so a synchronous throw becomes a rejection, reject only that task's promise, and keep draining the queue. Add retries with exponential backoff and a cap only for tasks that are safe to repeat.
  4. For UI events: debounce input that should fire once after typing stops (like a search box), throttle continuous events (scroll, resize), and use event delegation on a parent for many similar elements. Remove listeners, or pass an AbortSignal to addEventListener, when a component goes away.
  5. Handle stale responses. If a user fires request A then B and A returns last, ignore it using a request id or by aborting A with AbortController. Break long synchronous loops into chunks, or move them to a Web Worker, so input events are not blocked.

Follow-up

  • How would you add priorities to the queue, and how do you stop low-priority tasks from waiting forever?
  • What happens to queued tasks if the user closes the tab, and how would you persist or resume them?
  • Why can a long chain of microtasks freeze the page even though every step is async?

Write a function to verify if a student's answer to a math puzzle is correct.

Medium · Technical and Algorithmic Proficiency

Approach

  1. Pin down the answer format first: an integer, a fraction, a decimal, an algebraic expression, or a set of values. The checker should accept equivalent forms (3/4, 6/8, 0.75, -3/4 vs 3/-4, a mixed number like 1 1/2) and reject malformed or empty input with a clear message, not a crash.
  2. For exact numeric answers, parse to a rational number. Keep numerator and denominator as BigInt, normalise the sign into the numerator, reduce with Euclid's gcd (O(log min(a, b)) steps), then compare. Avoid floating-point equality: 0.1 + 0.2 === 0.3 is false, and integers above Number.MAX_SAFE_INTEGER (2^53 - 1) lose precision.
  3. Use a tolerance only when the expected answer really is approximate, and write it down as either an absolute or a relative bound. Reject a zero denominator explicitly.
  4. For expression answers, never call eval on student input, because it runs arbitrary code. Parse with a small recursive-descent parser or a vetted math library. Then check equivalence symbolically, or evaluate both expressions at several random points and compare within a tolerance. For polynomials, the Schwartz-Zippel lemma bounds the chance of a false match at degree/|S| per random point. Skip points where either side is undefined.
  5. For multi-part answers, compare as an order-insensitive multiset when order does not matter. Keep parsing, normalisation and comparison as separate functions so each can be unit-tested against a table of correct, equivalent and wrong inputs.

Follow-up

  • How would you accept sqrt(8) and 2*sqrt(2) as the same answer, and where does random-point testing give a false positive?
  • Should this check run in the browser or on the server, given the answer key must not be visible to the student?
  • How would you detect a common wrong answer (say, a sign error) and return targeted feedback instead of just 'incorrect'?

Optimize a provided piece of code for better performance or readability.

Medium · Technical and Algorithmic Proficiency

Approach

  1. Say what the code does and pin that behaviour down before touching it. Write or point to a few tests covering normal input, empty input and a boundary value, so every change can be checked against them.
  2. Measure before optimising: performance.now() around the hot section, or a browser DevTools Performance recording for UI code. Name the bottleneck you found instead of guessing.
  3. Go after the common algorithmic wins: a nested loop with includes/indexOf (O(n*m)) becomes a Set or Map lookup (O(n + m)); repeated spread or concat inside a loop (O(n^2) copying) becomes push; work that does not change between iterations moves out of the loop; repeated pure computations get memoised with a bounded cache.
  4. For DOM code: batch reads before writes, because reading offsetHeight or getBoundingClientRect after a style change forces a synchronous layout (layout thrashing). Build many nodes in a DocumentFragment and insert them once, and use event delegation instead of one listener per item.
  5. For readability: descriptive names, small extracted functions, early returns instead of deep nesting, and removing dead code and flag variables. State the trade-offs you accept, such as extra memory for a Set or cache, or a slightly slower but clearer version where the code is not hot.

Follow-up

  • How do you show the optimised version is behaviour-preserving, including edge cases the original handled by accident?
  • When would you leave slow code alone, and what would make you change your mind?
  • How would you keep this from regressing later, for example with a benchmark or a performance budget in CI?

Keep soft-deleted accounts from blocking re-registration

Easy · soft delete · unique constraints · engine differences

app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.

Approach

  1. Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
  2. Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
  3. For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
  4. State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.

Follow-up

  • A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
  • How do you honour an erasure request while resource_revision.actor_user_id still references this table?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?

Make the resource write endpoint safe for retrying clients

Easy · idempotency · unique constraints · retries

The edge API accepts POST /v1/resources at 1.2k writes/second peak under a 400 ms p99 budget, and clients retry on timeout. idempotency_key has PRIMARY KEY (tenant_id, idempotency_key) plus request_fingerprint CHAR(64), state in ('in_flight','succeeded','failed'), response_status, response_body, resource_id, locked_until and expires_at. Specify the exact sequence the handler runs: what the second request does when it arrives while the first is still executing, what a retry carrying a different body receives, what happens when a process dies mid-request, and how the table is kept from growing without bound.

Approach

  1. Start from the race rather than the happy path. Insert the key row first, in its own short transaction, with state 'in_flight' and locked_until set to now() plus the request deadline. The unique constraint on (tenant_id, idempotency_key) is the arbitration mechanism: exactly one caller commits the insert, and every other caller takes a unique violation (SQLSTATE 23505) and becomes a follower. Reading the table and then inserting is a check-then-act race that both callers pass, and they pass it most often under the load that generates the retries.
  2. Give the follower a branch for every state it can observe, with no fall-through to doing the work: fingerprint mismatch means the key is being reused for a different request, so reject with 422 and never replay the stored response; 'succeeded' replays response_status and response_body verbatim; 'failed' allows a fresh attempt; 'in_flight' with locked_until in the future is answered with 409 and a retry hint, or a short bounded wait, because the only alternative is executing the effect twice.
  3. Commit the effect and the record of the effect together. The resource insert, its resource_revision row, its outbox_event row and the transition of the key row to 'succeeded' all happen in one transaction, so no crash can leave the work done and the key still 'in_flight'. The earlier insert is deliberately a separate transaction: it has to be visible to a concurrent caller before the work starts, which an uncommitted row is not.
  4. Close the wedged-key path. A process that dies after the insert leaves a row that would otherwise block that key forever, which is what locked_until exists for. Reclaim with a single conditional statement, UPDATE ... SET state='in_flight', locked_until=now()+interval WHERE ... AND state='in_flight' AND locked_until < now(), and treat a zero rowcount as losing the reclaim. Two statements that read and then update reintroduce the original race one layer down.

Follow-up

  • The stored response body averages 200 KB and this table is now the largest in the database. What do you store instead, and what does a replay return once the body has been pruned?
  • A client library generates a fresh idempotency key on every attempt. What breaks, which layer should have caught it, and does the server have any defence?

Cache the tenant listing feed with a bounded staleness window

Easy · caching · cache invalidation · keyset pagination

GET /v1/resources returns one tenant's resources ordered by updated_at DESC, 20 per page, at 14k requests/second peak against a 120 ms p99. The table carries the index (tenant_id, status, updated_at DESC, resource_id DESC) and writes land on the primary at 1.2k/second. Design the read path: the pagination contract, the cache key and value, what a write invalidates, and the staleness a user can observe. State the request rate that actually reaches the database, and the one repopulation race that deleting on write does not close.

Approach

  1. Settle the pagination contract first, because it decides what is cacheable. OFFSET makes the database produce and discard the skipped rows, so page 500 costs five hundred pages of work, and rows inserted between two fetches shift across the boundary and are skipped or repeated with nothing in the response to reveal it. The cursor is the row value of the last row returned: WHERE tenant_id = $1 AND status = $2 AND (updated_at, resource_id) < ($3, $4) ORDER BY updated_at DESC, resource_id DESC LIMIT 21. That is a row-value comparison, not updated_at < $3 AND resource_id < $4, which is a different and wrong predicate.
  2. Confirm the index actually serves it: equality on the two leading columns, then a range on the pair that follows in exactly the index's sort order, so the plan is an index scan that touches 21 entries with no sort node. Requesting 21 to return 20 is how has_more is answered without a count. resource_id is not decoration - updated_at is not unique, and without the tie-break two rows sharing a timestamp at a page boundary are the skip that keyset pagination was adopted to remove.
  3. Key the cache on every value the predicate reads: tenant_id, status, cursor and page size. A key that omits tenant_id is a cross-tenant disclosure, and no test running against a single tenant's data will show it.
  4. Be honest that a write does not invalidate one key. An update moves its row to the head of the ordering, so it invalidates the first page and every cursor page whose range spans the row's old and new position, which is not enumerable. Cache the first page per (tenant_id, status) - that is where the traffic is - with a short TTL, invalidate it on write, and serve deep cursor pages uncached from a replica, since each is already a 21-row index scan and they are rare.

Follow-up

  • The tenant writes and immediately lists. What does it see, and what is the smallest change that makes its own write visible without sending all 14k requests/second to the primary?
  • A tenant has 4 million resources and a client walks every page nightly. What does that do to the cache hit rate, and should that traffic share this path at all?

Read latency spikes on a sixty-second sawtooth

Medium · cache stampede · single-flight · ttl

The cached listing read path serves about 14k reads/second at an 85% hit rate. p99 sits at 35 ms for 57 seconds, jumps to 900 ms for 3, and repeats. During each spike the primary shows several hundred identical listing queries starting within the same millisecond, all carrying one large tenant's id. Cache entries use a 60-second TTL. Give the mechanism, the ordered checks, the fix, and the correctness hazard your fix must not introduce.

Approach

  1. Match the period to a configured number before theorising about load. A spike every 60 seconds against a 60-second TTL is an entry expiring, and you confirm it by correlating spike timestamps with the entry's write time rather than with the traffic curve. If the period had matched a cron or a GC interval instead, this is a different investigation.
  2. Establish the concurrency of the miss. Several hundred identical queries in one millisecond means the miss path has no coalescing: every request that arrives between expiry and repopulation recomputes. The herd size is that key's arrival rate times its recompute time, so at 1.2k reads/second for the hot key and a 250 ms recompute you expect about 300 concurrent misses, which matches what is observed.
  3. Add single-flight on the miss path so one caller per key recomputes under a short-lived lock while the rest wait for its result. Prefer stale-while-revalidate where the read tolerates it: return the expired value immediately and refresh asynchronously, which removes the latency spike rather than serialising it into a queue of waiters.
  4. De-synchronise the keys. Write TTLs with jitter, for example 60 seconds plus or minus 10%, so a deploy or a mass invalidation does not align every key on the same second and turn a per-key herd into a fleet-wide one.

Follow-up

  • The same sawtooth appears on a key that is invalidated on write rather than expired. Is that the same bug?
  • How does your answer change if the recompute takes 4 seconds instead of 250 ms?

Suggested practice checklist

The plan follows the reported themes: JavaScript fundamentals and asynchronous code, the five reported coding questions, math-puzzle reasoning, and the pedagogy discussion. Each day ends with something you can show, and the last day is a full mock.

Day 1 — Confirm the format and refresh core JavaScript

  • Email your recruiter for the list of stages, whether each includes live code, whether you may look up syntax, and whether a long session can be split.
  • Without documentation, write short examples of closures, array methods (map, filter, reduce, sort with a comparator), and promise chaining, and predict the output of two snippets that mix timers and promise callbacks before running them.
  • Read through the AoPS website's lesson and problem pages and note what a student sees and does on one page.

Deliverable: A recruiter reply (or a written list of questions if none yet), a file of working JavaScript examples, and a half-page of notes on one AoPS page.

Day 2 — Task queues and asynchronous events

  • Write a task queue with a concurrency limit using promises: add(task) returns a promise for that task's result, failures are reported without stopping the queue, and a method resolves when the queue is empty.
  • Extend it with retry on failure and cancel for tasks not yet started, and write tests with tasks that resolve out of order and tasks that reject.
  • Explain aloud, in a minute, how async/await and the event loop decide when each task's code runs.

Deliverable: A tested task-queue module with limit, retry and cancel, plus a recorded one-minute explanation of the event loop.

Day 3 — Array manipulation and concurrency hazards

  • Solve three array exercises in plain JavaScript (for example in-place removal of duplicates, rotating an array, merging overlapping intervals), stating the brute-force cost and the improved cost for each.
  • Write a small example where two async handlers read, await, and write the same variable and lose an update, then fix it with a serialized queue or a single owner of the state.
  • Describe aloud what changes if the same code ran with web workers or several browser tabs sharing storage.

Deliverable: Three solved array exercises with complexities written beside them, and one race example with its fix.

Day 4 — Checking a student's answer to a math puzzle

  • Pick a puzzle with several valid answer forms (a fraction, a set of solutions, an expression). Write a checker function with a clear input contract, normalization, and exact comparison.
  • Write at least ten test cases covering equivalent forms, whitespace, empty input, negative numbers, large values and a wrong answer that looks close.
  • Solve three pattern-recognition or logical-deduction puzzles on paper, then model one in code to confirm the answer.

Deliverable: A tested answer-checking function with a list of edge cases, and three worked puzzles with the reasoning written out.

Day 5 — Optimizing and refactoring existing code

  • Take a slow or messy JavaScript function of your own (nested loops, repeated lookups in an array) and improve it in steps: first for clarity, then for cost, using a Map or Set where the access pattern calls for it, and confirm the output is unchanged with tests.
  • Build a small dynamic UI component that updates as a student types, then reduce the cost of its event handler with debouncing and by limiting how often it reads and writes the DOM.
  • Write a three-sentence description of each change: what was slow or unclear, what you changed, and what you traded away.

Deliverable: A before-and-after version of one function and one UI component, with tests and the three-sentence summaries.

Day 6 — Explaining your approach and teaching questions

  • Pick two coding puzzles you have not seen and solve each aloud, in order: restate, brute force, better idea, complexity, edge cases. Record yourself and note where you hesitated.
  • Write answers to three teaching prompts: how you would explain recursion to a beginner, what feedback you would show a student who answered incorrectly, and what you would look for when reviewing a lesson page or a student's work sample.
  • Write one example where a technical decision, such as an error message or a slow page, changed how a person learned or used your work.

Deliverable: Two recorded spoken solutions with hesitation notes, and a page of answers to the three teaching prompts.

Day 7 — Full mock and questions to ask

  • Run a full mock without documentation: one asynchronous task, one answer checker or array problem, one optimization exercise and one teaching question, talking throughout.
  • Review the recording for silent stretches and unstated trade-offs, then redo the weakest task once.
  • Write down questions for the team about how engineers and curriculum developers work together, and confirm any logistics from the recruiter.

Deliverable: A recorded mock, a note listing the weakest task and how you redid it, and a short list of questions for the team.

Pedagogy and student-focused questions

Candidates report that a software loop here can include teaching philosophy, student-teacher interaction, and reviewing lesson plans or student work, alongside the coding. These prompts are not reported verbatim; they are practice topics built from those reported themes. Answer them with the same structure you use for code: state the situation, the decision, the trade-off and the result for the learner.

  1. Describe your teaching philosophy, and give one example where you helped someone understand something technical.
  2. A student gives a wrong answer to a math puzzle. What do you want the interface and the feedback to do, and why?
  3. Walk through how you would review a lesson page or a student's work sample, and what you would check before it ships.
  4. Tell me about a time a curriculum developer or non-engineer asked for something that was hard to build. How did you reach a result you both accepted?
  5. Describe a technical choice you made, such as an error message, loading time or input handling, that changed how users experienced the product.

Estimate work you have never done and defend the range

Hard · estimation · backfills · expand-contract

You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.

Approach

  1. Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
  2. Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
  3. Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
  4. Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.

Follow-up

  • How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
  • Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?

Narrate an outage you owned from page to postmortem

Hard · incident response · blast radius · postmortems

Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.

Approach

  1. Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
  2. Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
  3. Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
  4. Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.

Follow-up

  • What would you do differently in the first five minutes, given the same dashboard and no more information?
  • Which follow-up action did you deliberately not take, and why was dropping it the right call?

Tell callers you do not own that their integration breaks

Medium · deprecation · compatibility · stakeholders

A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.

Approach

  1. Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
  2. Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
  3. Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
  4. Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.

Follow-up

  • How would you detect a consumer that reads the field only during a monthly export?
  • One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?