cswork / libraryOpen practice workspace

Interview guides · Empirical Foods

Empirical Foods Software Engineer Interview Questions & Guide 2026

Software engineer prep for Empirical Foods: design exercises weighted toward the constraints of ecommerce and retail work.

The 60-second brief

At Empirical Foods, the Software Engineer—often titled Industrial Water Resource Engineer—is a critical technical role focused on the intersection of advanced manufacturing processes and resource management. You will be responsible for designing, maintaining, and optimizing the complex systems that support the company's large-scale food production operations. Your work directly affects the company's ability to maintain efficiency, adhere to rigorous safety standards, and manage critical industrial resources at its facilities.

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

  • Design order state machines with explicit compensating transactions
  • Derive available-to-promise across nodes and open reservations
  • Reconcile payment state after timeouts instead of retrying

Overview

This position is ideal for engineers who thrive in hands-on environments where software solutions have immediate, physical-world consequences. You will contribute to projects that require a deep understanding of infrastructure, data-driven optimization, and high-stakes operational reliability. Expect a role that demands technical precision and a strong sense of ownership over the systems that keep Empirical Foods' production lines running effectively.

While the role title is Industrial Water Resource Engineer, the core competencies required are those of a Software Engineer with a focus on industrial automation and resource management systems.

The Empirical Foods interview process

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

Initial Phone Screen

The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.

What to demonstrate

  • Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
  • Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
  • Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural

How to prepare

  • Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
  • Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
  • Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one

HR Interview

When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.

What to demonstrate

  • Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
  • Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
  • Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
  • Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened

How to prepare

  • Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
  • Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
  • Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure

Hiring Manager Interview

Expect the estimate to be probed harder than the outcome. A manager asking how long something will take is checking whether your number covers the parts that reliably go wrong: migrating the rows that already exist, and the second service that has to change at the same time. A single confident number is the weak answer. The strong one states what the estimate assumes, names the unknown that could double it, and says what you would cut first if the date were fixed. Scope is the variable you control; hours are not.

What to demonstrate

  • Whether an estimate arrives with its assumptions attached, including what it excludes and how much of it is waiting on review, environments or another team
  • Whether you can name work you cut and the person you told, since a cut nobody heard about surfaces later as a surprise rather than a decision
  • How you order two tasks that both look urgent, and whether that order survives being told the deadline moved in by a week
  • Whether you separate what must be done before launch from what must be done before the thing can be left running unattended

How to prepare

  • Take the last three things you shipped and write down, from memory, the estimate you gave and the time it actually took. Bring the largest miss and why it missed; a candidate who has never missed has never committed to a date.
  • Against your current queue, write who is blocked until each item lands. Anything with nobody behind it becomes your worked example of what you would drop and how you would say so.
  • Rehearse the fixed-date answer aloud: which slice ships, which slice is deferred and written down somewhere it will be found, and the one thing you would refuse to cut because removing it makes the release unsafe rather than smaller.

Where candidates lose points

Treating a payment timeout as a failure and retrying with a fresh idempotency key

An HTTP timeout carries no information about whether the provider processed the request; the call may have succeeded and the response been lost in either direction. Retrying with a new key creates a second authorization or a second capture, and the customer sees two pending amounts on their statement — which is also the version of this bug that reaches support rather than a dashboard. The correct shape is to persist the intent with its key before the call, retry with the same key, and if the operation is one the provider does not make idempotent, resolve it by querying for your own reference before issuing anything else. The structural requirement is that the outcome type has three values, not two: a boolean return type makes the bug unavoidable no matter how careful the caller is.

Representing money as floating point, or rounding tax at a different grain than the total is computed at

Binary floating point cannot represent most decimal fractions exactly, so sums over thousands of lines drift and two services computing the same total from the same inputs disagree in the last place. Rounding grain is the subtler half: rounding tax per line and summing gives a different total than computing tax on the order subtotal and rounding once, and the difference is a cent or two per order — small enough to pass every test and large enough to make the payment provider's amount, the invoice and the finance ledger disagree at month end. Store minor units as integers, and treat the rounding rule as a specification that is applied at exactly one point, propagated everywhere else, and pinned by a CHECK constraint on the order total; which rule is required is jurisdiction-specific, so the engineering obligation is consistency and explicitness rather than a single universal choice.

