Interview guides · Shamrock Foods
Shamrock Foods Software Engineer Interview Questions & Guide 2026
Take-home style practice for Shamrock Foods software engineer candidates: a small service, a fixed budget, a written tradeoff.
The 60-second brief
As a Software Engineer at Shamrock Foods, you are at the intersection of traditional supply chain logistics and modern digital transformation. You will contribute to the development and maintenance of robust software solutions that ensure the seamless distribution of food products across the country. Your work directly affects the efficiency of internal operations and the quality of service Shamrock Foods provides to its customers.
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
- Reconcile payment state after timeouts instead of retrying
- Model variant attributes without wide tables or EAV
- Design order state machines with explicit compensating transactions
Overview
The role requires a high degree of technical versatility. You will be expected to navigate a complex technology stack, often working across the full lifecycle of a feature—from initial architectural design and coding to debugging and deployment. Because Shamrock Foods values operational reliability, your ability to write clean, maintainable, and performant code is essential to the engineering team's success.
The Shamrock Foods interview process
Shamrock Foods candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Screening
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 Assessments
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Onsite Assessments
A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
Where candidates lose points
Charging from a denormalised read model without a quote the checkout re-validates
Prices, promotions and tax rates reach the storefront through an asynchronous pipeline, so the number rendered on the page is a snapshot of state that has already moved on by the time the customer reaches capture. Charging whatever the cache said means honouring expired promotions indefinitely, because nothing in the flow ever re-derives the price; charging whatever is current means the captured amount differs from the displayed amount, which is the failure customers escalate and regulators care about. Mint a signed quote with a short TTL, store its id on the order, re-evaluate at capture, and make the mismatch branch an explicit, visible failure. The detail that gets skipped is the TTL: an unbounded quote is a durable discount that anyone who scripts the checkout can hold open.
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.
Never running a concrete value through the code
Trace one small input and one edge input by hand, index by index, out loud. Re-reading your own code catches design mistakes; walking a real value through it catches the off-by-one, the uninitialised accumulator and the loop that never advances.
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.
Questions & practice
How would you implement a palindrome detector?
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
What are the differences between == and ===?
Medium · data structures and algorithms
Approach
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Explain key Java features.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Explain the difference between StringBuilder and string.
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Recompute reserved units and report drift per fulfilment node
Easy · aggregation · hashing · reconciliation · streaming
You are given two unsorted streams. inventory_position rows carry (variant_id, node_id, on_hand_units, reserved_units, damaged_units, safety_stock_units, oversell_allowance_units) — up to 40 million rows. inventory_reservation rows carry (reservation_id, variant_id, node_id, units, state, expires_at_utc) — up to 20 million rows. Recompute reserved units from the reservation rows, report every (variant_id, node_id) whose stored reserved_units disagrees, and emit available-to-promise for the rest. Define which reservation states you count. One pass over each stream, no re-reads. State your time and space bounds.
Approach
- Fix the predicate before writing any code: reserved is SUM(units) over rows with state IN ('held','committed'). The case that decides the design is a 'held' row already past
expires_at_utcthat the sweeper has not reached. Count it, because the reserve path's conditional UPDATE still counts it — a reconciliation job using a different predicate from the writer reports drift forever instead of converging. - Build one hash map keyed
(variant_id, node_id)to an int32 sum over the reservation stream: O(R) time, one entry per key holding live units. Bound it out loud — 20 million reservations is the upper bound on live keys, and at roughly 48-64 bytes per map entry that is order 1 GB. If the box is smaller, partition byhash(variant_id) % Pand run P passes for 1/P the memory at P times the read cost. - Stream
inventory_positionand probe, rather than sorting either side. Per row computeatp = on_hand - damaged - recomputed_reserved - safety_stock + oversell_allowance. Clamp to zero for display only and keep the raw signed value for the report: a negative raw ATP means the oversell invariant is already breached, which is a different alert from a drifted counter. - Emit drift as
(key, stored, recomputed, delta)rather than writing a correction inline. A repair from a scan is only safe if the position row has not moved since it was read, so pin both reads to one snapshot — in PostgreSQL a REPEATABLE READ transaction takes its snapshot at the first statement and every later statement sees it — and make the repairUPDATE ... WHERE reserved_units = <observed>, judged by affected rows, so a concurrent reserve makes it a no-op instead of reintroducing the drift. - Complexity: O(R + P) time, O(keys with live holds) space, two sequential passes, no sort. The drift check rides on the same pass as the ATP computation, which is the argument for doing both here rather than in two jobs that can disagree.
Worked solution (25 min)
- Write the state predicate and the ATP expression on one line each, and say explicitly which side an expired-but-unswept hold falls on.
- Build a 100,000-reservation fixture with three planted defects: one position row whose
reserved_unitsis 2 high, one that is 1 low, and one key present only in the reservation stream. - Run the job and confirm exactly three findings, with the expected signed deltas and the orphan reported separately from the drift.
- Re-run with a concurrent writer inserting a held reservation on the 2-high key, and confirm the guarded repair affects zero rows instead of clobbering the new hold.
- Measure peak resident memory, divide by the live-key count to get bytes per entry, and reconcile that against your estimate.
Expected result: Three findings and no others: a +2 drift, a -1 drift, and one orphan key reported in its own category. Every non-drifting key's recomputed reserved figure equals its stored value exactly, and ATP is computed from the recomputed figure rather than the stored one.
Verify it yourself
- Summing
unitsover live reservations for any single key by hand matches the job's recomputed value for that key. - No key appears both in the drift report and in the clean ATP output.
- Setting a position row's
oversell_allowance_unitsto zero turns at least one previously legal row negative in raw ATP, proving the allowance term is actually in the expression.
Follow-up
- A key appears in the reservation stream with no matching
inventory_positionrow. Is that drift, a broken foreign key, or a race with node onboarding — and what does the job do with it? - The job finds a delta of -3 at 03:00 on a variant that is mid-drop. Do you correct it, and what evidence makes that decision?
- Two copies of this job run concurrently after a deploy. What makes the second one harmless?
Retire a variant without breaking historical order lines
Easy · soft deletes · audit history · referential integrity
product_variant has status in draft, active, discontinued, blocked, and sku_code UNIQUE. order_line references variant_id and already stores title_snapshot and unit_price_minor. A team proposes adding deleted_at to product_variant, hard-deleting anything discontinued for a year, and reusing the freed sku_code values. Say what each of those three does to order_line and to the unique constraint, give the DDL you would actually ship including any audit table, and write the query listing unshipped lines on confirmed orders whose variant is no longer active.
Approach
- Kill the hard delete on referential grounds rather than on principle. order_line.variant_id is a foreign key, so a DELETE either fails under the default NO ACTION or, with ON DELETE CASCADE, silently removes paid order history. Neither of those is a retention policy.
- Point at title_snapshot and unit_price_minor: that denormalisation is exactly what makes retirement survivable, because rendering an old order never joins product_variant. The row then only has to persist for the foreign key and for analytics, not for display, which is a much weaker requirement than the team assumed.
- Prefer the status enum that already exists over a parallel deleted_at. Two independent ways for a row to be dead means every query needs both predicates, and the one somebody forgets is the one that resurfaces a blocked variant in a facet or a sitemap.
- If soft delete ships anyway, the unique constraint must become a partial unique index — UNIQUE (sku_code) WHERE deleted_at IS NULL — or a retired row holds the code forever. Then argue against reuse on its merits: sku_code is the warehouse- and supplier-facing identifier, so reusing it makes every historical pick, return and supplier invoice ambiguous about which product it refers to.
- Put history in product_variant_audit at one row per change, keyed (variant_id, version), carrying the changed columns and changed_at_utc, written by the single writer or a trigger. For as-of questions either keep valid_from_utc and valid_to_utc with an EXCLUDE constraint on (variant_id WITH =, validity WITH &&) using btree_gist to forbid overlaps, or reconstruct by taking the newest audit row at or before the timestamp — the first costs a write-side constraint, the second costs a per-query sort.
Worked solution (25 min)
- Attempt a DELETE of a variant referenced by order_line and record the exact error and the constraint name it reports.
- Add deleted_at, mark one variant deleted, and try to insert a new variant reusing its sku_code; record the unique violation.
- Replace UNIQUE (sku_code) with a partial unique index WHERE deleted_at IS NULL and repeat the insert.
- Write and run the unshipped-lines query joining order_line to customer_order and product_variant, filtering order_line.state in ('reserved','released_to_node') and product_variant.status <> 'active'.
- Mark a variant discontinued rather than deleted and confirm the query still returns its lines.
Expected result: The DELETE fails with a foreign-key violation naming order_line's constraint; it never cascades. Under plain UNIQUE, SKU reuse fails even though the old row is soft-deleted; under the partial unique index it succeeds, which is precisely the ambiguity you then argue against. The query returns one row per affected order_line with its order state and the variant's current status.
Verify it yourself
- Two soft-deleted rows can now share a sku_code under the partial index — insert a second and confirm it is permitted, then decide whether that is acceptable.
- EXPLAIN the order-history read path and confirm product_variant does not appear in the plan at all, which is the snapshot columns doing their job.
- The query keys on order_line.state and product_variant.status, not on deleted_at, so it keeps working whichever retirement mechanism ships.
Follow-up
- A returns agent needs the variant's hazmat flag as it stood on the shipment date. Which of the two history shapes answers that in one index seek, and what does the other cost?
- An erasure request arrives for a customer, not a variant. Which columns on customer_order and order_line can be erased while the financial record stays reconcilable?
- Merchandising wants discontinued variants gone from search within a minute. What event do you publish, and what does the consumer do with it?
Model variant attributes without a wide table or EAV
Easy · attribute modelling · gin indexes · grain
product_variant holds variant_id, product_id, sku_code, status, attributes JSONB, list_price_minor, currency_code, and nullable weight_grams, length_mm, width_mm, height_mm. One table must carry apparel (size, colour), an appliance (voltage, capacity_litres) and a consumable sold by weight. Give the DDL: which attributes you promote to real columns, which stay in attributes, and every index you create. Then write the facet query for active charcoal size-M variants priced between two bounds, and say which index serves which predicate and which predicate no index helps.
Approach
- Fix the grain before the storage. Description, category and reviews hang off product; anything reserved, picked, weighed or rated by a carrier hangs off the variant. Promote to real columns exactly the attributes with cross-category meaning or a non-containment access pattern: list_price_minor because it is range-filtered and sorted, and the four dimension columns because the sellable check tests them for NULL and carrier rating reads them. Category-specific facets stay in attributes.
- Reject the two extremes with their costs stated rather than by taste. A wide table needs a migration and an index per category; PostgreSQL caps a table near 1600 columns and mostly-NULL rows are cheap because of the null bitmap, so the real price is schema coupling, not bytes. EAV turns a k-facet filter into k self-joins or a GROUP BY with k FILTER clauses and HAVING, drops type checking, and leaves the planner estimating cardinality it cannot see.
- Index the document for the operator you actually issue. GIN with jsonb_path_ops serves containment (@>) and is smaller than the default jsonb_ops, but it does not serve key existence (?), ordering, or any range. That limitation is what decides which attributes had to become columns.
- Add a B-tree expression index for every attribute filtered by range, for example ((attributes->>'capacity_litres')::int). Two preconditions: the query must repeat the identical expression or the index is ignored, and the cast must be immutable — text to int is, text to timestamptz is not and PostgreSQL will refuse the index.
- Make the indexes partial on status = 'active', since only active is sellable, and expect the plan to be a BitmapAnd: PostgreSQL bitmap-scans the GIN for containment and the B-tree for the price range and intersects them. That is why two indexes beat the single composite index that cannot exist across a JSONB operator class and a scalar column.
Follow-up
- Facet counts have to come back with the result set. How do you compute the count per colour without scanning the filtered set twice, and what does an out-of-stock filter do to those counts?
- A supplier feed rewrites 500,000 attribute values in one run. What does that do to the GIN index and its pending list, and what would you change about the load?
- Someone wants ORDER BY capacity_litres DESC LIMIT 24. Which of your indexes serves that, and what happens on the rows where the key is absent?
What are JavaScript polyfills, and why is transpilation necessary?
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What would you drop to keep the system up under load?
- What breaks first when traffic grows ten times?
How do you approach C# service debugging in a live environment?
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Hold inventory for one variant under a thousand attempts per second
Hard · lock contention · write-heavy · sharding · backpressure
A single variant with 500 units at one node draws 1,000 reservation attempts per second for ninety seconds. Reservations are rows in inventory_reservation (state, expires_at_utc, idempotency_key UNIQUE, partial unique on (owner_type, owner_id, variant_id, node_id) WHERE state IN ('held','committed')) and move inventory_position.reserved_units under a CHECK enforcing the oversell bound. Assume a 3ms lock hold for the local update and a 300ms payment authorization. Compute the throughput ceiling on that one row, then design a path that admits 500 winners without overselling, naming what each mitigation costs.
Approach
- Do the arithmetic before the architecture. A row lock is held from the UPDATE until COMMIT, not for the duration of the statement, so the ceiling on one row is roughly one update per hold time: about 333/s at a 3ms hold. Put the 300ms authorization inside that transaction and the hold becomes about 303ms, so the ceiling collapses to roughly 3/s while each waiter pins a connection. At 1,000 attempts/s the queue grows without bound and the connection pool dies long before the stock runs out. The first design decision follows from the number: no network call inside the reservation transaction.
- Split the flow into a short local phase and a long remote one. Reserve with a conditional UPDATE judged by affected-row count, commit, authorize outside the transaction, then commit the reservation with a compare-and-set on state='held'. The reservation TTL must exceed the p99 authorization latency plus the retry budget, or the expiry sweeper releases units out from under a customer whose payment is in flight, which is the phantom-stock failure in its most expensive form.
- Then remove contention, costing each option honestly. Sharding the counter into N sub-rows with random selection multiplies capacity by N, but 'how many are left' becomes a SUM over N rows and units strand in shards nobody selected, so it needs a rebalancer and it makes the last few units hard to sell. A single-writer partition per variant, with one consumer of a variant-keyed log, removes contention entirely but converts throughput into latency bounded by queue depth over service rate, and that partition is a single point of failure. An admission-control token bucket in front protects the database but must return a real waiting state to the customer, not an error.
- Exploit the asymmetry in this specific workload: 90,000 attempts chase 500 units, so the cheapest correct design rejects almost everything before it touches the row. A per-variant token bucket admitting roughly 600 (supply plus margin for abandonment) sends 6.7 admissions/s at the contended row, two orders of magnitude under its 333/s ceiling, while 89,400 requests get a fast rejection or a queue position and never open a transaction.
- Keep the database-level backstops regardless of which mitigation is chosen, because they are what survives a code path nobody remembered: the CHECK enforcing reserved <= on_hand - damaged + oversell_allowance, the partial unique index so a double submit yields one hold rather than two, the unique idempotency_key, and the sweeper reading (state, expires_at_utc) as an equality-then-range scan with an explicit guard that state='committed' is never expired.
Worked solution (45 min)
- Compute the single-row ceiling at a 3ms hold, then recompute it with the 300ms authorization inside the transaction, and write both numbers down.
- Rewrite the reserve step as one conditional UPDATE with the else-branch for zero affected rows, and place the authorization strictly outside its transaction.
- Size the reservation TTL from an assumed p99 authorization latency plus one retry, and state what the sweeper does to a reservation whose authorization is still in flight.
- Size the admission bucket from supply plus an abandonment margin, and compute the resulting arrival rate at the contended row.
- Name the mitigation you did not choose and the specific cost that ruled it out.
Expected result: About 333 updates/s on the contended row at a 3ms hold, falling to about 3/s with the payment call inside the transaction. The chosen design keeps arrivals at that row near 6.7/s by admitting roughly 600 of 90,000 attempts, sells all 500 units, rejects the rest at the gate, and relies on the CHECK constraint rather than application logic for the oversell bound.
Verify it yourself
- Both throughput numbers are stated explicitly, and the second one is the argument for moving the payment call out of the transaction.
- Sum of committed units across all shards or partitions never exceeds on_hand minus damaged plus the oversell allowance, verified by the CHECK rather than by a report.
- A double-submitted reserve with the same owner produces one held reservation, enforced by the partial unique index.
- Every mitigation in the answer is paired with a concrete cost, not just a benefit.
Follow-up
- Pick N for the sharded counter and defend it, then say how you detect stranded units and what you do about the last twenty.
- The sweeper crashes halfway through a batch of expiries. What is the state of the world, and what does the next run do differently?
- The token bucket admits 600 and only 430 convert. How do the remaining 70 units get sold?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
The estimate and the day you knew
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
Why do you want to work at Shamrock Foods?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
Explain your experience with Azure platform services.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Reverse a sharded inventory counter after production contradicts it
Medium · sharded counters · reversal · false negatives · reconciliation
You proposed sharding the hot variants' inventory row into sixteen sub-rows to survive drops, and it shipped. Two weeks later support reports items showing as unavailable while units sit on the shelf, and the remaining-count figure disagrees with the warehouse. Describe a decision you reversed after production contradicted it: the signal that changed your mind, what you measured before acting, how you unwound it without causing a second incident, what you kept from the original change, and how you told the people who had backed the original proposal.
Approach
- Name the failure mode of your own design rather than treating the reports as mysterious. With random shard selection, units strand in shards nobody selects, so a request can fail while the total is non-zero, and the false-negative rate rises as the tail drains. Separately, the remaining count is now a SUM over sixteen rows that is never a consistent snapshot, which is why it disagrees with the warehouse.
- Measure before reversing, because the reversal is itself a change with risk. Two numbers decide it: the distribution of units across shards for hot variants over time, and the count of reserve failures where the SUM across shards was greater than the requested quantity. The second number is the bug, quantified.
- Consider repair before reversal honestly. A miss that falls through to other shards removes the stranding but reintroduces contention exactly on the variants you sharded for, which is a real cost, so say which one you chose and why rather than presenting reversal as obvious.
- Narrow the blast radius instead of choosing globally. Sharding is only needed for a few hundred variants for a few hours a quarter, so the defensible landing is a per-variant flag that shards on the way into a drop and consolidates on the way out, which keeps the mitigation and deletes the permanent cost.
- Unwind carefully: consolidate the shards back into one row inside a single transaction, assert that the sum before equals the value after, do it behind the flag one variant at a time, and keep the CHECK constraint in place throughout so a mistake becomes a failed statement rather than an oversell.
- Say what you kept — usually the parts that were load mitigation rather than correctness changes, such as request coalescing on the availability read and admission control in front of checkout — and describe how you told the people who had supported the proposal, because a reversal handled badly costs you the next proposal.
Follow-up
- How do you consolidate shards while traffic is live without freezing sales on that variant?
- The next drop is in six days. Do you ship the per-variant flag or go unsharded and risk the contention? Argue it with numbers.
- What would you have measured before the original rollout that would have caught stranding in staging?