cswork / libraryOpen practice workspace

Interview guides · Liftoff

Liftoff Software Engineer Interview Guide

Testing practice for Liftoff software engineer candidates: the boundaries worth covering, and why flakiness is a real defect.

Interviewing at Liftoff

  • Practice bank: 5+ questions
  • Rounds: 2
  • Typical prep: 1–2 weeks

Interview rounds

RoundQuestions
Technical Screen1
Onsite4

How hard is the Liftoff Software Engineer interview?

DifficultyShareQuestions
Easy0%0
Medium100%5
Hard0%0

What Liftoff actually tests for

TopicShareQuestions
Coding & Algorithms80%4
Software Engineering Fundamentals20%1

The 60-second brief

As a Software Engineer at Liftoff, you are at the core of a high-scale, data-intensive platform that powers mobile marketing and retargeting campaigns. You are not just writing code; you are building systems that process billions of events daily, optimizing bidding algorithms, and ensuring that mobile advertisements reach the right users at the right time. Your work directly impacts the revenue and growth of the world’s leading mobile apps.

Liftoff candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

What a Software Engineer does at Liftoff

This role is both technically demanding and strategically significant. You will often work on problems related to distributed systems, real-time data processing, and large-scale infrastructure. Whether you are optimizing a backend service for lower latency or building a new feature to enhance campaign performance, you are expected to operate with high autonomy. Liftoff values engineers who can balance the need for robust, scalable design with the agility required to move fast in a competitive adtech market.

Liftoff places a high premium on candidates who demonstrate practical problem-solving skills over purely theoretical knowledge. Be prepared to show how your code handles real-world constraints.

The Liftoff interview process

Liftoff candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Technical Screens

Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.

What it evaluates

  • Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
  • Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
  • Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
  • Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply

How to prepare

  • For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
  • For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
  • Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last

Super Day

A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.

What it evaluates

  • Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
  • Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
  • Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
  • Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing

How to prepare

  • Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
  • Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
  • Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one

Coding Project

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What it evaluates

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Feedback Loop

Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.

What it evaluates

  • Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
  • Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
  • Whether you establish what decision is on the table before proposing anything
  • Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip

How to prepare

  • Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
  • Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
  • Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it

Questions Liftoff candidates report

Data structures and algorithms

State the bound, then read your own code against it, because the cost you forgot is usually inside a call you did not write. A membership test against a list is linear while the same test against a set is expected constant, so one of those inside a loop turns a linear scan quadratic. Taking a slice copies the elements wherever slices are values rather than views. Repeated string concatenation is quadratic wherever strings are immutable, unless you accumulate into a builder or a list and join once. None of that is visible in the shape of the algorithm, and all of it is visible in the runtime.

  • Write a function to manipulate a matrix (e.g., printing or transforming).
  • Can you implement a solution for a graph traversal problem?
  • Build a functional application (e.g., a game or a utility) given a specific set of requirements.
  • How would you approach a problem involving string manipulation and character counts?

System design and architecture

A design is a set of promises about what happens when something is slow, down, or delivered twice, and that is the part usually left unsaid. For each dependency, say what the system does when it is unavailable: serve stale data, reject the write, or accept it and settle later. For each relaxation of consistency, describe the anomaly a user would actually notice, in the product's own words, rather than naming a consistency model and moving on. The separator is volunteering the failure behaviour as part of the design instead of producing it only when pushed.

  • How would you design a system to handle high-throughput event logging?
  • How do you ensure your system remains available under heavy load?

Behavioural and engineering judgement

Every question in this category has a number in it somewhere, and engineers routinely tell the story without one. What the outage cost, what the delay cost, what the rewrite consumed in engineer-weeks against what it saved, and who absorbed it when it was not users: the on-call rotation, another team's quarter, the support queue. You will rarely have an exact figure, and an estimate with its method stated beats both a precise-sounding invention and silence. Say how you got to it and what it leaves out. That is the sentence that makes the follow-up about whether it was worth doing answerable at all.

  • How do you handle concurrency in your application design?