Treating a network call as though it were a local function call

A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.

Tests that assert on the implementation rather than the behaviour

Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.

Questions & practice

Find peak concurrently held units per variant with a sweep

Medium · sweep line · intervals · reservations

You have up to 50 million historical reservation records as (variant_id, node_id, units, start_utc, end_utc), where end_utc is the instant the hold stopped counting — its expiry, release or commit — and the interval is half-open. For each (variant_id, node_id) return the maximum units held simultaneously and the earliest instant that maximum was reached, then flag every key whose peak exceeded a supplied cap. Records are unsorted. State your complexity and your tie rule at equal timestamps.

Approach

  1. Expand each record into two endpoint events, (start_utc, +units) and (end_utc, -units), group by (variant_id, node_id), and sort each group by (timestamp, delta_sign) with negative deltas first. That ordering is the entire correctness argument for half-open intervals: a hold ending exactly when another begins must not appear to overlap, and putting starts first invents a peak that never existed.
  2. Sweep each group with a running sum, tracking the maximum and the timestamp at which it was first attained. The peak holds over [t, next_event_time), so report the window rather than the instant — an alert naming a moment is far harder to corroborate against the ledger than one naming an interval.
  3. Complexity: O(n log n) dominated by the sort, O(n) space, or O(n log g) when sorting within groups of size g. Grouping is not a micro-optimisation: a single global sort interleaves 50 million events from two million keys, and the running sum is meaningless unless it resets at every group boundary anyway.
  4. Offer the bucket alternative with its condition attached. If timestamps are second-granularity over a bounded window, a difference array over seconds makes each group O(n_g + W) for a window of W seconds. That wins for a hot variant with millions of holds and loses badly across a long tail of variants with three holds each, so choose per group by comparing n_g log n_g against n_g + W.
  5. Compare each peak against the cap on_hand - damaged + oversell_allowance as it stood at that time, not as it stands now. Judging last Tuesday's peak against today's on-hand produces false alarms on restocked variants and misses real breaches on depleted ones.

Follow-up

  • end_utc is NULL for holds still live at the end of the window. What do you substitute, and what does that choice do to the peak?
  • The same reservation appears twice because the export is at-least-once. Where do you dedupe, and on what key?
  • You now need the peak per hour rather than per window. What changes, and what stays the same?

Compute peak attempt rate per variant over a sliding window

Medium · sliding window · two pointers · event stream

Given one day of reservation attempts as (timestamp_ms, variant_id) — up to 300 million events across two million variants, arriving in timestamp order — compute for each variant the maximum number of attempts falling inside any 60-second sliding window, plus the start of the earliest window achieving it. The window is continuous, not a tumbling bucket. Give an algorithm linear in the number of events after grouping, with bounded memory per variant, and say precisely what that memory is bounded by.

Approach

  1. Group by variant_id, then run two pointers over each group's timestamps: advance the right pointer one event at a time and advance the left while t[right] - t[left] >= 60000, with the count being right - left + 1. Neither pointer moves backwards, so each group costs O(n_v) and the whole job is O(n) after grouping — the partition or sort is the only superlinear step in the pipeline.
  2. Justify examining only n windows: the maximum can always be attained by a window whose left edge sits on an event, because sliding a window without crossing an event cannot change its count. That is the step candidates skip and then cannot defend when asked why n windows suffice over continuous time.
  3. For the streaming form, hold a deque of timestamps per active variant and pop from the front while the front is older than 60 seconds. Memory per variant is bounded by the peak window count rather than by the day's event count — a variant at 1,000 attempts per second holds at most 60,000 timestamps, about 480 KB at 8 bytes each, and that is the number to quote when asked whether the job fits.
  4. Say why tumbling buckets are not a substitute, with the concrete case: 100 attempts at t = 59.9s and 100 at t = 60.1s give a tumbling maximum of 100 against a true sliding maximum of 200. The understatement can reach a factor of two, which is the difference between tripping an admission-control threshold and sailing past it.
  5. Complexity: O(n) after grouping, O(peak window count) space per active variant. Evict variants with no event in the last 60 seconds so the live map stays proportional to concurrently active variants rather than to the two-million-variant catalogue.

