Interview guides · Cloud Imperium Games
Cloud Imperium Games Software Engineer Interview Questions & Guide 2026
Cloud Imperium Games software engineer prep: algorithm drills where every complexity claim has to name the assumption under it.
The 60-second brief
A Software Engineer at Cloud Imperium Games is at the heart of building the most ambitious persistent universe in gaming history. Whether you are working on complex AI behaviors, high-performance network infrastructure, or mission-critical IT systems, your work directly impacts the fidelity, stability, and immersion of the player experience. You are not just writing code; you are solving unique engineering challenges at a scale that pushes the boundaries of modern game engine technology.
Find out what you will be typing into. A shared plain-text editor with no autocomplete, compiler or test runner changes what you have to hold in your head, and practising inside your own configured environment hides exactly that gap.
Focus areas
- Budget per-tick simulation, serialization and bandwidth
- Prevent negative balances with conditional single-statement debits
- Make cross-service trades atomic through escrow sagas
Overview
The role requires a blend of technical precision and creative problem-solving. Because Cloud Imperium Games operates as a global, collaborative studio, you will work alongside artists, designers, and fellow engineers to bridge the gap between complex technical systems and high-end gameplay. Success in this role means contributing to a product where technical efficiency is synonymous with player enjoyment, requiring you to be both a rigorous developer and a team-oriented collaborator.
The Cloud Imperium Games interview process
Cloud Imperium Games candidates report 2 rounds · ≈ 2-4 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
Before anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.
What to demonstrate
- Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
- Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
- Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled
How to prepare
- Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
- Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
- Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market
Technical Rounds
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 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
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.
Read-modify-write on a balance or a stack quantity - SELECT the current value, check it in application code, then UPDATE to the computed new value.
The gap between the read and the write is a lost-update window of a few milliseconds. It survives every code review and every load test run at baseline concurrency, then fires reliably when a seasonal event puts two requests on the same wallet inside that window, or when one player has two clients open. The fix is one statement - UPDATE ... SET qty = qty - :n WHERE qty >= :n - plus an affected-row check; the trap is that the broken version returns the correct answer in every single-threaded test you will write.
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.
Check-then-act on shared state
Read, decide, write is not safe under concurrency unless the decision and the write are one atomic step: a unique constraint with conflict handling, a compare-and-set, or a row lock held for the whole transaction. Two requests can both pass the existence check before either inserts, which shows up as duplicate rows under load and never in a single-threaded test.
Questions & practice
Merge cheapest active listings across shards without full scans
Medium · k-way merge · heap · sharding
market_listing is sharded by seller_account_id across 64 shards. A buyer opens the page for one item_def_id and wants the K = 50 cheapest status='active' listings by ask_price_minor, ties broken by created_at. Each shard can return its own listings for that def already sorted, in pages. Catalog load is concentrated: a popular def may have a million active rows shard-wide. Return the global cheapest K while reading as few rows as possible, state the number of rows you read, and name the index that makes each shard's page cheap.
Approach
- k-way merge driven by a min-heap holding one entry per shard, keyed on the full tuple (ask_price_minor, created_at, listing_id) so the ordering is total and the same query returns the same page twice. Heapify 64 heads in O(S), then K pops at O(log S) each: O(S + K log S), roughly 64 plus 50 times 6 comparisons.
- Rows read is at most S + K, here 114, against the million active rows for that def. Page each shard lazily, requesting its next row only when its current entry is popped, and batch the request into pages of roughly K/S plus margin so the round trips amortise.
- The per-shard query needs a partial composite index on (item_def_id, ask_price_minor, created_at) WHERE status = 'active'. Without it, each shard sorts a million rows to hand you one, and the partial predicate keeps the index off the sold, cancelled and expired rows that dominate the table over time.
- Name the other shape too: over a single unsorted stream of n rows, use a bounded max-heap of size K for O(n log K) time and O(K) space, rejecting an incoming row in O(1) when its price is worse than the heap root. Quickselect is O(n) average but needs all n resident and gives up streaming.
- State the concurrency caveat rather than pretending the result is a snapshot: the shards are read at slightly different instants, so a listing can sell between two shard reads. Revalidate the chosen listing's status transition at purchase time; the merge result is a view, not a lock.
Worked solution (25 min)
- Take four shards with sorted prices in minor units: s0 = [120, 130, 400], s1 = [125, 126, 127], s2 = [500], s3 = [118, 900], and K = 5.
- Seed the heap with the four heads (118, 120, 125, 500) and record the read count as 4.
- Pop five times, advancing only the popped shard and incrementing the read count on each advance that returns a row.
- Sort the full nine-row union independently and compare the first five entries.
Expected result: The cheapest five are 118, 120, 125, 126, 127. The merge reads 8 rows out of the 9 that exist: 4 heads plus 4 successors, with the final pop draining s1 and triggering no read. The heap never holds more than 4 entries.
Verify it yourself
- Rows read never exceeds S + K, which is 9 here and 114 at the stated production numbers.
- Heap size never exceeds the number of shards that still have rows.
- The merged output is identical, element for element, to the first K of the fully sorted union.
Follow-up
- The buyer asks for page 20 rather than page 1. What does deep offset cost in a k-way merge, and what do you change to make it flat?
- One shard times out. Do you return 49 listings, serve a cached page, or fail the request, and how does the answer differ for a browse page versus a purchase confirmation?
Trace a duplication ring through time-respecting trade edges
Medium · temporal graph · reachability · fraud detection
An investigation gives you a seed set of accounts known to hold duplicated ItemInstances as of time t0, plus the day's completed trades as edges (from_account, to_account, instance_id, traded_at): 40 million edges over 20 million accounts. Return every account that could have received tainted supply, meaning reachable from a seed along a path whose timestamps are non-decreasing and all at or after t0. A trade that happened before its sender was tainted carries nothing. Give the complexity, the memory bound, and say why an ordinary BFS over the edge set is wrong.
Approach
- Recognise this as earliest-arrival temporal reachability, not static reachability. Sort the edges by traded_at ascending, O(E log E) over 40 million edges, sorting on a packed (timestamp, u, v) key rather than dragging the full row through the sort.
- Keep taint_time[account], initialised to t0 for seeds and to positive infinity elsewhere. With dense account ids a flat int64 array over 20 million accounts is 160 MB; with sparse ids use an open-addressed map sized to the seed's expected reach rather than to the account space.
- One forward pass in time order: for edge (u, v, t), if taint_time[u] <= t then taint_time[v] = min(taint_time[v], t). One pass suffices because every edge that could have tainted u carries a timestamp at most t and has therefore already been processed. O(E) after the sort, O(V) space.
- Fix the equal-timestamp rule explicitly and defend it. Letting same-timestamp edges chain lets a single millisecond propagate arbitrarily far through the graph; the defensible default applies a whole equal-timestamp batch against the taint set as it stood before the batch.
- Explain why static BFS is a strict over-approximation: it accepts paths that run backwards in time, which in a transfer graph asserts that an account received supply it could not yet have held. On a graph with heavy repeat trading between the same accounts, that inflates the rollback set well past what the evidence supports, and a rollback list is something you have to defend to real players.
Follow-up
- The rollback has to be defensible to the player whose items you claw back. What do you record per tainted account so you can produce the path that implicated them?
- Trades keep arriving. Make it incremental, then say what happens when a late edge shows up out of timestamp order.
- Propose two signals over these same edges that separate a duplication ring from a high-volume legitimate trader.
Audit ledger legs for unbalanced transactions in one pass
Easy · ledger · hashing · reconciliation
You are handed one day's currency_ledger partition as a stream of legs: txn_id UUID, wallet_id, currency_code, delta_minor BIGINT (signed), reason. Legs arrive in arbitrary order, not grouped by txn_id. Reasons in MINT_SINK = {match_reward, loot_roll, purchase_fulfil, sink_craft, sink_repair, refund_reversal, operator_adjustment} deliberately create or destroy value. Every other txn_id must have legs summing to zero and must use a single currency_code. The partition holds 2.7 million legs. Return each offending txn_id with its residual, in one pass, and state your memory bound.
Approach
- One hash map keyed by txn_id holding (running sum, first currency_code seen, mint_sink flag, leg count). Single pass, O(n) time, O(T) space for T distinct txn_ids.
- Set the exempt flag the moment any leg of a txn carries a MINT_SINK reason, but keep accumulating the sum anyway so the residual is still reportable when an operator asks why a mint was that size.
- Do not evict a txn when its running sum reaches zero. Legs are unordered, so a later leg can unbalance a txn that momentarily netted out; eviction is only safe at end of partition, or behind a watermark plus a known expected leg count.
- Memory: 2.7 million txn_ids at roughly 16 B of UUID plus 8 B of sum plus 8 B of flags is about 86 MB before hash overhead. If that is too much, hash the UUID down to a 64-bit key and accept collision risk, or run k range-partitioned passes at k times the I/O.
- Report two distinct defect classes, not one: residual != 0 for a non-exempt txn, and more than one currency_code under a single txn_id. The second can occur with residual exactly 0 and is invisible to a pure sum check.
Follow-up
- Run the same audit continuously over an unbounded stream with no partition boundary. What bounds the map now?
- Two legs of one txn land in different daily partitions because the writer straddled midnight. What does your job report, and what do you change so it stops lying?
- How do you tell a genuinely unbalanced txn from one whose compensating leg was written and later reversed by a new row?
Soft-delete an item stack and keep one live row
Easy · soft delete · partial index · audit history
item_instance(instance_id UUID PK, owner_account_id BIGINT, item_def_id INT, item_def_version INT, quantity INT CHECK (quantity >= 0), bind_state TEXT, escrow_id UUID NULL, origin_grant_id UUID NULL, acquired_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, consumed_at TIMESTAMPTZ NULL, row_version BIGINT). Rows are never hard-deleted: consuming a stack decrements quantity and sets consumed_at at zero, so the transfer chain stays replayable. Product requires at most one live stack per (owner_account_id, item_def_id, bind_state), with acquisitions merging into the existing stack. Write the constraint that enforces this without blocking re-acquisition after a consumption, plus the session-start inventory query over 10^2-10^4 stacks and the index that serves it.
Approach
- Show why the obvious
UNIQUE (owner_account_id, item_def_id, bind_state)is wrong: consumed rows stay in the table forever, so the first time a player finishes a stack they are permanently blocked from ever acquiring that item again. - Use a partial unique index —
CREATE UNIQUE INDEX uq_live_stack ON item_instance (owner_account_id, item_def_id, bind_state) WHERE consumed_at IS NULL;— so consumed rows fall out of the index entirely while staying in the table. - Say what you do on an engine without partial indexes: InnoDB has none, so the equivalent is a nullable generated column that is NULL once
consumed_atis set, because a unique index treats NULLs as distinct and they never collide. - Write the read as
WHERE owner_account_id = :a AND consumed_at IS NULL AND escrow_id IS NULL ORDER BY acquired_at, backed by(owner_account_id, acquired_at) WHERE consumed_at IS NULLso the ordering comes from the index and no Sort node appears. - Distinguish the two filters: escrowed stacks are hidden from the playable inventory but are still owned, so a supply audit or a support investigation must include them rather than reuse this predicate.
- Point at
row_versionas the optimistic token for equip and consume, notupdated_at:now()is transaction-start time, so two updates in one transaction share a timestamp and timestamp equality is not a safe concurrency check.
Follow-up
- A stack is escrowed and the listing expires unclaimed. Which row releases the lock, and what stops two sweep workers releasing it twice?
- Support needs the full transfer chain for one instance. What does this model let them reconstruct, and what does it still lose?
Detect ledger drift with a window over consecutive entries
Medium · window functions · reconciliation · deterministic ordering
currency_ledger(entry_id BIGSERIAL PK, txn_id UUID, wallet_id BIGINT, currency_code TEXT, delta_minor BIGINT, balance_after_minor BIGINT, reason TEXT, created_at TIMESTAMPTZ) records both the signed delta and the balance the writer computed after applying it. Every write to one wallet is serialized by a row lock on that wallet. The table takes roughly 10^9 rows a year and is partitioned by month. Write a query that finds entries where balance_after_minor disagrees with the deltas while scanning only yesterday's rows rather than replaying all history, and say what the first entry per wallet inside that window does to your check.
Approach
- Reject the cumulative form for a bounded scan:
SUM(delta_minor) OVER (PARTITION BY wallet_id ORDER BY entry_id)restarts at zero at the window's first row, so every row in yesterday's slice disagrees withbalance_after_minorby that wallet's opening balance. It is only correct over all history, which at 10^9 rows a year is not a scheduled job. - Use the consecutive-difference form instead:
balance_after_minor - LAG(balance_after_minor) OVER (PARTITION BY wallet_id ORDER BY entry_id)must equaldelta_minor. It only ever compares neighbours, so it is correct on any contiguous slice. - Handle the boundary row explicitly. LAG is NULL for the first entry of each wallet in the window, the comparison evaluates to NULL, and the row silently passes — either exclude it with
ROW_NUMBER() = 1or seed the window with the last entry before the partition boundary so the join to prior history is exact. - Make the ordering total, and do not reach for the frame clause to do it:
LAGignores the window frame entirely in PostgreSQL — onlyfirst_value,last_valueandnth_valueare frame-sensitive — so addingROWS BETWEEN 1 PRECEDING AND CURRENT ROWchanges nothing. Ordering by a non-uniquecreated_atbreaks the check because the order among tied rows is arbitrary:LAGreturns whichever peer the sort happened to place before the current row, which need not be the entry that applied before it, so correct rows are reported as drift and the same query can disagree with itself across plans. Order byentry_id, or by(created_at, entry_id)if you want time order. Peer inclusion under the defaultRANGEframe is a real hazard for the cumulative-SUMform above, because window aggregates do respect the frame — it just has no bearing onLAG. - Name the precondition that makes
entry_idorder meaningful: writes to a single wallet serialize on that wallet's row lock, so per-wallet id order matches apply order. Globalentry_idorder does not match global commit order and the sequence is not gapless, but neither matters because the window is partitioned by wallet. - Cost it: the window needs input ordered by
(wallet_id, entry_id), so a btree on those columns lets the scan feed the window directly and skip an O(n log n) sort. Bound the scan to one partition and one currency.
Worked solution (30 min)
- Write both the cumulative-sum and the LAG version against a single day's partition.
- Corrupt exactly one row's
balance_after_minorby a known amount. - Run both queries and count the rows each returns.
- Give one wallet two entries with identical
created_at, switch the ORDER BY tocreated_atalone, and addlag(entry_id) OVER (same window)to the select list to see which neighbour each row was actually given; toggleenable_indexscanandenable_seqscanto change the plan and watch that neighbour, and the drift verdict, move while the data does not. EXPLAINthe LAG version with and without a(wallet_id, entry_id)index and compare for the presence of a Sort node.
Expected result: The LAG query returns exactly the injected row; every other row satisfies balance_after_minor - LAG(balance_after_minor) = delta_minor. The cumulative-sum form returns every row in the window, because the running total starts at zero rather than at each wallet's opening balance.
Verify it yourself
- One injected corruption produces exactly one returned row, not a wallet's worth.
- The first entry per wallet in the window is excluded explicitly rather than passing on a NULL comparison.
EXPLAINshows the window fed by an index scan with no Sort above it.- Under
ORDER BY created_atalone,lag(entry_id)on a tied pair returns something other than the immediately precedingentry_idand those rows are reported as drift; addingentry_idto the ORDER BY removes them, while adding an explicitROWSframe does not.
Follow-up
- The check fires on 40 wallets. Is the ledger wrong or the maintained balance wrong, and what distinguishes them?
- Make this cheap enough to run every five minutes instead of daily. What do you give up?
- A row is inserted into yesterday's partition after the check ran. How do you notice?
How would you structure a modular AI behavior tree system?
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
Explain how you would design a logging and monitoring system for a distributed IT infrastructure.
Medium · system design and architecture
Approach
- 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.
- 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?
Design a system for managing thousands of concurrent network entities.
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Explain the difference between stack and heap memory allocation in C++.
Medium
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Explain the role of data-oriented design in modern game development.
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Design the wallet spend and balance read path
Easy · ledger · concurrency · idempotency
Soft currency lives in an append-only currency_ledger (entry_id, txn_id, wallet_id, currency_code, delta_minor, balance_after_minor, reason, idempotency_key UNIQUE) alongside a maintained wallet.balance_minor. Store pages read balances about 8,000 times per second; spends peak near 2,000 per second, and one account can have two clients open. A balance must never go negative and a retried spend must not debit twice. On PostgreSQL at READ COMMITTED, give the read path, the exact spend statement, and what the caller does with the affected-row count.
Approach
- Split the read from the history: serve balances as an O(1) point read of wallet.balance_minor and keep the ledger for audit. At roughly 10^9 legs a year, SUM over history is a seconds-scale scan even time-partitioned, so it belongs in a periodic reconciliation job, not on the store page.
- Make the debit one statement: UPDATE wallet SET balance_minor = balance_minor - :n WHERE wallet_id = :w AND currency_code = :c AND balance_minor >= :n. Under READ COMMITTED a concurrent writer forces this UPDATE to block on the row lock and then re-evaluate the predicate against the newly committed row version, so the check cannot be read from a stale snapshot.
- Branch on the affected-row count, not on an exception: 0 rows means insufficient funds and is a 409 the client must not blindly retry; 1 row means the debit applied.
- Insert the ledger leg in the same transaction with idempotency_key derived from server-authoritative state. The UNIQUE index turns a retry into a unique-violation the handler maps to 'already applied, return the original result' rather than a second debit.
- State what changes at stricter isolation: under REPEATABLE READ or SERIALIZABLE the second writer aborts with SQLSTATE 40001 instead of re-evaluating, so the caller owes a bounded retry with jitter and a retry budget.
- Name the trade-off you accepted: one hot row per wallet serializes that wallet's spends. That is correct rather than unfortunate, because the invariant is per-wallet; sharding the balance row would buy throughput nobody needs and lose the guarantee.
Worked solution (20 min)
- Write both spend forms in SQL and draw the two-transaction interleaving that lets the naive version debit 120 from a balance of 100.
- Estimate the cost of reconstructing one balance by SUM over a year of legs, and state where the maintained aggregate is refreshed.
- Write the ledger insert with an idempotency key built only from server-side values, and say what the handler returns on unique violation.
- Define the reconciliation query and its cadence, including what it compares and what it alerts on.
- Assign HTTP status codes to insufficient funds, duplicate request and serialization failure.
Expected result: A single conditional UPDATE guarded by balance_minor >= :n, an affected-row check where 0 means insufficient funds, a same-transaction ledger leg whose UNIQUE idempotency_key absorbs retries, balance reads served from the maintained aggregate, and an explicit retry policy for 40001 at stricter isolation levels.
Verify it yourself
- Two concurrent spends of 60 against a balance of 100: exactly one commits, the other sees 0 affected rows.
- Replaying the same spend request five times produces exactly one ledger leg and one balance change.
- After a fault-injected crash mid-request, SUM(delta_minor) per wallet still equals wallet.balance_minor.
- No code path anywhere computes a balance with a floating-point type.
Follow-up
- Reconciliation finds a wallet whose ledger sum and cached balance disagree by 40 minor units. How do you repair it without freezing the wallet?
- A cart spends two currencies and must be all-or-nothing. What changes, and what if the wallets live on different database hosts?
Listings expire an hour early twice a year in one region
Medium · timezone · dst · timestamptz
market_listing.expires_at is TIMESTAMPTZ and listings are created with a 48-hour duration. Twice a year, players in one region report listings expiring about an hour early; on one of those two days a batch expired an hour late instead. A nightly sweep cancels listings where expires_at < now(). The creation path formats the creation time into the region's local zone, adds 48 hours as a local wall-clock offset, then parses the result back. Give the ordered checklist, the root cause, and the fix.
Approach
- Date the reports before opening any code. Two incidents a year in one region with opposite signs is the daylight-saving transition signature. Clock skew would be small, continuous and not region-specific, so NTP is already ruled out by the shape of the data.
- Be precise about what TIMESTAMPTZ does: PostgreSQL stores an absolute instant, normalised to UTC, and converts on input and output using the session TimeZone. It does not store a zone. The column is therefore not the problem — the corruption happens in application code that round-trips through a local wall-clock representation.
- Name the arithmetic error exactly. Adding 48 hours to a local wall-clock time and converting back is calendar arithmetic across a DST boundary, and it lands on a different instant than adding a 48-hour duration to an instant. Spring forward makes the elapsed real time 47 hours, so the listing expires an hour early; fall back makes it 49 hours, so it expires an hour late. That is the two-sided symptom.
- Check the second, smaller defect in the sweep: PostgreSQL's now() returns the transaction start time, not the current instant, so a long-running sweep transaction compares every row against a timestamp that is already stale by however long it has been open. Use clock_timestamp() if the sweep needs the real current time mid-transaction, and bound the batch size either way.
- Fix the main bug by computing expires_at = created_at + interval '48 hours' as instant arithmetic on the timestamptz, and keep zone conversion strictly at the presentation edge.
- Pin it with a regression test frozen to a fixed instant on both a spring-forward and a fall-back date in an affected zone. The bug is unreproducible on the other 363 days, so without a frozen-clock test it comes straight back.
Follow-up
- When is calendar arithmetic the correct choice instead — for example a daily reset that must always land at local 04:00 regardless of DST?
- How do you test this deterministically without waiting for a transition or mutating the machine clock?
Suggested practice checklist
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.
'We rewrote it in a better framework' is not a story
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
Tell me about a time you had to mentor a junior engineer or explain a complex technical concept to a non-technical stakeholder.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
How do you handle disagreements with designers regarding technical feasibility?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Describe a time you had to pivot your technical approach due to shifting project requirements.
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?