Practice exercises with worked solutions

The prompts above tell you what gets asked.

Count minted supply against a cap from grant history

item_definition(item_def_id INT PK, mint_cap INT NULL). An auditor asks how many units of a capped cosmetic have ever been minted. The first answer offered is SELECT SUM(quantity) FROM item_instance WHERE item_def_id = :d. In this model stacks split into new rows, merge away, have quantity decremented in place on consumption and are soft-deleted at zero, and grant_receipt stores only a payload_hash rather than itemised contents. Propose the model change that makes minted supply countable, write the audit query, and describe the counter that enforces the cap at mint time and what it costs in throughput.

How a strong candidate works it

  • Kill the proposed query precisely. SUM(quantity) measures current supply, not minted supply: consumption decrements in place and soft-deletes at zero, so the figure only ever falls. Counting rows instead fails differently — splits create a row and merges retire one while the total is unchanged. Destroyed supply must still count against the cap or craft-and-destroy is an unlimited mint.
  • Move the fact to where it is written exactly once. Minting is an event, so itemise it: grant_line(grant_id UUID REFERENCES grant_receipt(grant_id), line_no SMALLINT, item_def_id INT, quantity INT CHECK (quantity > 0), PRIMARY KEY (grant_id, line_no)) turns the opaque payload_hash into something countable per definition without parsing a blob.
  • Write the audit as a sum over applied lines, and state the reversal convention you assume: either the original's status flips to reversed and the filter excludes it, or the original stays applied and a reversal grant carries offsetting lines that must be netted. Both are defensible; leaving it unstated makes the number wrong under one of them.
  • Enforce at mint time with the same conditional single-statement shape as a wallet debit: UPDATE def_supply SET minted_total = minted_total + :q WHERE item_def_id = :d AND minted_total + :q <= cap, with an affected-row count of zero meaning the cap is reached. The audit query is the drift check against this counter, not the enforcement.
  • Cost the counter honestly. It is one hot row per capped definition and every mint serializes on its row lock, so the ceiling is roughly one mint per lock hold — on the order of 10^3 per second with a millisecond transaction. That is fine for a capped cosmetic and not fine for anything minted per match.
  • If that ceiling binds, lease blocks of quota to shards and name both failure modes it introduces: an unreachable shard strands its unused lease as supply that exists on paper but cannot be minted, and reclaiming a lease after a false-positive failure detection double-allocates. During a partition the choice is to block minting or to breach the cap, and it has to be made deliberately rather than discovered.

Follow-ups

  • Two regions minted under leases and the partition heals with the cap breached by 40 units. What happens to those 40 items?
  • The auditor wants the figure as of a date three months ago. Does the model answer that, and at what cost?
  • Which detection signal over this data would surface a duplication bug before the auditor asks?

Spend soft currency without losing an update under concurrency

wallet(wallet_id BIGINT PK, account_id BIGINT, currency_code TEXT, balance_minor BIGINT NOT NULL CHECK (balance_minor >= 0)) holds maintained balances. currency_ledger(entry_id BIGSERIAL PK, txn_id UUID, wallet_id BIGINT, delta_minor BIGINT, balance_after_minor BIGINT, reason TEXT, idempotency_key TEXT NOT NULL UNIQUE) is the append-only history. A player with two clients open buys the same crafting material twice, four milliseconds apart, from a balance of 500 at 300 each. Write the SQL that debits :amount from :wallet_id and records the ledger leg so neither spend is lost and the balance cannot go negative. State the isolation level you assume and what the caller does when the debit does not apply.