Worked solution (20 min)

  1. Construct one variant's timestamps by hand with a burst deliberately straddling a bucket boundary, and compute both the tumbling and sliding answers on paper.
  2. Implement the two-pointer sweep and confirm it reproduces the paper sliding figure.
  3. Implement the tumbling version, confirm it reports the smaller number, and keep that gap as a regression test.
  4. Run over 10 million events across 100,000 variants, then halve the input and compare wall times to confirm linearity.

Expected result: The sliding maximum is 200 for the planted straddling burst where tumbling reports 100. The reported window start is the timestamp of a real event, and the window is half-open over 60,000 ms.

Verify it yourself

  • Recounting events with timestamps in [start, start + 60000) directly reproduces the reported count.
  • No variant's sliding maximum is below its tumbling maximum.
  • A variant with fewer events than one window's worth reports its total count as the maximum.

Follow-up

  • Events can arrive up to five seconds late. What do you buffer, what do you emit, and what happens to an event that is twenty seconds late?
  • You need peaks over both 60-second and 5-minute windows. Do you pay twice?
  • This result feeds a token bucket that throttles the variant. Describe the feedback loop you have just created.

Roll up facet counts over a category graph without double counting

Medium · dag · topological sort · distinct counting · sketches

Categories form a directed acyclic graph of up to 100,000 nodes and 300,000 child-to-parent edges, and a category may have several parents. Two million active variants each sit in exactly one category. For every category, return the number of distinct active variants in it or any descendant, and refuse to produce counts at all if a supplier feed has introduced a cycle. The memory budget is one gigabyte. State your complexity and where exactness is lost, if it is.

Approach

  1. Run Kahn's algorithm first: repeatedly remove zero-in-degree nodes, and if any node remains the graph contains a cycle. It costs O(V+E) and hands you the topological order the rollup needs anyway. Report the residual node set so the feed owner sees which edges close the cycle, rather than an assertion that the feed is bad.
  2. Show why addition is wrong here. On a tree, counts accumulate exactly in reverse topological order. On a DAG, a category reachable from an ancestor by two paths contributes twice, so a straight sum overstates every node above a diamond — and the overstatement is largest at the high-traffic parent categories, which is where a wrong number is most visible.
  3. Exact distinct counting needs set union. A bitset per node is 2,000,000 bits, or 250 KB, and 100,000 nodes is 25 GB — twenty-five times the budget. Compute that number and abandon the approach explicitly instead of hand-waving past it.
  4. Use HyperLogLog and merge in reverse topological order. Union is lossless because it is the register-wise maximum, which is exactly the property that makes it safe on a DAG where one variant arrives by two paths. At m = 4,096 registers the relative standard error is 1.04/sqrt(m), about 1.6%, at roughly 4 KB per sketch — about 400 MB for 100,000 nodes, inside budget. At m = 1,024 it is 3.25% error and about 100 MB.
  5. Complexity: O(V + E) traversal with one sketch merge per edge, O(V*m) space. Then state the exactness policy rather than leaving it implicit: keep exact sets below a cardinality threshold and switch to sketches above it, because the pages where an off-by-a-few count is noticeable are the small ones.

Follow-up

  • A facet count and the filtered result count differ by 1.4%. What do you show the customer, and which of the two numbers do you fix?
  • One variant moves between categories. What must be recomputed, and can it be done incrementally?
  • Counts must now exclude variants with zero ATP at every node. Where does that predicate live, and what does it do to your refresh cadence?

Explain why the reservation expiry sweeper stopped using its index

Medium · indexing · sargability · queue tables

