cswork / libraryOpen practice workspace

Interview guides · Ironbelly Studios

Ironbelly Studios Software Engineer Interview Questions & Guide 2026

Take-home style practice for Ironbelly Studios software engineer candidates: a small service, a fixed budget, a written tradeoff.

The 60-second brief

A Software Engineer at Ironbelly Studios plays a foundational role in bringing complex interactive experiences to life. Whether you are working on high-fidelity gameplay mechanics for a Multiplayer FPS or architecting systems within Unreal Engine, your contributions affect the fluidity, performance, and player immersion of Ironbelly Studios' products. This role is not just about writing code; it is about solving technical challenges that demand both creative engineering and a deep understanding of game architecture.

Ask whether any round happens inside an existing repository instead of a blank file. Reading unfamiliar code, isolating a fault and making the smallest correct change is a different skill from writing a function from scratch, and it needs its own practice.

Focus areas

  • Derive idempotency keys from server-authoritative state
  • Audit item supply from append-only ledgers
  • Prevent negative balances with conditional single-statement debits

Overview

You will operate in a fast-paced environment where technical precision is paramount. The work often involves pushing the boundaries of what is possible within C++ and the Unreal Engine framework. Candidates who thrive here are those who view code as a craft and are eager to contribute to projects that require scalability and high-performance standards. You will be expected to balance rapid iteration with the need for clean, maintainable, and well-documented systems that your teammates can rely on.

The Ironbelly Studios interview process

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

Initial Screening

The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.

What to demonstrate

  • Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
  • Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
  • Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year

How to prepare

  • Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
  • Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
  • Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter

Hands-on Technical Demonstration

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

Final Technical Review

Nobody in the room with you decides this. Interviewers typically write their rounds up separately, often before seeing anyone else's, and the outcome is settled later from those write-ups. A split panel gets resolved by whichever note carries specific evidence, so what you want out of each room is one concrete thing that person could write down: a bug you caught yourself, a trade-off you named, a decision you owned. The rest is arithmetic. The project you describe in a behavioural conversation is often the same system you sketched an hour earlier, and the two accounts have to agree.

What to demonstrate

  • Whether the scale, team size and timeline you attach to a project hold steady when that project resurfaces in a different round
  • Whether each interviewer leaves with a specific thing to cite rather than a general impression of competence
  • Whether a trade-off you defended in one round survives a challenge in another, instead of being quietly swapped for the answer the new interviewer seemed to want
  • Whether a question you have already answered earlier in the day gets the same answer at the same depth, without visible impatience

How to prepare

  • Write a one-page sheet per project fixing the figures you will quote — request volume, data size, team size, elapsed time, what broke — and say them aloud from the sheet until they come out identical every time
  • For each round on the schedule, decide in advance the one sentence you want in that person's notes, then check in a mock that you said it outright instead of leaving it to be inferred
  • Have someone ask you the same project question twice, an hour apart, and diff the two answers for numbers that moved or a trade-off that reversed

Where candidates lose points

Treating the message queue as exactly-once, so the reward consumer applies a grant without a dedupe check.

A broker that redelivers on consumer failure is at-least-once by construction, and a consumer that crashes between writing the grant and acknowledging the message will see that message again. Exactly-once end-to-end would need a transaction spanning the broker and the database, which does not exist across heterogeneous systems, so the sink has to be idempotent. The failure is invisible in steady state and shows up as a burst of duplicate grants exactly when a deploy or an OOM restarts consumers.

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.

Sharing mutable state with no stated owner

Say which thread, request or task owns each mutable structure, and what protects it when the answer is more than one: a lock, a queue that hands ownership across, or an immutable copy per reader. A structure documented as safe for concurrent reads is usually not safe for a concurrent write alongside those reads.

Issuing one query per row of a result set

Fetch related rows in a single batched query keyed by the ids you already hold, or join them into the original query. A per-row round trip multiplies network latency by the row count, and it looks perfectly fine against the ten rows in your development database.

Questions & practice

Budget per-tick visibility queries under a 16.7 ms tick

Hard · spatial index · tick budget · bandwidth

A match instance simulates 100 entities at 60 Hz in a 1000 m by 1000 m arena. Every tick, each entity needs the set of others within 60 m so the snapshot serialiser can skip the rest. Entities move at up to 8 m/s. The whole tick, meaning inputs, physics, visibility and serialisation, must fit in 16.7 ms, and one host runs many instances. Give the naive cost, a structure that beats it, its worst case on this exact workload, and the downstream bandwidth per client at 30 Hz with 40-byte per-entity deltas.