How a strong candidate works it

  • Name the exact interleaving first: both transactions SELECT balance_minor and read 500, both find 500 >= 300, both UPDATE wallet SET balance_minor = 200. Final balance is 200 after 600 was spent, and the CHECK (balance_minor >= 0) never fires because 200 is a legal value — the constraint cannot catch this class of bug.
  • Collapse the read and the write into one statement: UPDATE wallet SET balance_minor = balance_minor - :amount WHERE wallet_id = :w AND balance_minor >= :amount RETURNING balance_minor. Under PostgreSQL READ COMMITTED the second writer blocks on the row lock, then re-evaluates the WHERE clause against the newly committed row version, so it matches zero rows instead of overwriting.
  • Branch on the affected-row count, not on a re-read. Zero rows is insufficient funds — a business outcome the caller returns to the player, not an exception.
  • Insert the ledger leg inside the same transaction, taking balance_after_minor from the UPDATE's RETURNING rather than a second SELECT, and set idempotency_key from server-derived state so a retry collides on the unique index (SQLSTATE 23505) instead of debiting twice.
  • State the isolation contrast: under REPEATABLE READ or SERIALIZABLE the same statement aborts with SQLSTATE 40001 rather than matching zero rows, so the caller owes a bounded retry loop with a distinct error path.
  • Keep the transaction short — the wallet row lock is held until commit, so no store-service call, receipt validation or other network I/O belongs inside it.

Expected result (20 min): From a balance of 500 with two concurrent spends of 300, exactly one UPDATE reports one affected row and leaves 200; the other reports zero rows and returns insufficient funds with no ledger leg written. Under REPEATABLE READ the second raises SQLSTATE 40001 instead.

Check yourself

  • The failing spend writes no currency_ledger row — a leg with no matching balance movement is exactly the drift a reconciliation job hunts.
  • balance_after_minor on the successful leg equals the UPDATE's RETURNING value, not a value re-read afterwards.
  • Replaying the same request with the same idempotency key raises 23505 and the caller returns the original result rather than an error.
  • The CHECK (balance_minor >= 0) constraint never fires on either path, confirming it is a backstop rather than the mechanism.

Follow-ups

  • The ledger leg is written and a later statement in the transaction fails, rolling back the debit. What does the reconciliation job see, and which of the two rows is authoritative?
  • A refund lands days after the currency was spent. Do you carry a negative balance as a debt, or record a shortfall that blocks future purchases?
  • How does this change if the wallet row and the ledger row live in different databases with no shared transaction?

Detect grant bursts with an exact sliding window per account

grant_receipt rows stream in at roughly 350 per second as (account_id, grant_id, source_type, source_ref_id, created_at), with non-decreasing timestamps. Raise an alert the first time any account accumulates 4 or more grants of the same source_type inside any 10-minute window, and include the offending grant ids. Memory must be bounded and one burst must not alert repeatedly. Give the amortised per-event cost, the expected number of live entries, and state precisely what a 10-minute tumbling-bucket counter would miss.

How a strong candidate works it

  • Hash map from (account_id, source_type) to a deque of timestamps. Per event: push the new timestamp, pop from the front while front <= now - 600s, then test the deque length. Each timestamp is pushed once and popped once, so the cost is O(1) amortised per event even though a single event can pop many.
  • Do the arithmetic before reaching for an approximation: 350 events per second times a 600-second window is about 210,000 live timestamps, single-digit megabytes. An exact window is affordable here, so a probabilistic counter would trade accuracy away for nothing.
  • Bound memory against key explosion rather than against the window: drop a (account, source_type) key when its deque empties, and cap each deque at the threshold. Keeping only the last 4 timestamps is exactly sufficient, because the tightest set of 4 ending at the current event is the last 4; if those span more than 600s, no set of 4 fits the window ending now.
  • Suppress repeat alerts explicitly. Record the newest timestamp at fire time and stay quiet until the deque has drained past it, or apply a fixed refractory period. Without this a sustained burst alerts on every subsequent event and the alert channel becomes unreadable at exactly the moment it matters.
  • Quantify the tumbling-bucket failure rather than asserting it: with buckets of width W and threshold t, an adversary places t-1 events in each of two adjacent buckets, so 2t-2 events can occur inside a span shorter than W with neither bucket tripping.