inventory_reservation has 400 million rows and an index on (state, expires_at_utc). The sweeper runs SELECT reservation_id FROM inventory_reservation WHERE state = 'held' AND expires_at_utc < now() ORDER BY expires_at_utc LIMIT 500 FOR UPDATE SKIP LOCKED, then updates those rows to 'expired'. The backlog of expired held rows never drops below a few thousand, so the LIMIT is always satisfiable. It ran in 4 ms for months; during a promotion it went to 9 seconds and EXPLAIN now shows a sequential scan. Give four distinct explanations consistent with those facts, say how you would distinguish them, and give the index you would ship.

Approach

  1. Kill the selectivity answer first, because it is the one everybody reaches for and it does not survive the LIMIT. state = 'held' is an equality on the leading column and expires_at_utc < now() is a range on the second, so the scan opens at the start of the held group and walks forward already ordered by expires_at_utc; the range bound is a stop condition, not a filter. It touches roughly 500 index entries and 500 heap tuples whether held is 0.2 percent of the table or 40 percent of it. The competing plan has to sort 400 million rows before it can emit the first one, because Sort is blocking and a LIMIT cannot discount a startup cost. No distribution of held makes that cheaper. So a sequential scan here means the ordered path became unavailable, not that it was outbid, and every explanation worth having is about availability.
  2. First explanation: the planner cannot see a usable index. A CREATE INDEX CONCURRENTLY or REINDEX CONCURRENTLY that failed or was cancelled under promotion write load leaves pg_index.indisvalid = false, and the planner ignores such an index entirely while every write still maintains it, so write cost looks unchanged while reads fall off a cliff. Same class: a migration dropped or renamed it, or the table was partitioned and the index was only created on some partitions. Distinguish with indisvalid and indisready in pg_index, and with pg_stat_user_indexes.idx_scan, which stops incrementing at a timestamp you can line up against the deploy log.
  3. Second explanation: the statement stopped matching the index. Three shapes, all of which remove the ordered path rather than outrank it. The sort key or the range got wrapped — date_trunc('minute', expires_at_utc) < now(), or expires_at_utc::date — and no plain B-tree serves an expression. The leading column got wrapped or made collation-sensitive — state::text = $1, lower(state) = 'held', state LIKE 'held' under a non-C collation — so the equality that anchors the scan is gone and the index can neither bound nor order. Or the sweep was widened to state IN ('held','releasing'), which replaces that single equality with an array qualifier; whether nbtree can still return such a scan in index order is version-dependent (its array handling was reworked in PostgreSQL 17), so read EXPLAIN on your own version instead of asserting either way. Distinguish by diffing the normalised text in pg_stat_statements against what you believe is deployed, then EXPLAIN each candidate form side by side.
  4. Third explanation: the planner was not allowed to pick an index scan. enable_indexscan = off is the setting that reliably produces this shape; note that a merely hostile random_page_cost is not, for the same reason the selectivity story failed — the LIMIT means only about 500 random fetches are ever costed, so multiplying their price does not reach the cost of a full sort. Such a setting arrives from pg_db_role_setting, a connection pooler's startup options, or an ORM's per-session setup, which is why you read pg_settings name, setting, source and sourcefile from the sweeper's own connection rather than from your psql session. Those two sessions differing is the finding.
  5. Fourth explanation: the 9 seconds and the sequential scan are two incidents, and only one of them is a plan change. The sweeper updates state, which is indexed, so no update on this table can be a HOT update and every sweep leaves a dead index entry behind. Let a long-running transaction or a replication slot pin the xmin horizon and autovacuum cannot reclaim them, so the ordered scan walks a growing prefix of invisible entries to find 500 live ones, and FOR UPDATE SKIP LOCKED makes it read past every row the other sweeper instances hold on top of that. That is seconds of work with the index still in the plan, which means it explains the timing but not the EXPLAIN output, and saying so out loud is the point. Distinguish with n_dead_tup against n_live_tup, pg_stat_activity ordered by xact_start, pg_replication_slots.xmin, and EXPLAIN (ANALYZE, BUFFERS) buffer counts weighed against an output of 500 rows.
  6. Ship CREATE INDEX CONCURRENTLY ON inventory_reservation (expires_at_utc) WHERE state = 'held'. Moving the state predicate out of the key and into the index predicate leaves a key that is ordered by expires_at_utc globally, so the ORDER BY no longer depends on an equality surviving in the WHERE clause — and if the sweep is widened later, you widen the index predicate to WHERE state IN ('held','releasing') and keep the ordering, rather than losing it to an array qualifier in the key. It holds only rows the sweeper wants, so the walk never steps over committed or expired entries, and it shrinks as rows leave held, which bounds the dead prefix from the fourth explanation without removing it. Two preconditions: the planner must prove the query's predicate implies the index's at plan time, so 'held' has to reach it as a literal and not as a bound parameter inside a generic plan; and expires_at_utc must stay unwrapped in both the predicate and the ORDER BY. Say plainly that this index does not fix bloat — only vacuum and releasing the xmin horizon do.

