The Software Engineer role at Midwest Foods is a critical function dedicated to digitizing and optimizing the complex logistics that power a premier wholesale food distributor. As a family-owned business with a vast reach across the Midwest, Midwest Foods relies on robust technical solutions to manage inventory, streamline distribution, and ensure that fresh products reach food establishments reliably and safely.
You will build and maintain the digital infrastructure that supports Midwest Foods' operations, from warehouse management to delivery routing. Your work will directly affect the efficiency of the company's supply chain and the quality of service it provides to its clients, including restaurants, hospitals, and schools. This is an opportunity to solve tangible, real-world problems in a fast-paced environment where your technical contributions have immediate, visible results on the business.
While the role is titled Software Engineer, candidates should be prepared to discuss how their technical solutions integrate with the physical realities of supply chain management and logistics, such as delivery routing and inventory tracking.
Initial Screening
reportedThe title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
Technical Assessment
reportedWhat this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Team Discussions
reportedYou cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Final Technical Evaluation
reportedNobody in the room with you decides this. Interviewers typically write their rounds up separately, often before seeing anyone else's, and the outcome is settled later from those write-ups. A split panel gets resolved by whichever note carries specific evidence, so what you want out of each room is one concrete thing that person could write down: a bug you caught yourself, a trade-off you named, a decision you owned. The rest is arithmetic. The project you describe in a behavioural conversation is often the same system you sketched an hour earlier, and the two accounts have to agree.
What to demonstrate
- Whether the scale, team size and timeline you attach to a project hold steady when that project resurfaces in a different round
- Whether each interviewer leaves with a specific thing to cite rather than a general impression of competence
- Whether a trade-off you defended in one round survives a challenge in another, instead of being quietly swapped for the answer the new interviewer seemed to want
- Whether a question you have already answered earlier in the day gets the same answer at the same depth, without visible impatience
How to prepare
- Write a one-page sheet per project fixing the figures you will quote — request volume, data size, team size, elapsed time, what broke — and say them aloud from the sheet until they come out identical every time
- For each round on the schedule, decide in advance the one sentence you want in that person's notes, then check in a mock that you said it outright instead of leaving it to be inferred
- Have someone ask you the same project question twice, an hour apart, and diff the two answers for numbers that moved or a trade-off that reversed
PracHub editorial advice for the preparation topics above.
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.
Applying order state changes as blind writes when events arrive at-least-once and out of order
A cancel request and a 'picked' event travel different paths with no shared ordering, and a message bus that guarantees at-least-once delivery will redeliver either one after a consumer crash. Read-modify-write handlers then let the later writer win arbitrarily, so you ship a cancelled order or refuse to cancel one still sitting on a shelf, and both outcomes are expensive. Every transition needs to be a compare-and-set on the expected current state with an explicit branch for zero affected rows, plus a processed-event table keyed on the producer's event id so that a redelivery is a no-op rather than a second application. Exactly-once delivery is not available over an unreliable network; at-least-once delivery combined with idempotent effects is, and conflating the two is what produces handlers that are correct only on the first run.
Listing technologies instead of trade-offs
Name the property the design needs first, such as ordered range scans, multi-entity transactions, cheap appends, or a predictable p99, then pick something that provides it and say what it gives up in exchange. Almost any component is defensible once you state the requirement it satisfies and the one it sacrifices.
Assuming the input fits in memory
Ask how large the input is in bytes before committing to an in-memory algorithm; beyond that point the options are a single streaming pass, an external sort with bounded buffers, or a sketch that trades exactness for constant memory. An algorithm that assumes random access to the whole input is a different algorithm from one that sees each element once.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Compute peak attempt rate per variant over a sliding window
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
- 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 whilet[right] - t[left] >= 60000, with the count beingright - 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. - 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.
- 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.
- 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.
- 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.
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.
Fold a partitioned catalog event log into current variant state
Seven days of catalog events — about 800 million records of (variant_id, version, payload), payloads averaging 2 KB — sit across 32 partition files. Version is monotonic per variant, but there is no ordering across partitions. There are two million distinct variants. In a container with 512 MB of memory, produce the winning event per variant, then compare against an index snapshot of (variant_id, indexed_version) to emit the stale, orphaned and missing sets. State your passes, your I/O in bytes, the device latency and bandwidth your plan assumes, and your tie rule.
Approach
- Compute the bytes and the key count separately, because they point at different algorithms. 800 million records at a 2 KB payload plus a 64-byte header is about 1.7 TB on disk — the headers alone account for 51 GB — while there are only two million distinct variants. Memory is governed by the key count and I/O by the byte count, so an external merge sort of 1.7 TB is the expensive wrong answer, and explaining why is the substance of the question.
- Pass one folds to offsets rather than payloads. Hold
variant_id -> (version, partition_id, offset)at about 32 bytes of payload per entry plus map overhead, order 150 MB for two million keys, which fits in 512 MB. Retaining the 2 KB payloads instead would be about 4 GB, which does not. - Price pass one in bytes, not in records, and say which layout you are assuming. If the log is row-oriented with payloads inline, pass one streams the whole 1.7 TB — about 3,400 s at 500 MB/s — because seeking past each payload would be 800 million seeks and is strictly worse than reading them. If headers live apart from payloads (columnar segments, or a sidecar index of
(variant_id, version, offset)), pass one touches only 51 GB, about 100 s. Quote the layout before quoting the runtime. - Pass two fetches the winners, and selectivity decides the access pattern. Two million winners out of 800 million records is one in 400, an average stride of about 845 KB. One random read costs 0.1 ms; scanning 845 KB at 500 MB/s costs 1.7 ms. So seek: two million reads is about 200 s and moves 4.2 GB, against about 3,400 s and another 1.7 TB for a second full scan — roughly a 17x win, and about 3,600 s end to end rather than 6,800 s. The breakeven stride is latency times bandwidth, 0.1 ms * 500 MB/s = 50 KB, or one winner per 24 records; denser than that and the re-scan wins. On a 10 ms-seek device breakeven is 5 MB and two million seeks would cost 20,000 s, so the answer inverts — quote the device, not the rule of thumb.
- Sort the winning offsets by
(partition_id, offset)even though you are seeking: one file handle open at a time, every seek forward, readahead coalescing winners that land in the same block, at O(K log K) on two million entries. Keep 32 to 64 reads in flight so the 200 s is latency you overlap rather than latency you serialise. - Make the tie rule total and loud: order by
(version, partition_id, offset)and count ties on(variant_id, version)separately. A tie means a producer emitted two events at one version, which is an upstream defect; resolving it silently is how that defect survives a year unnoticed. - Reconcile against the index snapshot by merge or hash probe, emitting three disjoint sets —
indexed_versionbelow the winning version is stale, present in the index but not sellable under the folded state is orphaned, sellable but absent is missing. Sellability is a conjunction over the folded state (status = 'active', dimensions present whenis_shippable, a price in every active currency, a category assigned), never over an intermediate event. - Emit repairs as absolute assignments naming the target version, so the indexer's existing rule of discarding any event at or below the version it already holds makes a replayed repair a no-op. Complexity: one sequential pass over n records, O(K log K) to order the winning offsets, then K random reads — the log is read end to end once, not twice — and O(K) memory for K distinct variants.
Worked solution 45 min
- Write both sizing figures down first — about 1.7 TB of records against roughly 150 MB of keys — and state which one bounds the memory and which one bounds the I/O plan.
- Build a 2-million-record fixture across four partitions with 2 KB payloads and 5,000 distinct variants, so the stride between winners matches production at one in 400; plant out-of-order versions, one duplicate
(variant_id, version)pair, and one variant whose winning event makes it unsellable. - Implement the offset fold, confirm the winning version per variant matches a brute-force max computed with unlimited memory, and confirm the duplicate is reported rather than silently resolved.
- Implement both pass-two strategies — offset-sorted random reads and a full sequential re-scan — time them with the page cache dropped between runs, and compare the measured ratio against the stride-over-breakeven figure you predicted.
- Rebuild the fixture with 64-byte records, leaving the key count alone, and confirm the comparison flips to favour the re-scan.
- Run the reconciliation and confirm the three emitted sets are disjoint and jointly account for every difference against the index snapshot.
Follow-up
- A partition file is truncated mid-record. What does the fold do, and what must it not do?
- The repair job and the live indexer both write while the fold runs. What makes the outcome decidable?
- Distinct variants grow to 200 million. Which of your two passes changes first, and into what?
Rank the most contended variants in a bounded-memory stream
A drop produces a stream of reservation attempts, each (timestamp, variant_id, outcome) with outcome granted or rejected, arriving in timestamp order at up to 200,000 events per second. The catalogue holds two million variants and one variant can take over a thousand attempts per second. Using fixed memory of m counters, with m = 10,000 and far below the distinct-key count, emit the top 50 variants by rejected attempts for each 10-second bucket. Name the summary you use and give the bound that summary actually provides, including its direction — not a claim of approximate correctness.
Approach
- Separate the two problems first. Exact top-k when the distinct keys fit memory is a hash map plus a size-50 min-heap at O(n log 50) time and O(distinct) space — two million keys at roughly 48 bytes is about 100 MB. Say that plainly, so it is clear the bounded-memory constraint rather than the data is what forces a sketch.
- Commit to one summary and quote the rule and the bound that belong to it. Misra-Gries with m counters: increment on a hit; on a miss with a counter free, store the arriving key at count 1; on a miss with all m occupied, decrement every counter by one, drop the zeros, and discard the arriving key. Each such round accounts for m+1 occurrences of the stream — m decrements plus the discarded arrival — so there are at most N/(m+1) rounds over a bucket of N events. Therefore every stored count is at most the true count and low by at most N/(m+1), and every key whose true count exceeds N/(m+1) is still present with a positive counter.
- Space-Saving is a different algorithm with the opposite error direction, not another name for the same one. On a miss with all m occupied it evicts the minimum-count entry and inserts the arriving key at count min + 1, keeping that min as the entry's error term. Since min is at most N/m, every reported count is at least the true count and at most N/m above it. Substituting one for the other flips the sign of the error and invalidates any assertion written as reported-at-most-exact, so pick one, state its bound, and write the test that belongs to that bound.
- Put the numbers in: a 10-second bucket at 200,000 events per second is N = 2x10^6, so Misra-Gries at m = 10,000 is low by at most 199 per key (N/(m+1) = 199.98), and Space-Saving at the same m is high by at most 200 (N/m). A variant taking 1,000 attempts per second contributes 10,000 in the bucket and clears either bound fifty times over, while two keys separated by less than the bound are not reliably rankable against each other — stating that boundary is part of the answer, not a hedge.
- Feed the summary into a size-50 min-heap at bucket close rather than maintaining the heap continuously. The summary holds at most m entries, so that pass is O(m log 50) once per bucket, negligible against the 2x10^6 counter updates already performed.
- Reset counters at each boundary and name the cost: an item straddling a boundary is split across two summaries, so a burst centred on a boundary can miss the top 50 in both. Two offset bucket series or exponentially-decayed counts fix it, and the decayed variant no longer carries the clean Misra-Gries bound — which is the trade you are making, not a detail.
Follow-up
- You need exact counts for the 50 you reported. What does a second pass cost, and when can you not have one?
- Attempts arrive up to two seconds late. Does that break the bound, or only the bucket assignment?
- Operations wants the top 50 by rejected units rather than attempts. Which guarantees survive weighting?
Add a partial unique index to a live reservation table
inventory_reservation holds 400 million rows and takes 2,000 writes per second. You must add UNIQUE (owner_type, owner_id, variant_id, node_id) WHERE state IN ('held','committed') so a double-submitted checkout cannot create two live holds, and separately make a newly added column NOT NULL. Duplicates already exist in the table. Give the ordered plan with the exact statements, state which lock each one takes, say what happens if the concurrent build fails halfway, and say why the application change ships before the index rather than after.
Approach
- Ship the application guard first, and be able to say why. CREATE UNIQUE INDEX CONCURRENTLY validates rows inserted while it is running, so a duplicate created mid-build fails the build after it has already done most of its work. Closing the window in code — the reserve path looking for an existing live hold and returning it — is what makes the build succeed on the first attempt instead of the third.
- Resolve the existing duplicates before building: GROUP BY owner_type, owner_id, variant_id, node_id with state IN ('held','committed') HAVING count(*) > 1, then release all but one through the normal release path. Not a blind UPDATE — releasing units without decrementing reserved_units leaves the counter permanently high, which is exactly the drift the reconciliation job then has to explain to somebody.
- Build concurrently and know the failure mode precisely. CREATE UNIQUE INDEX CONCURRENTLY takes SHARE UPDATE EXCLUSIVE rather than ACCESS EXCLUSIVE, cannot run inside a transaction block, and makes two passes over the table. On failure it leaves an invalid index behind that is ignored by queries but still maintained on every write, and a unique one still enforces uniqueness against newly inserted rows; find it with pg_index.indisvalid = false and remove it with DROP INDEX CONCURRENTLY before retrying.
- Be precise about promoting the index to a named constraint: ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX takes ACCESS EXCLUSIVE but only briefly because the index already exists — and it refuses a partial index. On this table the partial index is the enforcement mechanism, and there is no constraint object to attach.
- Get NOT NULL without a full-table scan under ACCESS EXCLUSIVE. ADD COLUMN with a constant DEFAULT is metadata-only from PostgreSQL 11; backfill in batches keyed on the primary key with a commit per batch; then ADD CONSTRAINT chk CHECK (col IS NOT NULL) NOT VALID, VALIDATE CONSTRAINT under SHARE UPDATE EXCLUSIVE, then SET NOT NULL, which from PostgreSQL 12 uses the validated check and skips the scan.
- Wrap every ALTER in SET lock_timeout = '2s' with a retry loop, because ACCESS EXCLUSIVE queues behind any open transaction on the table and every statement arriving afterwards queues behind it — so one long-running report turns a metadata-only change into a full stall on the write path. Between backfill batches, watch replication lag and dead tuple accumulation rather than only wall-clock progress.
Follow-up
- The build fails at 2am with a duplicate-key error naming a reservation created ten minutes earlier. What does that tell you about step one, and what do you do before retrying?
- Roll the whole thing back. What is the reverse of each step, and which one is not reversible?
- The same constraint is wanted on a table range-partitioned by created_at_utc. What changes, and which columns must now be in the key?
Retire a variant without breaking historical order lines
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.
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?
How do you approach designing an application that needs to handle real…
How do you approach designing an application that needs to handle real-time updates for logistics and dispatching?
Approach
- Separate accepted, pending, failed and confirmed; they are different facts.
- Design the error taxonomy before the success shape; callers branch on it.
- State how the contract changes without breaking existing clients.
Follow-up
- What does a partial failure look like to the caller?
- How does a client discover it is on an old version of this contract?
How do you ensure data accuracy when building tools that track physica…
How do you ensure data accuracy when building tools that track physical product movement or inventory?
Approach
- Design the error taxonomy before the success shape; callers branch on it.
- Say who the caller is and what they do when the call fails halfway.
- Separate accepted, pending, failed and confirmed; they are different facts.
Follow-up
- How does a client discover it is on an old version of this contract?
- What happens if the caller retries after a timeout?
What is your process for integrating third-party APIs with internal wa…
What is your process for integrating third-party APIs with internal warehouse management systems?
Approach
- Separate accepted, pending, failed and confirmed; they are different facts.
- Say who the caller is and what they do when the call fails halfway.
- Define the identity of a request so a retry cannot double-apply it.
Follow-up
- What does a partial failure look like to the caller?
- How does a client discover it is on an old version of this contract?
Make a checkout endpoint idempotent from submit to capture
A storefront calls POST /checkout with a cart id, a signed price quote id and a client-minted idempotency key. Double-clicks, mobile network retries and gateway timeouts all replay the identical request, sometimes after the first attempt has already committed. Using customer_order (idempotency_key VARCHAR(128) UNIQUE, price_quote_id, state, grand_total_minor), design the handler so that exactly one order exists per logical checkout. Specify the write sequence, what the second caller receives while the first is still in flight, and what happens when the same key arrives with a different cart.
Approach
- Fix the key's scope and lifetime before anything else: it is minted once when the cart page renders the checkout button, not once per HTTP attempt, or the retry carries a fresh key and the whole mechanism is decorative. Store a request_fingerprint (a hash over the canonicalised body: cart id, line quantities, quote id, total) next to it so a replay with an altered body is distinguishable from a genuine retry.
- Replace SELECT-then-INSERT with INSERT ... ON CONFLICT (idempotency_key) DO NOTHING, then re-read by key. The existence check is a race that no isolation level below SERIALIZABLE closes, because both transactions can observe the absence before either writes; the unique index closes it because the losing INSERT blocks on the index tuple until the winner commits. The precondition to state: this works at READ COMMITTED, where each statement takes a fresh snapshot so the re-read sees the row the winner just committed. At REPEATABLE READ the transaction's snapshot is fixed at its first statement, so DO NOTHING skips the insert while the re-read cannot see the row that caused the conflict, and the handler concludes nothing exists.
- Define three responses rather than two. Fresh insert returns 201 with the new order. Conflict with a matching fingerprint replays the stored response body, so a retry after a lost response is indistinguishable from the original. Conflict with a different fingerprint returns 409, never the first order, because the customer who edited their cart must not be charged for the old one.
- Order the side effects around the gate, never in place of it. The conditional INSERT commits first, in its own short transaction, writing state 'pending' and the quote's grand_total_minor; that is the handler's first action and the only thing that makes the key exclusive. Only the caller that won the insert proceeds, and it then runs the side effects reversible-first: reserve inventory (releasable), authorize payment (voidable), capture (irreversible), advancing state by compare-and-set after each so a crash leaves a row recording how far it got. Persist the payment intent keyed by order id before the provider call, so a crash mid-call leaves a resolvable record rather than an authorization no row points at. Inverting this and authorizing before the INSERT means two concurrent first attempts both reach the provider before either touches the unique index, and the index then rejects one order after both authorizations already exist.
- Accept the cost of gating first: a 'pending' row is a gate, not an order. Rows that never leave 'pending' past the quote expiry are swept, and every downstream reader — fulfilment, finance, the customer's order list — filters on state at or beyond 'authorized', or abandoned checkouts surface as orders and inflate every count derived from the table.
- Handle the in-flight case from state rather than from lock waits. Because the winner commits the gate row before calling the provider, a duplicate's INSERT conflicts within milliseconds and the re-read finds state 'pending' or 'authorizing' with no stored response body, which is a 409 'in progress' rather than a replay. The only lock wait left is the narrow window in which the duplicate arrives before the winner's insert commits; bound it with a statement timeout and return the same 409, so a burst of double-clicks never parks connections for the length of a payment call.
Worked solution 25 min
- Write the DDL: the unique index on idempotency_key and a request_fingerprint CHAR(64) NOT NULL column.
- Implement the handler as INSERT ... ON CONFLICT DO NOTHING followed by a re-read, and return the three outcomes from the re-read plus a fingerprint comparison.
- Commit the gate row before the first side effect, then stub the authorization to sleep 300ms and confirm two concurrent first attempts produce exactly one provider call.
- Run two concurrent identical requests against the same key and confirm one 201 and one 200 with the same order_id.
- Run the same key with a changed line quantity and confirm 409 rather than a replay.
- Repeat the concurrency test under REPEATABLE READ and observe the re-read returning no row, so the isolation-level precondition is a measured fact rather than a claim.
Follow-up
- The customer edits the cart and resubmits. What tells the client to mint a new key, and where does that decision live?
- The gate row commits and the process dies during the provider call. What finds that row later, and how does it decide whether an authorization exists?
- Does the stored response include the payment outcome, and what do you return when the first attempt is still waiting on the provider?
Duplicate authorizations after a payment provider slowdown
A payment provider's p99 went from 400 ms to 9 s for forty minutes; the client timeout is 5 s. Afterwards payment_transaction holds 1,900 rows in state 'unknown', several orders carry two rows of kind 'authorization' with different idempotency_key values, and later captures fail as expired or not found. Customers report two pending amounts. Using the schema's idempotency_key, request_fingerprint, psp_reference and auth_expires_at_utc, find the defect, give the ordered recovery for the existing rows, and the change that prevents a repeat.
Approach
- Quantify before theorising, then compare the keys. Group payment_transaction by (order_id, kind) within the incident window keeping counts above one, and separately group by state, which distinguishes 'we issued duplicates' from 'we have unresolved attempts'. Then compare idempotency_key and request_fingerprint across the sibling rows: identical fingerprints with different keys is the whole diagnosis, because the retry presented itself to the provider as a new operation and the provider did exactly what it was asked.
- Locate where the key is minted and when it is persisted. Minted per HTTP attempt rather than per logical checkout is the first half; written after the call rather than before is the second, because a crash before the write loses the key and the next attempt cannot reuse it even if the code wanted to.
- Inspect the return type of the client, because a disciplined caller cannot rescue a lossy signature. If authorize returns a boolean or raises on timeout, 'unknown' has nowhere to live and collapses into failure, which makes the retry not just likely but correct-looking. The outcome type needs three values and the timeout branch must be distinguishable at the call site.
- Recover by resolving, never by re-issuing. For each 'unknown' row, query the provider by the stored idempotency key or psp_reference and set the row to succeeded or failed with the returned reference. Issuing any new call to discover the outcome is how one duplicate becomes two.
- Remediate the duplicates in cost order, because the compensations are wildly asymmetric. Void the redundant authorization while it is still valid — a void is free and releases the customer's held funds immediately; after auth_expires_at_utc it lapses on its own but the hold is visible to the customer until then; if the duplicate was captured the only remaining compensation is a refund, which carries fees and takes days.
- Prevent the repeat with four specific changes — retry with the same key, replace synchronous fan-out with a bounded queue and backoff so a slow provider produces queue depth rather than timeouts, add a circuit breaker that sheds instead of piling up, and run the reconciliation sweeper off the partial index on (state, requested_at_utc) where state is 'pending' or 'unknown', which is safe beside the original caller because it resolves by query and writes with a compare-and-set on state. Handle the capture failures as a separate consequence rather than folding them in: authorization validity is finite and varies by scheme, method and acquirer, so authorizations that lapsed during the forty minutes need re-authorization, and retrying a capture against a lapsed authorization fails every time however often it runs.
Follow-up
- A split shipment captures twice against one authorization. What is the key scoped to, and how does the sum stay within the authorized amount?
- A replay arrives with the same idempotency key but a different request_fingerprint. What is the correct response, and what breaks for the customer if you return the original result instead?
- The sweeper and the original caller resolve the same 'unknown' row concurrently. Show why the outcome is still one definite state.
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays 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.
Practice prompt ↗Practice prompt ↗03Sorting, 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.
Practice prompt ↗Practice prompt ↗04Recursion, 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, 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.
Practice prompt ↗Practice prompt ↗06One 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.
Practice prompt ↗Practice prompt ↗07Solve 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.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
For anything that touched live traffic, be ready to say how you would have undone it: a flag, a staged rollout, dual writes with the old path still authoritative. Once the old column is dropped or the source rows are overwritten there is no reverse, so name what you kept a copy of and for how long.
Describe a time you had to pivot your technical strategy due to changi…
Describe a time you had to pivot your technical strategy due to changing business requirements.
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Can you describe a time you had to troubleshoot a technical issue that…
Can you describe a time you had to troubleshoot a technical issue that was impacting an operational workflow?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
Tell me about a complex technical challenge you solved and the steps y…
Tell me about a complex technical challenge you solved and the steps you took to reach the solution.
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
- 01
Describe a time you had to pivot your technical strategy due to changing business requirements.
- 02
Can you describe a time you had to troubleshoot a technical issue that was impacting an operational workflow?
- 03
Tell me about a complex technical challenge you solved and the steps you took to reach the solution.
Is this an official Midwest Foods interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Midwest Foods. Rounds and questions reflect what candidates have reported, not a process Midwest Foods has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the typical interview process take?
The timeline varies, but most candidates move through the stages within a few weeks. Midwest Foods prioritizes clear communication and keeps candidates updated on their status throughout the process.
PracHub interview research ↗What is the company culture like?
Midwest Foods is a family-owned business that values hard work, integrity, and operational excellence. The company describes a culture where everyone's contribution is visible and essential to its success.
PracHub interview research ↗Should I prepare for whiteboard coding?
Expect a mix of practical coding exercises and design discussions. Focus on demonstrating your thought process and how you handle edge cases rather than just arriving at the perfect syntax.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-22