Expected result (20 min): Exactly one alert, fired on the event at minute 12, naming the four grant ids at minutes 8, 9, 11 and 12, whose span is 4 minutes. The deque never exceeds 4 entries. The tumbling version counts 2 and 2 and never alerts.

Check yourself

  • The shifted replay still produces exactly one alert at the same relative position, whereas the tumbling counter's output changes under the same shift, which is the failure made visible.
  • Total pops never exceeds total pushes, and both equal the event count once the stream drains.
  • Spacing the same four events 11 minutes apart produces no alert at all.

Follow-ups

  • Two producers clock-skew and timestamps stop being non-decreasing. What breaks in the deque invariant, and what do you do about it without buffering the whole stream?
  • Change the rule to 4 grants sharing one source_ref_id across different accounts, which is a duplication signal rather than a velocity one. What changes in the key, and what changes in the memory bound?

Paginate an inventory endpoint that mutates while you read

A game client calls GET /accounts/{id}/inventory at session start and must load every stack; accounts hold 10^2 to 10^4 rows in item_instance (instance_id UUID, owner_account_id, item_def_id, quantity, bind_state, escrow_id, acquired_at, consumed_at, row_version). Trades and escrow locks mutate rows mid-walk, and consumed stacks are soft-deleted rather than removed. On a failed page the client retries that page only. Specify the pagination contract - parameters, cursor contents, ordering, and the index that serves it - and state what the client is guaranteed about rows that change during the walk.

How a strong candidate works it

  • Reject OFFSET on both counts: page N scans and discards N rows, so a full walk of a 10^4-row inventory is quadratic, and concurrent inserts or soft-deletes shift the window so rows are skipped or repeated across page boundaries.
  • Use keyset pagination: ORDER BY acquired_at, instance_id with WHERE (acquired_at, instance_id) > (:last_ts, :last_id). The instance_id tie-break is mandatory, not cosmetic - acquired_at defaults to now(), which in PostgreSQL is transaction start time, so an entire reward fan-out inserted in one transaction carries identical timestamps.
  • Serve it with a partial composite index on (owner_account_id, acquired_at, instance_id) WHERE consumed_at IS NULL. Each page is one index range scan costing O(log n + limit) instead of O(offset).
  • Make the cursor opaque and self-describing: encode the sort key plus a hash of the filter set and a format version, so a cursor replayed against different filters is rejected rather than quietly returning the wrong rows.
  • State the guarantee honestly rather than implying a snapshot. Keyset gives stable progress, not a consistent point-in-time view: a row mutated after its page was served is already stale in the client. Either return an as_of token the client can pass to a delta endpoint, or return row_version per row and require it on every mutating call, which converts a stale read into a rejected write instead of a double spend.

Expected result (25 min): GET /accounts/{id}/inventory?limit=500&cursor=<opaque>, ordered by (acquired_at, instance_id), served by a partial index on (owner_account_id, acquired_at, instance_id) WHERE consumed_at IS NULL. Cursor decodes to {v, acquired_at, instance_id, filter_hash}. Response is {items:[{...,row_version}], next_cursor, as_of}. Documented guarantee: pages do not repeat or skip rows that existed for the whole walk, no snapshot isolation across pages, and mutations require the row_version returned with the row.

Check yourself

  • The sort key is unique, so no two rows can compare equal at a page boundary.
  • Page cost does not grow with page number - the query plan is an index range scan starting at the cursor, not a scan-and-discard.
  • A row soft-deleted mid-walk cannot cause a different, untouched row to be skipped.
  • A cursor issued under one filter set is rejected, not silently reinterpreted, when the filter changes.

Follow-ups

  • How does the client learn that a stack it already paged was consumed or escrowed behind the cursor?
  • What changes if a cursor can be resumed an hour later, after the underlying rows have been archived?
  • The client needs a total count for a progress bar. What do you serve, and what does it cost at 10^4 rows?

Session start latency jumped tenfold after an inventory refactor