Follow-up

  • SKIP LOCKED is doing real work here. What happens to the ORDER BY and the LIMIT when 400 of the top 500 rows are already locked by a second sweeper?
  • A committed reservation must never be expired. Where does that guard live so a second sweeper instance cannot break it?
  • The sweeper's UPDATE is now the slowest statement in the system. Would you batch it differently, and what does each batch size do to replication lag?

Keep the search read model correct when events arrive out of order

Hard · denormalisation · read models · event ordering

Search serves 10,000 queries per second over two million variants with filters, facets and sorting. The origin tables are product_variant, a price table, and one inventory_position row per variant per node. Design the denormalised row the index is built from, justify it from that read pattern rather than from convenience, and write the upsert a consumer runs. Catalog, pricing and inventory publish independently, each with its own monotonic per-row counter, and delivery is at-least-once. State exactly what your upsert does with an event older than the row it hits.

Approach

  1. Justify the denormalisation with arithmetic rather than with the word performance. Every query filters, facets and sorts, so a live join would hit three sources, and availability alone is a GROUP BY over one inventory_position row per node per variant. At 10^4 queries per second that join is not affordable per request; at 10^2 it would be, and saying so is the difference between a judgement and a habit. One precomputed row per variant is what the read pattern forces.
  2. Pin the grain and state its cost. One row per variant means a product with forty colourways floods the first page; collapsing to product grain means the displayed price is an aggregate — the cheapest variant satisfying the active filters, which is a different number from the cheapest variant overall. Pick one and make the facet counts agree with it, because counts computed before the filter and results produced after it is a correctness defect, not a ranking preference.
  3. Reject a single version column. Three producers with independent sequences cannot share one counter: gating a price update with catalog's version either discards good prices or admits stale ones, depending on which producer happens to be ahead. Carry catalog_version, price_version and inventory_version, each gating only the columns its producer owns.
  4. Make a stale event a no-op rather than an error: INSERT ... ON CONFLICT (variant_id) DO UPDATE SET ... WHERE variant_search_row.catalog_version < EXCLUDED.catalog_version. Then state the consequence the consumer must handle, because it is the part teams miss: a conflict suppressed by that WHERE affects zero rows and RETURNING yields nothing, and a consumer that reads zero rows as failure will retry that event forever.
  5. Decide availability semantics before storing them. in_stock_any_node is a hint for filtering and ranking and never a promise; the binding decision stays the conditional write against inventory_position. That makes seconds of staleness acceptable here, and makes the worst outcome a customer reaching a variant that turns out to be gone — a refusal at reserve time rather than an oversell.
  6. Set freshness budgets by consequence rather than uniformly. Price and availability must propagate within seconds because they are user-visible and materially wrong when late; synonyms, boosts and relevance weights ride the next full rebuild, which doubles as the recovery path for a consumer that mis-applied events for an hour.

