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.
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.
Technical Screens
reportedInput 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 to demonstrate
- 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
reportedA 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 to demonstrate
- 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
reportedThe 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 to demonstrate
- 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
reportedWhere 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 to demonstrate
- 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
PracHub editorial advice for the preparation topics above.
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.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Write a function to manipulate a matrix (e.g., printing or transformin…
Write a function to manipulate a matrix (e.g., printing or transforming).
Approach
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Can you implement a solution for a graph traversal problem?
Can you implement a solution for a graph traversal problem?
Approach
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Build a functional application (e.g., a game or a utility) given a spe…
Build a functional application (e.g., a game or a utility) given a specific set of requirements.
Approach
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
How would you approach a problem involving string manipulation and cha…
How would you approach a problem involving string manipulation and character counts?
Approach
- State the target complexity and say which constraint rules the naive version out.
- Name the brute-force solution and its complexity before improving on it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
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.
Approach
- 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.
Worked solution 20 min
- Take one account and one source_type with grants at minutes 8, 9, 11 and 12, threshold 4 in any 10-minute window.
- Walk the deque event by event, recording its contents and the front-eviction test each time.
- Score the same four events through 10-minute tumbling buckets [0,10) and [10,20).
- Replay the sliding-window version with every timestamp shifted forward by 3 minutes and compare.
Follow-up
- 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?
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.
Approach
- 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 opaquepayload_hashinto 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
statusflips toreversedand the filter excludes it, or the original staysappliedand 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-up
- 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.
Approach
- Name the exact interleaving first: both transactions
SELECT balance_minorand read 500, both find 500 >= 300, bothUPDATE wallet SET balance_minor = 200. Final balance is 200 after 600 was spent, and theCHECK (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_minorfrom the UPDATE's RETURNING rather than a second SELECT, and setidempotency_keyfrom 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.
Worked solution 20 min
- Write the naive version and draw a two-column timeline of T1 and T2 statements with the stored balance after each, showing the final value is 200 rather than -100 or a rejection.
- Rewrite it as one UPDATE with the guard in the WHERE clause, returning the new balance.
- Run both spends from two sessions, holding
BEGINopen on the first to force the second to block, and record the second statement's row count. - Add the ledger INSERT in the same transaction using the RETURNING value, with an idempotency key built from the order or match that authorised the spend.
- Replay the second spend with the same idempotency key and confirm it fails on 23505 rather than debiting again.
Follow-up
- 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?
How would you design a system to handle high-throughput event logging?
How would you design a system to handle high-throughput event logging?
Approach
- Choose a partition key and say what query it makes expensive.
- Fix the scope first: who calls this, how often, and what they do when it fails.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
How do you ensure your system remains available under heavy load?
How do you ensure your system remains available under heavy load?
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the failure you are designing for, then the recovery path.
- Choose a partition key and say what query it makes expensive.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
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.
Approach
- 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.
Worked solution 25 min
- Write the naive query with LIMIT 500 OFFSET 1000 and the keyset query side by side, and name the index each one needs.
- Construct a concrete interleaving: the client reads page 1, a trade soft-deletes a row that sorted into page 1, the client reads page 2 with OFFSET 500 - identify exactly which instance_id is never returned.
- Insert two rows in one transaction and confirm they share an acquired_at value, then show that a cursor holding only acquired_at either repeats or skips one of them.
- Define the cursor payload and the response envelope, including what the server does with a cursor whose filter hash does not match the current request.
- Write one sentence of contract text stating the consistency guarantee, and one stating the client's obligation to present row_version on mutating calls.
Follow-up
- 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.
Approach
- 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-up
- 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?
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Diagnostic, 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Largest 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.
Practice prompt ↗Practice prompt ↗03Drill 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.
Practice prompt ↗Practice prompt ↗04Second 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗05The 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.
Practice prompt ↗Practice prompt ↗06Retest 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.
Practice prompt ↗Practice prompt ↗07Full 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Every story you tell gets read for blast radius and judgement: what could have broken, who else it touched, what you knew at the moment you decided. Nobody can audit your code in an hour, so they audit your reasoning instead. Pick work where the call was genuinely yours and the consequences were real enough to remember.
How do you handle concurrency in your application design?
How do you handle concurrency in your application design?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
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.
Approach
- 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-up
- 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.
Approach
- 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-up
- 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?
- 01
How do you handle concurrency in your application design?
- 02
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.
- 03
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.
Is this an official Liftoff interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Liftoff. Rounds and questions reflect what candidates have reported, not a process Liftoff has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the interview process take?
The process typically spans 4 to 8 weeks, though it can be expedited for strong candidates. The recruiting team is known for being highly responsive and efficient in scheduling.
PracHub interview research ↗Is the 4-hour coding project difficult?
It is designed to be a realistic reflection of the work you would do on the job. Focus on creating a working, well-structured solution rather than over-engineering the design.
PracHub interview research ↗How do I stand out?
Successful candidates demonstrate a strong sense of ownership and an ability to communicate their thought process clearly while coding. Showing that you can iterate on your own work is highly valued.
PracHub interview research ↗Does Liftoff offer remote work?
Policies can vary by team and location, but Liftoff has successfully conducted many remote interview processes. Clarify specific location requirements with your recruiter early on.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-24