After a release, p50 session-start time rose from 38 ms to 260 ms and p99 from 120 ms to 1.4 s. Inventory service CPU is flat; the PostgreSQL primary shows statement count up 30x with unchanged mean statement time and unchanged rows returned per statement. The release replaced one item_instance query with a repository layer that loads a stack, then fetches its item_definition row per stack. Accounts hold 10^2 to 10^4 stacks. Produce an ordered diagnostic checklist, the root cause, and the fix with its expected latency.

How a strong candidate works it

  • Split the regression by layer before reading any code: compare service-side p50/p99 against database-side total time per request, taken as a pg_stat_statements delta across the release. Statement count up 30x with mean statement time flat means per-statement cost is fine and the count is the defect, so stop looking at indexes and plans.
  • Confirm the shape: order pg_stat_statements by calls for the window. An N+1 appears as one normalised query whose calls track stacks-per-account rather than requests per second, with a tiny mean_exec_time — it never shows up in a slow-query log.
  • Do the arithmetic rather than asserting it is slow: the cost is 1 + N serial round trips. At a 0.4 ms in-datacentre round trip and N = 500 stacks that is roughly 200 ms of pure waiting no index can remove, and it scales with account size, which is why p99 blew out further than p50.
  • Collapse to two statements: load the stacks, then one SELECT over item_definition WHERE (item_def_id, item_def_version) IN (...) across the distinct pairs, and hydrate in memory. Distinct definitions per account are in the tens, so the second statement is a small keyed lookup.
  • Respect the domain constraint in the batch key: the join is on (item_def_id, item_def_version), not item_def_id alone. Batching on the def id alone silently serves the current catalog version to stacks that were minted under an older one, which is a correctness bug dressed as a performance fix.
  • Prevent recurrence with a per-request statement-count budget asserted in an integration test, so the next accidental lazy load fails the build instead of a release.

Follow-ups

  • Definitions are nearly immutable. What changes if you cache them in-process, and what invalidates that cache when a new catalog version is published?
  • How would you bound the worst case for a 10^4-stack account, where even the two-query version returns a large result set on every session start?

Ship to a fixed date with debt you can name

Prepare a five-minute account of shipping to a date that could not move, with shortcuts you took knowingly. Pick work where the load was also spiky — a launch, a seasonal rollover, a migration window — rather than a routine sprint. Constraint: name at least two shortcuts you took deliberately and one you refused, and say which invariant each one put at risk. Deliverable: the decision as it stood at the time, the kill switch or fallback you kept, the debt's paid-or-open status today, and what you would cut first if the date moved closer.

How a strong candidate works it

  • Anchor on what made the date immovable and what made the load spiky. An event or rollover that multiplies write volume by 20 to 50 times for a few hours is not simply more of the same traffic; it is the concurrency at which lost-update and duplicate-write races stop being theoretical and start firing on every deploy.
  • Classify each shortcut by reversibility. A skipped cache warm is recoverable in minutes. A skipped unique index on an idempotency key creates value or state you cannot claw back once it has moved between accounts. Strong answers cut only in the recoverable column and say explicitly that the line exists.
  • Name the refusal and its price in days or scope. Someone who cut nothing was not really under a deadline; someone who cut everything had no line. The refusal is where the judgement is visible.
  • Describe the fallback you kept live: a flag that disables the path without a deploy, a consumer you could pause, a dual-write with the old path still authoritative. Say who could pull it at three in the morning and whether that person had been told.
  • Close with the debt's state today and the mechanism that kept it visible — a ticket with an owner and a trigger condition beats a wiki page. 'We never went back' is an acceptable ending only if you can say what made it acceptable.

Follow-ups

  • Which of those shortcuts would you refuse at any deadline now, and what changed your mind?
  • How did you communicate the residual risk to whoever owned the date?

Decide a rollback that also reverses legitimate user actions