Worked solution (40 min)

  1. Create variant_search_row with variant_id as primary key, the display and filter columns, and three separate version columns; seed 100,000 rows.
  2. Apply a catalog event at version 7, then replay version 5 for the same variant, recording rows affected on each.
  3. Interleave a price event and a catalog event for one variant in both orders and compare the final rows.
  4. Time the filtered facet query against the denormalised row versus a live three-way join with a per-node GROUP BY, using the same predicate.
  5. Collapse the three version columns into one, repeat step three, and record what changes.

Expected result: The version-5 replay affects zero rows and returns nothing from RETURNING, leaving the row at version 7. With separate guards, interleaving price and catalog events in either order converges to the same final row. The denormalised query is orders of magnitude cheaper than the live join, and the gap widens as fulfilment nodes are added because the join's GROUP BY grows with them. With one shared version column the final row depends on arrival order.

Verify it yourself

  • Rows affected is zero for the stale event — assert on it explicitly, because a consumer that treats it as failure retries forever.
  • After the single-column collapse, the same two events applied in opposite orders produce different rows; that divergence is the bug made visible rather than argued about.
  • A reserve against a variant the index still calls in stock refuses correctly, confirming the read model is a hint and not an authority.
  • Facet counts and the returned result count agree under your chosen out-of-stock policy; if they diverge, the filter and the count are running at different points.

Follow-up

  • A consumer lags an hour and then replays from its offset. What does your version guard do, and what is still wrong after the replay finishes?
  • Out-of-stock variants: disappear, sink, or render with a restock date? Give the answer and say what each choice does to facet counts.
  • A repricing job emits 200 events per second for one variant for ten minutes. What do you change in the consumer so the rest of the index does not fall behind?

Make cart line edits safe for a retrying mobile client

Easy · idempotency · rest semantics · optimistic concurrency

The caller is a mobile app on a cellular network that retries any request it does not hear back from within three seconds, and it cannot distinguish a lost request from a lost response. Today POST /carts/{cart_id}/lines with {variant_id, quantity} appends or increments server-side, so a retry silently doubles what the customer sees. Redesign the cart mutation contract: the verbs, the resource paths, who mints the identifiers, and the concurrency control. For each design, state what a retry after a lost response returns and what the app does with that answer.

Approach

  1. Name why the current shape cannot be patched: POST is not idempotent by definition (RFC 9110 section 9.2.2) and the body carries a delta, so the resulting state is a function of how many times the request arrived. Any fix has to make the state a function of the request content alone.
  2. Move line identity to the client: PUT /carts/{cart_id}/lines/{client_line_id} carrying an absolute quantity, where client_line_id is a UUID the app mints when the line first enters the cart. Replaying it converges on the same state and the server stores nothing extra. The cost is real: an 'add one more' button becomes read-modify-write in the app, and two of the customer's own tabs resolve as last-writer-wins on quantity rather than summing.
  3. Where a delta genuinely is the intent, keep POST but require an Idempotency-Key minted once per button press rather than per HTTP attempt, stored alongside the response with a TTL and a hash of the request body, so a replay carrying a different body is a 409 conflict instead of a silent replay of a stale answer.
  4. Add If-Match over an ETag for the concurrent-editor case, and state the limit out loud: If-Match does not make a retry safe, because a retry of a request that already succeeded receives 412 and the client cannot tell 'I did this' from 'someone else edited this'. Conditional requests solve concurrency; idempotency keys solve retries; the contract needs both.
  5. Choose the ETag grain deliberately. A cart-level ETag is one value and makes simultaneous edits to two different lines conflict spuriously; a per-line ETag lets them proceed but gives up any whole-cart snapshot guarantee.
  6. Return the full cart representation and its new ETag from every mutation, so a retry that lands as a no-op still leaves the client holding the truth without a follow-up GET.

Worked solution (25 min)

  1. Write the three candidate shapes side by side: POST delta, POST delta with an Idempotency-Key, and PUT absolute against a client-minted line id.
  2. Trace each through the four timeout outcomes: request lost, response lost, duplicate delivered after the first succeeded, duplicate delivered while the first is still in flight.
  3. Pick PUT-absolute as the default and write the identifier rule, including when the app is allowed to reuse a client_line_id.
  4. Specify the concurrency story: ETag grain, If-Match required or optional, the status on mismatch, and the client's recovery path from it.
  5. Work out what 'duplicate delivered while the first is in flight' does in the database: both writes target the same primary key, the second waits on the row lock and then applies, which is only safe because the value is absolute rather than a delta.

