Interview guides · Acunor
Acunor Software Engineer Interview Guide
Acunor Software Engineer interview guide: JavaScript scoping and async answers, product-list and cart builds, API integration and a 7-day plan.
The 60-second brief
Candidates describe the Software Engineer work at Acunor as client-facing web development: user interfaces for e-commerce functions such as product displays and cart management, fed by RESTful or GraphQL APIs. The requirements candidates describe put JavaScript first, along with building and deploying web applications, working with external APIs, managing application state, and debugging and developer tools. Candidates also describe code reviews with senior engineers, requirement refinement with product teams and ownership of troubleshooting for their features.
This guide is for engineers interviewing for the Software Engineer role at Acunor who need to show working JavaScript in live and written settings. It covers the two reported JavaScript questions (variable declarations, and asynchronous handling with Promises and async/await), the practical build tasks candidates describe (rendering products fetched from an endpoint, a working cart, modernising legacy code to ES6+), the state, loading, error and formatting work those tasks require, and the project discussions candidates report in the later rounds.
Candidates report five stages, opening with initial screenings and closing with a leadership discussion, with written assessments, live coding and in-depth technical interviews in between; some say the whole process ran up to two months.
Focus areas
- JavaScript
- Coding Interviews
- API Integration
Overview
The topics candidates report for this role are JavaScript, coding interviews, API integration, cart feature implementation and e-commerce application development. Candidates describe practical tasks, such as building a component from a supplied API or a functional shopping cart, rather than algorithm puzzles. Your practice target is therefore a small working front-end feature written from a blank file, not a graph or dynamic-programming problem set.
Those builds have a predictable technical core. Fetching data means handling a fetch that resolves on a 404, showing loading, error and empty states, and formatting prices and other fields for display. A cart means modelling items, quantities and totals without floating-point drift, and keeping one source of state shared between components such as a header badge and a cart panel. Candidates also mention the browser render cycle, so know how to batch DOM writes and avoid re-rendering a whole list when one quantity changes.
The language questions sit underneath the builds. Candidates list closures, prototypes and asynchronous patterns as core concepts, and the two reported questions ask about let/const/var and about Promises versus async/await. The strongest way to answer both is with code: predict the output of a short snippet, explain why, and connect it to a bug you would hit in a product list or cart.
Candidates report that the later technical and manager-led conversations dig into past projects, and that client-facing work means tying technical detail to business value. For every project on your resume, be ready to sketch the architecture, name the trade-offs you made, say what the project achieved, and explain one decision in terms a non-engineer would accept.
Where to focus your preparation
Candidates report the five stages by type (screenings, written assessments, live coding, technical interviews, a leadership discussion) rather than in a fixed order, so the preparation below is consolidated into one stage.
Preparation focus
Candidates report five stages for this role, starting with initial screenings and ending with a leadership or executive discussion, with written assessments, live coding challenges and in-depth technical interviews in between; some report the process taking up to two months. The order of the middle stages is not reported, so ask your recruiter before each one whether it is written or live and what language environment you will use. Refresh technical fundamentals early and save project detail for the later conversations: work JavaScript scoping and async handling first, then the practical builds (a product list rendered from an API, a working cart, a legacy refactor), and your project stories last. Candidates also report writing code on a whiteboard or in a live environment with little boilerplate tooling, so practise every build in a blank file.
What to demonstrate
- Working JavaScript written live: scoping, closures, prototypes and Promise-based async code
- Building a component from a provided API, with loading, error and data-formatting states handled
- Cart and e-commerce feature implementation with state shared between components
- Depth on your own past projects: architecture, trade-offs and what each achieved
How to prepare
- In a blank HTML file with no framework scaffold, fetch products from a public mock API and render them with loading, error and empty states
- Build a cart module (add, change quantity, remove, totals in integer cents) and wire it to both a header count and a cart view
- Explain let/const/var and Promises versus async/await aloud by predicting the output of short snippets you wrote yourself
- Write one-page architecture notes for two past projects: components, data flow, one trade-off and a measured result
Where candidates lose points
Reciting "let is block-scoped" for the let/const/var question without showing a consequence, or claiming const makes an object immutable.
Answer with code: the var loop that prints 3 3 3, a temporal dead zone ReferenceError, and const cart = [] followed by cart.push. Practise predicting snippet output until you can explain each result without running it.
A product-fetch component that only handles the happy path: no loading or empty state, and an assumption that fetch rejects on a 404.
Make response.ok checks, a try/catch/finally around the request, and separate loading, error and empty renders part of every practice build, so they appear without thinking when you build a component from a supplied API.
Cart totals that drift (0.1 + 0.2 giving 0.30000000000000004, or 1.1 * 3 giving 3.3000000000000003) or a UI that does not update because the cart array was mutated in place.
Store prices as integer cents, derive totals instead of storing them, return new state from every cart operation, and format for display with Intl.NumberFormat. Test duplicate adds, a quantity set to zero and invalid input before calling the cart done.
Misusing async/await under pressure: awaiting independent calls in sequence, using forEach with an async callback, or swallowing errors in an empty catch.
Decide before writing whether calls are dependent; use Promise.all or allSettled for independent ones and for...of for ordered ones, and make every catch either recover visibly or rethrow.
Describing a past project as a feature list with no architecture, trade-offs or result in the later project discussions candidates report.
For each resume project, write out the components and data flow, one decision with the alternative you rejected and why, and what the project achieved in numbers. Rehearse explaining one of those decisions in business terms for a client-facing audience.
Questions & practice
How do you handle asynchronous operations using Promises and async/await?
Medium · JavaScript Proficiency
Approach
- Define a Promise as an object that is pending, then settles once to fulfilled or rejected. Its then/catch callbacks run as microtasks: after the current synchronous code and before the next macrotask such as setTimeout(fn, 0). Be ready to order console.log output for a snippet mixing all three.
- Explain async/await as syntax over Promises: an async function always returns a Promise, await suspends only that function, and an awaited rejection throws, so try/catch/finally replaces .catch. Use finally to clear a loading flag whether the request succeeded or failed.
- Contrast sequential and parallel work: awaiting independent requests one after another adds their latencies together. Start them together and pick the combinator: Promise.all rejects on the first failure, Promise.allSettled returns every outcome (useful for rendering what loaded), Promise.race settles with the first result and Promise.any with the first success. Note that array.forEach with an async callback does not wait; use for...of for sequence, or map plus Promise.all for parallel.
- Apply it to a product fetch: fetch does not reject on HTTP error statuses, so check response.ok and throw for 4xx/5xx yourself. It does reject on network failure, on CORS or invalid-URL errors, and with an AbortError when you abort it. Parse JSON inside the same try, and treat an empty list as its own state rather than an error.
- Cover the UI edge cases: cancel stale requests with AbortController when a search term changes or a view is torn down (or drop responses whose request id is not the latest), and in the catch check err.name === 'AbortError' and ignore it so a request you cancelled on purpose never shows the error state. Retry with exponential backoff only for idempotent GETs, and never leave a rejection unhandled.
Follow-up
- Write a function that fetches 50 product ids with at most 5 requests in flight at once.
- Implement Promise.all yourself: how do you keep results in input order, and what happens to the other requests when one rejects?
- A user types quickly into a search box and older results overwrite newer ones. How do you fix it?
Track a rolling failure rate per destination for circuit decisions
Easy · sliding window · ring buffer · circuit breaker
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
Approach
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
Follow-up
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
Identify the heaviest tenants in a five-minute window under memory pressure
Medium · top-k · heavy hitters · streaming
The edge service handles about 3,000 requests per second across roughly 50,000 tenants, peaking near 9,000. Expose the 50 heaviest tenants by request count over the trailing five minutes so limits can be tightened before one tenant's backfill starves the fleet. You may not retain five minutes of raw records. Give the exact solution and its memory, then the bounded-memory approximation with its error stated as a formula, and say which you would ship and at what tenant cardinality that choice changes.
Approach
- Do the exact version first, because it is affordable at this cardinality: a ring of 300 one-second counters per tenant, advanced lazily, is 1,200 bytes of counters per tenant and roughly 60 to 90 MB for 50,000 tenants with overhead. Carry a running total and subtract the bucket you overwrite so a window read is O(1) rather than 300 adds.
- Extract the top 50 with a size-k min-heap over the tenant sums: O(d log k) for d tenants, against O(d log d) to sort them all. Maintaining the heap continuously instead of on query requires a tenant-to-heap-index map, because incrementing a count already inside the heap means sifting from a known position, and without that map you rebuild the heap on every request.
- State the approximation precisely rather than gesturing at sketches. Misra-Gries with m counters retains every item whose true count exceeds N/(m+1), and each retained count underestimates the truth by at most N/(m+1). With m = 1,000 and N = 900,000 requests in the window the error is roughly 900 requests, which is fine for spotting a tenant sending 50,000 and useless for ranking two tenants 200 apart.
- Say what breaks when the window slides: Misra-Gries and Space-Saving are insert-only and cannot be decremented as records age out. The workable construction is one summary per sub-window, say ten seconds, with 30 summaries merged at query time, and the merged error is the sum of the per-summary errors, so the bound degrades linearly in the number of sub-windows.
Follow-up
- The heaviest tenant is heavy because of one export job rather than user traffic. Should the limiter treat those as the same tenant?
- Two tenants sit tied at the boundary of the top 50. Does your answer flap, and does the flapping matter?
Explain why the owner filter ignores the listing index
Medium · composite index · query plans · sort elimination
The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.
Approach
- Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
- For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
- Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
- Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.
Follow-up
- 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
- A dashboard runs this for 40 owners in one page load. What changes about the design?
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
- 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.
- 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.
- 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.
- 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?
Explain the difference between let, const, and var in modern JavaScript.
Medium · JavaScript Proficiency
Approach
- Lead with scope: var is function-scoped (or global at top level), while let and const are block-scoped, so a let declared inside an if or for body does not exist after the closing brace.
- Be precise about hoisting: all three declarations are hoisted, but var is initialised to undefined, whereas let and const stay in the temporal dead zone until the declaration line runs, so reading them earlier throws a ReferenceError (even typeof does).
- Show the loop case: for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) prints 3 3 3 because every callback closes over one shared binding; with let each iteration gets a fresh binding and it prints 0 1 2. This links the answer to closures.
- Cover the remaining differences: var can be redeclared in the same scope, and in a classic script a top-level var becomes a property of the global object (window in browsers); in an ES module a top-level var stays module-scoped and creates no window property. let and const cannot be redeclared and never create that property. const forbids reassigning the binding, not mutating the value, so const cart = [] still allows cart.push(item), and Object.freeze is only shallow.
- Close with your rule: const by default, let when the binding must change (counters, accumulators), var only when reading legacy code. Add that a mechanical var-to-let refactor can change behaviour where code relied on function scope, on using a variable before its declaration, or on a global window property that disappears once the file becomes a module.
Follow-up
- The loop prints 3 3 3; how would you fix it without changing var to let?
- If const does not make an object immutable, how would you stop other modules mutating a shared cart object?
- What could break if you replaced every var with let in a legacy file?
Convert the listing endpoint from offset pages to stable cursors
Medium · pagination · cursors · index access path
GET /v1/resources returns a tenant's resources newest-updated first, today with page and per_page, backed by index (tenant_id, status, updated_at DESC, resource_id DESC). Callers are a browser feed and a nightly sync job that walks every page. Users report items appearing twice or vanishing between pages, and page 400 is slow. Design the cursor contract: what the cursor contains and how it is encoded, the exact WHERE and ORDER BY, what happens when a row's updated_at changes mid-walk, how a client detects the end, and what the sync job does when a cursor is rejected.
Approach
- Separate the two defects, because they have different fixes. Cost: OFFSET n makes the engine produce and discard n rows, so price grows with page depth rather than page size and page 400 pays for 400 pages of work. Correctness: while the set shifts, rows cross the offset boundary and are skipped or repeated, and nothing in the response lets the client detect it.
- Write the seek: WHERE tenant_id = $1 AND status = $2 AND (updated_at, resource_id) < ($k, $id) ORDER BY updated_at DESC, resource_id DESC LIMIT n. The tie-break is not decoration - updated_at is not unique, and two rows sharing a timestamp across a page boundary reintroduce exactly the skip this was adopted to remove. PostgreSQL seeks the composite index on the row-value comparison directly; on an engine that does not optimise a row constructor, expand it into the equivalent OR form or the plan quietly degrades to a scan.
- Encode the cursor as an opaque token carrying the sort key, the id, and a fingerprint of the filter and sort order, signed or at minimum validated. A cursor replayed against a different sort or filter must be a 400 with its own code, not a silently wrong page - the sync job cannot notice the difference otherwise.
- State the guarantee precisely rather than generously. Keyset is stable against inserts and deletes elsewhere in the set, because the position is a value and not a count. It is not a snapshot: updated_at is mutable, so a row that is edited during the walk re-sorts and may be seen twice or not at all. If the sync job needs exactly-once coverage, order on an immutable key such as (created_at, resource_id), or walk resource_revision by revision_id and treat updated_at as data.
Follow-up
- The client wants a total count and the ability to jump to page 400. What can you honestly offer instead, and what does each option cost?
- How do you paginate backwards, and what does that require of the index?
Every query on one table stalls for forty seconds mid-deploy
Medium · lock contention · ddl · migrations
During a release on PostgreSQL, every query touching resource times out for about 40 seconds and then recovers with no intervention. The release ran one migration, ALTER TABLE resource ADD COLUMN archived_reason TEXT, and the migration log shows it completing in 6 ms. Unrelated tables showed no change in error rate. Explain how a 6 ms statement caused a 40-second stall, give the ordered checks you would run on a live system to confirm it, and give the migration procedure that prevents a repeat.
Approach
- Separate the statement's duration from the lock's duration. ADD COLUMN with no default is a catalogue-only change and genuinely runs in milliseconds, but it requires ACCESS EXCLUSIVE, and it cannot acquire that until every transaction already touching the table has finished.
- Account for the queueing, which is the part that surprises people. A lock request that is waiting blocks later requests for conflicting modes behind it rather than letting them overtake, so one long-open transaction holds the DDL and the DDL holds all the traffic. The stall length is set by the longest open transaction, not by the size of the change.
- Confirm on a live system in this order: pg_stat_activity for that table ordered by xact_start, looking for the oldest transaction and specifically for state = idle in transaction; then pg_locks where granted = false to find the waiter; then join them on pid to name blocker and blocked. pg_blocking_pids() does that join for you and is the fastest single call.
- Prevent rather than merely time it better. Set lock_timeout to a second or two on the migration session so the DDL abandons the queue after a bounded wait and is retried, instead of holding it for as long as the oldest transaction lives. Be exact about what that buys: queries arriving during the wait still queue behind the pending ACCESS EXCLUSIVE request, so each attempt costs them up to one lock_timeout of added latency. The outage goes from 40 seconds to about one second per attempt, not to zero. Also run migrations away from deploy-time peaks, and put a statement timeout and an idle-in-transaction timeout on the analytics role that opens the long transactions.
Follow-up
- The same release also wants NOT NULL on that column. What is the sequence that gets there without a long lock?
- Your lock_timeout retry fails ten times in a row because the analytics transaction is always open. What do you change?
Suggested practice checklist
Each day targets one reported topic (JavaScript fundamentals, async handling, API-driven rendering, the cart build, legacy refactoring, project stories) and ends with a working file or written notes, finishing with a full mock build.
Day 1 — Scoping, closures and prototypes
- Write ten short snippets mixing var, let and const (loop closures, temporal dead zone access, redeclaration, const object mutation), predict each output on paper, then run them in Node or the browser console.
- Implement once(fn), a counter factory and memoize(fn) with closures, and write one line on what each closure captures.
- Write the same Product type as an ES6 class and as a constructor function with prototype methods, then trace a method lookup through the prototype chain.
- Answer the let/const/var question aloud, timed, using two of your snippets as evidence.
Deliverable: A snippets file with predicted versus actual output, three closure utilities, the class/prototype pair with a written lookup trace, and a recorded spoken answer.
Day 2 — Promises, async/await and the event loop
- Predict the log order for five snippets mixing synchronous code, Promise.then, await and setTimeout, then check each one.
- Implement promiseAll and promiseAllSettled from scratch, keeping results in input order.
- Write mapWithLimit(items, limit, fn) that keeps at most limit calls in flight, plus a retry helper with exponential backoff for GET requests.
- Answer the Promises and async/await question aloud, timed, including error handling and cancellation.
Deliverable: Checked event-loop predictions, two Promise combinators, a concurrency-limited mapper, a retry helper and a recorded spoken answer.
Day 3 — Render a product list from an API
- In a blank HTML/JS file, fetch products from a public mock API, render name, image and a price formatted with Intl.NumberFormat, and show loading, error (including non-OK HTTP status) and empty states.
- Add a debounced search box that cancels stale requests with AbortController and ignores the resulting AbortError instead of showing the error state.
- Work the practice question on moving a listing endpoint from offset pages to stable cursors, then add a cursor-based load-more button to your list.
Deliverable: A working product-list page with three states, cancellable search and cursor-based load more, plus written notes on the pagination question.
Day 4 — Build the cart
- Model the cart as a map keyed by product id holding quantity and unit price in integer cents; write add, setQuantity (removing at zero), remove and a derived total, each returning new state.
- Connect one cart state to a header badge and a cart panel so both update together, and persist it to localStorage under a versioned key.
- Add an optimistic add-to-cart call against a mock endpoint that rolls back and shows an error when the request fails.
- Write unit tests for duplicate adds, zero, negative or non-integer quantities and an out-of-stock product.
Deliverable: A tested cart module wired into the Day 3 page, with a shared badge, persistence and optimistic rollback.
Day 5 — Refactor legacy code and harden network calls
- Take an ES5 file with nested callbacks or XMLHttpRequest, var and string concatenation, and convert it to async/await, let/const, arrow functions, destructuring, template literals and modules, listing every place behaviour could change (this binding, hoisting, and top-level var no longer creating a window property once the file is a module).
- Work the practice question on tracking a rolling failure rate per destination, then add a guard to your fetch helper that stops retrying an endpoint whose recent failure rate passes a threshold.
- Record a DevTools Performance trace of the product list, batch DOM writes with a DocumentFragment, and confirm you are not forcing layout inside a loop.
Deliverable: A before/after refactor with a written list of behaviour risks, a failure-rate guard in the fetch helper, and an annotated performance trace.
Day 6 — Project deep dives and behavioral stories
- For two past projects, write the architecture (components, data flow from API to UI, who owns which state), one trade-off with the alternative you rejected, and a measured outcome.
- Prepare the outage story and the reversed-decision story from the practice behavioral questions, with numbers attached to each.
- Prepare a story about helping a stuck teammate, then rehearse explaining one technical decision from your projects in plain business terms.
Deliverable: Two project one-pagers, three behavioral stories with numbers, and a recorded plain-language explanation of one decision.
Day 7 — Full mock
- Timed, in a blank file: build a product list from a mock endpoint with an add-to-cart button and a running total, talking through each decision aloud.
- Have a partner ask the let/const/var and Promise questions cold and follow up with output-prediction snippets you have not seen.
- Write three questions for your interviewers about the team's current challenges and its project roadmap.
Deliverable: A completed mock build, notes on which conceptual answers wobbled, and three questions to ask.
Project and ownership stories
No specific behavioral questions are reported for this role. Candidates do report that the later technical and manager-led discussions go deep on past projects and the reasoning behind technical choices, and that client-facing work means linking technical detail to business value. Each story should carry an architecture, a trade-off you owned and a result you measured.
- Walk through the architecture of a web application you built and deployed: the components, how data moved from the API to the screen, and one trade-off you would make differently now.
- Describe a feature where the requirement from a product or client team was unclear. What did you ask before writing code, and how did the shipped version differ from the first request?
- Tell me about a production bug in a feature you owned. How did you reproduce it, which developer tools did you use, and what did you change so it would not recur?
- Explain a technical decision on a client-facing project in terms a non-engineer would accept: what it cost, what it saved and how you measured that.
- Describe a code review where you disagreed with a more senior engineer, or they with you, and how it was resolved.
Unblock an engineer without taking the keyboard
Easy · mentoring · leases · at-least-once
A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.
Approach
- Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
- Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
- Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
- Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.
Follow-up
- How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
- Their handler calls an external endpoint before recording that it did. What do you tell them to change first?
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
- 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.
- 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.
- 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.
- 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?
Reverse your own decision and price the reversal
Medium · reversibility · measurement · migrations
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?