Prepare a six-minute account of an incident where the clean fix would also have undone correct work by real users — a replayed batch, a bad backfill, value granted twice and then spent. Constraint: you must give the option you rejected and the number that decided it. Deliverable: how you bounded the affected set, the three options you weighed (full reversal, partial reversal under a rule, absorb the loss and close the hole), who outside engineering had to agree, and how you explained the residual harm to someone who does not read code.

How a strong candidate works it

  • Start with bounding, because every option depends on it: the predicate that separates affected rows from unaffected ones, the time window you were willing to trust, and your confidence at the edges. Say what you did with rows you could not classify, and that over-including or under-including them was a deliberate choice rather than an accident.
  • Lay out the three options with their asymmetric costs. Full reversal is the cheapest to implement and the hardest to defend, because it reverses correct actions taken in good faith. Partial reversal needs a rule a support agent can restate in one sentence. Absorbing the loss costs money once and preserves trust, but only if you can show the hole is closed.
  • Give the number that decided it — affected accounts, value at stake, ratio of collateral to genuine — and name who owned the decision. An incident of this shape is rarely solely an engineering call, and treating it as one is itself the scope error the question is looking for.
  • Describe how you made the repair idempotent and re-runnable, because a compensation applied twice is a second incident on top of the first. A reversal written as a new row referencing the original can be audited and replayed; an in-place update cannot be either.
  • Close with the prevention and the proof it holds: the constraint added, the test that fails without it, and the monitor that would catch the same shape arriving from a different source.

Follow-ups

  • Support cannot restate your partial-reversal rule in one sentence. What do you change — the rule or the explanation?
  • Some of the value was already spent before you acted. Do you leave a negative balance carrying a debt, or record a shortfall that blocks future spending?
  • How would you make the same call with half the data and half the time?

Where candidates lose points

Representing prices and balances as floating point, or storing a display currency amount instead of integer minor units plus a currency code.

Binary floating point cannot represent most decimal fractions exactly, so repeated credits and debits accumulate drift that only surfaces when reconciliation compares the ledger sum against the cached balance and finds them a fraction apart. Regional price books make it worse: the same SKU has different minor-unit scales across currencies, and a currency with no minor unit breaks any code that assumes two decimal places.

Caching inventory or entitlement at the edge or in the client without a version token, so a spend is validated against a stale copy.

Cache invalidation races the spend directly: a player with two sessions can spend the same stack twice inside the TTL, and a revoked entitlement stays usable until it expires. The symptom looks like a duplication bug in the inventory service, so investigation starts in the wrong place. Serving the token (a row version or ETag) and requiring the client to present it on the mutating call turns a stale read into a rejected write instead of a double spend.

Choosing a schema before the access patterns are known

Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.

Abandoning working code to chase the optimal solution

Get the straightforward version correct, state its complexity, and only then optimise, keeping the working version until the faster one passes the same cases. A correct quadratic solution with a stated path to linear beats a half-written optimal one that never ran.

Weakness triage, driven by a day-one self-assessment

Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.

Day 1 — Diagnostic, scored before you study anything

  • Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
  • Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
  • Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.

Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.

Day 2 — Largest gap: find the boundary rather than the subject

  • Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
  • Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
  • Re-attempt one of them from blank four hours later with nothing open.

Deliverable: A sub-skill map with the two blocking sub-skills circled.

Day 3 — Drill the blocking sub-skill by repeating the shape

  • Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
  • State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
  • Have someone else read your one-sentence rule and find the precondition you left out.

Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.

Day 4 — Second gap, plus maintenance on the strongest area

  • Run the same sub-skill decomposition on the second-largest gap in half the time.
  • Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
  • Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.

Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.

Day 5 — The gap that is not a skill

  • Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
  • Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
  • Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.

Deliverable: Two recordings with a counted reduction in time-to-first-question.

Day 6 — Retest under day-one conditions

  • Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
  • For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
  • Write down which single block you would still lose the offer on.

Deliverable: A second scored rubric placed beside the first, with one named remaining risk.

Day 7 — Full loop under interview conditions

  • Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
  • Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
  • Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.

Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.