Approach

  1. Naive all-pairs is C(100,2) = 4,950 distance tests per tick, 297,000 per second per instance, and 5.9 x 10^8 per second across a 2,000-instance fleet at 200k concurrent players. The argument is the scaling, not the absolute number: at O(P^2) per tick, doubling the roster quadruples the work.
  2. Uniform grid with cell edge equal to the query radius, 60 m: ceil(1000/60) = 17 cells per side, 289 cells, mean occupancy 100/289 = 0.35. Cell edge at least the radius means the 3x3 neighbourhood provably covers every candidate, since a point within 60 m differs by at most one cell index on each axis. That is about 3.1 candidates per query and roughly 310 distance tests per tick, 16 times fewer. Rebuild clear-and-reinsert each tick at O(P); at P = 100 and 0.13 m of movement per tick that is cheaper than incremental updates and immune to stale-cell bugs.
  3. State the worst case on this workload rather than quoting the average as the budget. All 100 entities inside one cell, which is exactly what a contested objective looks like, makes the 9-cell scan return all 100 for every query: 10,000 tests, about twice the naive triangular count, because the grid neither halves symmetric pairs nor prunes anything.
  4. Choose by density, not by fashion. A uniform grid wins on roughly even spread with one fixed radius; a k-d tree or BVH costs O(P log P) to build and only pays off at much larger P or with widely varying query radii. Halve the work in either case by visiting each pair once, scanning only cells at or after the current cell in traversal order, and by comparing squared distances so no test carries a sqrt.
  5. Bandwidth: 30 visible entities at 40 B is 1,200 B per snapshot; at 30 Hz that is 36 KB/s, or 288 kbit/s downstream per client, plus about 28 B of UDP and IPv4 header per packet for another 840 B/s. At 60 Hz it doubles to 576 kbit/s, which is the concrete argument for running the snapshot rate below the simulation rate.

Follow-up

  • Entities converge on one objective and the grid degrades to its worst case. What do you change first: cell size, a cap on the visible set, or the snapshot rate, and what does each cost the player?
  • How does the visibility set interact with lag compensation, where the server rewinds to the shooter's view time to resolve a hit?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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)

  1. 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.
  2. Seed the heap with the four heads (118, 120, 125, 500) and record the read count as 4.
  3. Pop five times, advancing only the popped shard and incrementing the read count on each advance that returns a row.
  4. 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?

Parse decimal price strings into integer minor units safely

Easy · parsing · money · overflow

Regional price books arrive as text. Write parse_minor(text, exponent, decimal_sep, group_sep) returning integer minor units for purchase_order.price_minor BIGINT. exponent is the currency's minor-unit digit count and is 0, 2 or 3 depending on the currency. Input may carry a leading sign, grouping separators, and fewer fraction digits than exponent. Reject rather than round any value carrying more fraction digits than exponent, and reject anything outside signed 64-bit range. No binary floating point may appear anywhere in the solution or its tests.

Approach

  1. Single character scan classifying sign, digit, group_sep and decimal_sep, rejecting anything else including a second sign or a second decimal separator. O(len) time, O(1) extra space.
  2. Accumulate the integer part into an integer accumulator, and before each acc = acc*10 + d check that acc <= (INT64_MAX - d) / 10. Detect overflow before it happens: signed overflow after the fact is undefined behaviour in C and C++ and wraps silently in most compiled languages, so a post-hoc check on the result is not a check.
  3. Count fraction digits as you read them. More than exponent is a reject. Fewer than exponent means multiply by 10^(exponent - count), using the same pre-multiply bound check.
  4. Combine as total = int_part * 10^exponent + padded_fraction and apply the sign last. At exponent 0 the presence of a decimal separator at all is a reject, not a truncation, because a currency with no minor unit has no place to put those digits.
  5. Keep locale out of the parser by taking decimal_sep and group_sep as arguments. The same glyph is a decimal separator in one locale and a thousands separator in another, so a parser that guesses turns one written amount into either 1500 or 1.5 depending on who typed it.

Follow-up

  • Now render the stored minor units back for display in the player's locale. What breaks at exponent 0 and at exponent 3?
  • The upstream store returns an exponent that disagrees with your price book for the same currency code. Which one wins, what do you store, and what do you log?

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

  1. 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.
  2. 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.
  3. 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_at is set, because a unique index treats NULLs as distinct and they never collide.
  4. 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 NULL so the ordering comes from the index and no Sort node appears.
  5. 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.
  6. Point at row_version as the optimistic token for equip and consume, not updated_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?

Count minted supply against a cap from grant history

Hard · append-only modelling · global cap · sharding · hot row contention

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

Worked solution (35 min)

  1. Construct one definition with a mint of 10, a split into 6 and 4, a merge back, and a consumption of 3, then record how SUM(quantity) and a row count each move.
  2. Add grant_line and write the audit query over applied grants.
  3. Pick a reversal convention, write the netting for it, and reverse one grant to check the figure moves by exactly the reversed quantity.
  4. Write the conditional def_supply update and exercise it at the cap boundary and one unit past it.
  5. Compare def_supply.minted_total against the audit query and confirm they agree.