Expected result: A contract in which every mutation is safe to send twice: PUT with a client-minted line id and an absolute quantity as the default shape, Idempotency-Key reserved for operations that are genuinely not idempotent, and If-Match used for concurrent editors with a documented client recovery from 412.

Verify it yourself

  • For all four timeout outcomes, replaying a single request leaves the cart in the same state as sending it once.
  • Your answer states the ETag grain you chose and what it costs: under a cart-grain ETag two concurrent edits to different lines produce one 412, under a line-grain ETag both succeed.
  • The client never needs a GET to learn the outcome of its own mutation.
  • No endpoint in the design produces a different result on its second delivery than on its first.

Follow-up

  • The app is offline for two minutes and queues four edits to the same line. What does your contract do when all four arrive at once, in the wrong order?
  • How long do you retain idempotency keys, and what does the server return for a key whose record has aged out?
  • A quantity of zero arrives on the PUT. Is that a delete, a validation error, or a stored zero, and which one does the app expect?

Design order-line transitions when warehouse events go silent

Medium · state machines · at-least-once delivery · compensation · consistency vs availability

order_line carries state in ('reserved','released_to_node','picked','shipped','delivered','cancelled','returned'), assigned_node_id, carrier_tracking_code and shipped_at_utc. Cancellations arrive from the storefront; pick, pack and manifest events arrive from the warehouse over an at-least-once bus with no ordering guarantee across the two channels. During a peak hour the warehouse connection drops for twenty minutes. Specify the transition table, the guard every writer uses, the deduplication mechanism, and the compensation for a cancel arriving in each state. State what the storefront tells a customer while warehouse events are unavailable.

Approach

  1. Write the transition table at line grain, not order grain, because one line ships while a sibling is cancelled or back-ordered. List the terminal states (delivered, cancelled, returned) and the illegal transitions by name. Cancel is legal up to and including picked by policy; it becomes illegal the moment carrier_tracking_code is non-NULL, because that is the physical point of no return and no amount of software makes it reversible.
  2. Make every transition UPDATE order_line SET state = $new WHERE order_line_id = $1 AND state = $expected, and treat zero affected rows as the losing branch rather than an error, since that branch is where compensation is decided. The guard buys nothing unless every writer goes through it, including admin tooling and backfills; one blind UPDATE in a support script reintroduces the entire race.
  3. Deduplicate on the producer's event id in a processed_event table written in the same transaction as the effect. At-least-once delivery combined with idempotent effects is achievable; exactly-once delivery over an unreliable network is not, and conflating them produces handlers that are correct only on their first run.
  4. Enumerate compensations by state and note that they are wildly asymmetric, which is the whole reason the ladder must exist before it is needed: reserved means release the reservation and void the authorization, which is free; released_to_node means a recall request that can lose the race; picked means cancel-at-pack, still cheap; shipped means a return authorization with a pre-paid label and a refund on receipt, which takes days and frequently costs more than the margin on the order.
  5. For the named failure, separate the accept path from the promise. With the warehouse bus down your local state is stale, so a cancel cannot be confirmed. Choose availability for accepting the request and consistency for the assertion: persist the cancel as an intent, tell the customer 'cancellation requested' rather than 'cancelled', and reconcile on recovery, converting the intent to a return authorization when the replayed events show a manifest already happened. Asserting a cancellation you cannot enforce is the failure mode that generates both a shipped parcel and a refund.

Follow-up

  • The bus recovers and replays twenty minutes of events out of order. Walk through a line that was picked, then cancelled, then manifested.
  • Where does the cancel intent live, and what expires it if the warehouse never confirms?
  • One line ships and one is cancelled on the same order. What is the capture amount, and when is it decided?

Reservations expire an hour early after a clock change

Medium · timezones · clock semantics · expiry