Expected result: SELECT SUM(gl.quantity) FROM grant_line gl JOIN grant_receipt g USING (grant_id) WHERE gl.item_def_id = :d AND g.status = 'applied', netted for reversals under the stated convention, equal to def_supply.minted_total and unchanged by the splits, merges and consumptions that move SUM(item_instance.quantity).

Verify it yourself

  • Splitting a stack of 10 into 6 and 4 changes the row count while both the audit figure and SUM(quantity) stay at 10 — so any row-counting answer is already wrong at this step.
  • Consuming 3 units drops SUM(quantity) to 7 and leaves the audit figure at 10.
  • The mint that lands exactly on cap succeeds and the next one reports zero affected rows.
  • A reversed grant moves the audit figure by exactly the reversed quantity, once, under the convention you stated.

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?

Can you describe your proficiency level with C++ in a game development context?

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Work from the requirement backwards to the design.
  3. Say what you would check first and why it is the highest-information step.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

What are the most complex systems you have implemented in Unreal Engine to date?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. State your assumptions explicitly before working the problem.
  3. Clarify what is being asked and what a complete answer contains.

Follow-up

  • What assumption would you test first?
  • How would you know your answer was wrong?

How many years of professional experience do you have programming in Unreal Engine?

Medium

Approach

  1. Say what you would check first and why it is the highest-information step.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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)

  1. Write both spend forms in SQL and draw the two-transaction interleaving that lets the naive version debit 120 from a balance of 100.
  2. Estimate the cost of reconstructing one balance by SUM over a year of legs, and state where the maintained aggregate is refreshed.
  3. Write the ledger insert with an idempotency key built only from server-side values, and say what the handler returns on unique violation.
  4. Define the reconciliation query and its cadence, including what it compares and what it alerts on.
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.

Day 1 — Coding, one pass at shallow depth

  • Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
  • For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
  • Fix nothing today. The value of the pass is the unfixed record.

Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.

Day 2 — Design, one pass at shallow depth

  • Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
  • After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
  • Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.

Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.

Day 3 — Fundamentals and the practical rounds

  • Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
  • Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
  • Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.

Deliverable: Eight scored short answers and one written reading of unfamiliar code.

Day 4 — The rounds that are about you, and the map

  • Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
  • Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
  • Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.

Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.

Day 5 — First chosen area, to the depth you skipped

  • Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
  • After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
  • Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.

Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.

Day 6 — Second chosen area, where the gap is coverage rather than speed

  • Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
  • Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
  • Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.

Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.

Day 7 — Reassemble the loop

  • Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
  • Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
  • Reduce the week to one page holding only the rules you can state without reading them.

Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.

The bug you decided not to fix

Engineers over-index on what they repaired. A stronger answer covers something you knowingly left broken: the alert you tuned down, the data inconsistency you documented instead of chasing, the cleanup you deferred past two quarters. Give the reasoning and the condition that would have reopened it, so it reads as a decision and not as neglect.

What is your experience with source control and collaborative development workflows?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Name the disagreement and how you resolved it with evidence.
  3. State the situation in two sentences and spend the rest on the reasoning.

Follow-up

  • How did you know your change caused the improvement?
  • What did you decide not to do, and why?

Ship to a fixed date with debt you can name

Medium · deadlines · technical debt · risk

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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?

Reverse a decision after the data contradicted you

Hard · reversal · migration · evidence

Prepare a six-minute account of a technical decision you made and later reversed, where the reversal cost something real: shipped code, migrated data, or a commitment another team had already planned around. Constraint: the trigger must be an observation rather than a change of mood, and you must state what you had predicted. Deliverable: the original decision with its stated assumption, the measurement that broke it, the cost of unwinding and who paid it, and how you sequenced the reversal so the system stayed correct while both paths were live.

Approach

  1. State the original decision with its assumption attached, because a reversal is only interesting when the assumption was written down. 'We denormalised the definition id onto the listing row so the cheapest-N query needs no join, assuming reads dominate and the catalog is stable' is reversible reasoning; 'we chose the denormalised schema' is not.
  2. Name the observation that falsified it and the lag between the first contradicting data and your acting on it. That lag is the honest part of the story, and volunteering it rather than being asked is most of what distinguishes the answer.
  3. Sequence the unwind as a migration, not a revert. Reverting code restores behaviour; it does not un-write rows. Describe the period where both paths were live with one authoritative, the comparison job that measured the difference between them, and the criterion that finally permitted the old path to be retired.
  4. Quantify what it cost and who paid: engineer-weeks, a slipped commitment, a backfill over N rows with its runtime and its throttle so it did not starve the write path. Reversals are expensive, and pretending otherwise reads as never having done one.
  5. Close with the property that made the reversal cheap, or the missing one that made it expensive: a flag around the read path, an append-only table you could replay, a soft delete instead of a hard one.

Follow-up

  • What would you have instrumented on day one to learn this sooner?
  • How did you decide the old path was safe to delete, and what did you keep a copy of and for how long?
  • Did anyone argue against the reversal, and what was their strongest point?