inventory_reservation.expires_at_utc is TIMESTAMPTZ and reservations carry a fifteen-minute TTL. After a daylight-saving transition, holds created by one checkout path began expiring about an hour before their TTL, releasing units while authorizations were still in flight. A second symptom predates it: during the nightly sweep window, rows that are past expiry are left in state 'held' for tens of minutes. Find both defects, give the ordered checks that separate them, and the fix.

Approach

  1. Split the problem with one query before theorising: compare expires_at_utc minus created_at_utc on the affected rows. If the delta is not fifteen minutes, the write is wrong; if it is exactly fifteen minutes, the write is fine and the sweeper's comparison is wrong. Here both are true on different rows, which is why one theory never explained everything.
  2. Group the bad deltas by writing service and release. Only one checkout path is affected, which rules out the column type — a TIMESTAMPTZ column stores an instant and cannot itself be ambiguous — and points at what the client sent.
  3. Check what that client sends and what the session interprets it as. The path computes expiry from a naive local datetime and serialises it without an offset, so the value is resolved using the connection's TimeZone setting. Compare SHOW TimeZone across the services; the writer and the sweeper do not agree.
  4. Use the size of the shift as the discriminator. The error moved by exactly the offset change at the transition, not by a drifting amount, which rules out clock drift and NTP and rules in zone arithmetic on a naive value. Fix by constructing timezone-aware instants, or by letting the database compute now() + INTERVAL '15 minutes', and by rejecting naive datetimes at the serialisation boundary rather than defaulting them.
  5. Now take the second symptom. In PostgreSQL now() is transaction_timestamp(), fixed at transaction start, so a sweeper that opens one transaction and grinds through a large batch is comparing against a cutoff that is as stale as the transaction is long. Use clock_timestamp(), or recompute the cutoff per batch and commit per batch, which also keeps the batch off the index for less time.
  6. Pin both with tests that cannot rot: a frozen-clock test at the transition instant asserting the stored delta, and a constraint or write-path guard rejecting any reservation whose expires_at_utc minus created_at_utc falls outside the TTL band, so the next path that serialises a naive value fails at the write rather than in the warehouse.

Follow-up

  • The sweeper predicate is state = 'held' AND expires_at_utc < cutoff. What happens if the state clause is dropped, and which invariant does that break?
  • A promotion window is configured as local wall-clock times in an operator's zone. Which two instants in the year have no single correct interpretation, and what does the schema need?
  • Where in this system is it wrong to measure an elapsed duration with the wall clock at all?

Suggested practice checklist

Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.

Day 1 — Fix the scope and take a cold baseline

  • Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
  • Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
  • Set the week's rule: one thing finished every night, including the night you only have forty minutes.

Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.

Day 2 — One pattern, written three times from blank

  • Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
  • On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
  • Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.

Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.

Day 3 — One design, only to the depth you can defend

  • Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
  • Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
  • Write the one tradeoff you are choosing against and the observation that would make you reverse it.

Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.

Day 4 — Only the fundamentals you will have to defend

  • Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
  • Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
  • Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.

Deliverable: Three written answers, each under two hundred words and each timed aloud.

Day 5 — Your own work, timed

  • Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
  • Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
  • Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.

Deliverable: Two timed narratives with one defensible number in the opening line.

Day 6 — The one full rehearsal, in the weekend block

  • Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
  • Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
  • Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.

Deliverable: Mock notes naming three failure moments with a specific fix written under each.

Day 7 — Taper

  • Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
  • Re-read only your own notes from this week and open no new material.
  • Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.

Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.

Review and mentoring: what you let through

Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.

How do you manage conflicting priorities during a complex project rollout?

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Pick a story where you made the decision, not one where you watched it.
  3. Close with what you would do differently, concretely.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Describe a situation where you had to adapt your technical approach based on operational feedback.

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Pick a story where you made the decision, not one where you watched it.
  3. Close with what you would do differently, concretely.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Can you provide an example of how you have contributed to a team's success under strict deadlines?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Close with what you would do differently, concretely.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

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

Tell me about a time you were criticized by your supervisor.

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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