Interview guides · Kruger
Kruger Software Engineer Interview Questions & Guide 2026
Software engineer interview prep for Kruger: original coding, system design and debugging practice, each with a worked solution.
The 60-second brief
At Kruger, the Software Engineer role is not merely a desk-bound position; it is a critical function that bridges the gap between complex industrial operations and modern digital efficiency. You will be responsible for designing, implementing, and maintaining systems that ensure the seamless operation of Kruger’s diverse manufacturing and supply chain infrastructure. Whether optimizing production workflows or enhancing internal system reliability, your work directly impacts the company’s ability to remain a leader in the forest products and renewable energy sectors.
Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.
Focus areas
- Model units, lots and state machines precisely
- Keep a site operating during a network partition
- Fold late, out-of-order scan events by event time
Overview
This role is uniquely challenging because it requires a blend of traditional engineering discipline and modern software agility. You will often find yourself working in environments where your code or system architecture interacts with physical machinery and large-scale supply chain logistics. The complexity of these systems means that your contributions are vital to maintaining operational uptime and safety across various sites. If you enjoy solving high-stakes problems where digital solutions have tangible, real-world consequences, this position offers a compelling platform for your professional growth.
The Kruger interview process
Kruger candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
HR 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
Manager Interview
Part of what is being decided is what your manager's week looks like once you are on the team: how they find out that something you shipped is broken, and whether they hear it from you or from a customer. The material that moves this round is therefore not the launch, it is the week after. Describe how you knew the change was working, which number you watched and for how long, and what you would have reverted to. A project that ends at the word shipped leaves the manager guessing about the part they care about most.
What to demonstrate
- How a change of yours was verified in production, and whether that check existed before the deploy or was assembled afterwards once something looked wrong
- Whether the rollback story is specific, including the case where a revert is not enough: once a migration has dropped a column or rewritten data, going back is its own change with its own risk
- Whether you can describe an incident you caused, how it was found, and how much time passed between it starting and anyone noticing
- Whether bad news in your account travels early and from you, or consistently arrives via somebody else
How to prepare
- For your last significant change, write down what you watched after the deploy, for how long, and the number that would have made you revert. If nothing was watched, say that plainly instead of inventing a dashboard.
- Work out the rollback answer for a change that touched stored data, and be able to say what made it reversible or what you would have had to do instead. That distinction is a level signal on its own.
- Write the two-minute version of an incident you owned: what broke, how it surfaced, what you did in the first ten minutes, and the change that stopped it recurring. Get the timeline straight, because this is the story most likely to be interrupted with questions.
Behavioral Discussions
Many of these questions are about something that went wrong, and the grading sits mostly in the hours after you knew. Who found out first, whether that was you or an alert or a user, how long it took you to say it out loud, and whether the people who needed the news got it while they could still act on it. Engineers under-tell this part because it feels like confessing. The pattern it is looking for is the opposite: the quiet fix, an incident absorbed without telling anyone, after which nothing changed and the same failure is still available.
What to demonstrate
- How the problem was found, and whether that route was one you had built or one that happened to you, since a user reporting it first means your instrumentation did not cover that failure
- Whether time-to-detect and time-to-tell are separate numbers in your account and whether you know both, because a fast fix that nobody heard about until the retro is a different answer from a slow one that was announced immediately
- Whether the resolution left something durable behind, a check that fires or a default that changed, rather than depending on people remembering to be careful
- Whether you can say what the failure cost without either inflating it or waving it away
How to prepare
- Reconstruct one incident you were part of as a timeline with clock times: first bad request, first signal, first person who knew, first message outside the team, mitigation, permanent fix. The gaps between those entries are what gets asked about
- Look up the configuration of the signal that caught it, including its evaluation window and threshold. An alert defined on a five-minute aggregate cannot fire until the condition holds across that window, which puts a floor under time-to-detect that has nothing to do with how severe the failure was. Be able to say what that floor was and whether anyone had chosen it deliberately
- Prepare one story where you escalated early and the severity turned out to be smaller than you thought, including what it cost the people you pulled in. Without it, every answer you give about raising alarms is unfalsifiable
External 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.
Where candidates lose points
Holding a database transaction open across a physical or third-party operation
It is natural to open a transaction, lock the position, call the rating or tendering API, and commit on the response, and it works perfectly until the partner's p99 goes from 200 milliseconds to thirty seconds. At that point every request holding a lock on a hot item-node pair queues behind it, the connection pool fills with transactions that are waiting on the network rather than on the database, and an unrelated service sharing the pool fails at the same moment. In this domain the effect is amplified because demand concentrates on a few hot rows during a promotion or a seasonal peak, exactly when partner latency is also degraded. The structural fix is to keep transactions short and local - commit the state change together with an outbox row, let a relay perform the external call, and reconcile asynchronously - accepting at-least-once delivery and making the effect idempotent rather than trying to stretch a transaction over something the database cannot roll back.
Retrying an irreversible external call without an owned idempotency key
Timeouts and connection resets are ambiguous by construction - the partner may have committed before the response was lost - so a generic retry policy around an HTTP client is, in this domain, a machine for buying two labels and dispatching two trucks. Relying on the partner's deduplication is not a substitute, because their window is usually short, their key is often derived from fields you may legitimately change on retry, and many older integrations have no such concept at all. The workable pattern is to generate the key yourself, persist it with an explicit unknown status before the call, and resolve ambiguity by querying the partner for that key rather than re-issuing; the sweeper that does this is the component that has to be correct, not the call site. Teams get this right for payments and then forget that a carrier tender, a warehouse work release and an EDI shipping notice have the same shape.
Retrying a write that is not safe to repeat
A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Questions & practice
Parse partner quantity and date segments without floating point
Easy · parsing · unit-of-measure · time zones
A partner sends a delimited flat file whose segments look like QTY+17:4.500:CS and DTM+232:20260317. Quantities carry up to three decimal places in a named unit; each item has an integer pack factor to eaches that is versioned by effective date. Produce observation_event rows with an integer quantity in eaches, the unit recorded, and occurred_at resolved, rejecting any line whose conversion is not exact. Files reach 4 GB. Single pass, no floating point. State your bounds and what you do with the zoneless date.
Approach
- Scan with indices, not a regex. Find the segment terminator, then split the payload on
:by scanning forward and slicing — O(bytes) with no backtracking and no per-line allocation beyond the output row. A regex with nested quantifiers over 4 GB is where this job silently becomes CPU-bound. - Parse the decimal as an integer mantissa plus a scale.
4.500becomes(mantissa = 4500, scale = 3): accumulate digits into an int64, count digits after the point, and never call the runtime's float parser. Reject a mantissa that would overflow int64 when multiplied by the pack factor, checking before the multiply rather than after. - Convert by integer arithmetic and demand exactness:
numerator = mantissa * factor; emitnumerator / 10^scaleonly whennumerator % 10^scale == 0, otherwise reject the line with a reason code. Worked:4.500 CSat 12 eaches per case gives 54000/1000 = 54, exact.0.125 PLat 36 eaches per pallet gives 4500/1000 = 4.5, which is half a sealed unit and must fail at the boundary, not round three services away. - Select the pack factor by the file's effective date, not by the current item master. Factors are properties of a point in time — a supplier moving a case from 12 to 10 must not reinterpret last quarter's file — so the lookup is a range query on the factor's validity interval.
20260317carries no zone and no time. Attach the IANA zone of the node the segment refers to, captured at ingestion, because it cannot be recovered downstream; then pick and document one convention for a date-only field (start of local day is the usual choice) and store the raw local string beside the resolved instant so the assumption stays visible. Complexity: O(total bytes) time, O(longest line) working space, one pass.
Follow-up
- The same file is re-sent in full tomorrow with three lines changed. What makes the second ingest a no-op for the unchanged lines?
- A line names a unit that is not in the ladder for that item. Reject the line, reject the file, or quarantine it — and who finds out?
- The partner starts sending an offset like
+0100on some dates. Does that change your storage decision for a future appointment as well as a past event?
Split an order across the fewest nodes that can fill it
Hard · set cover · bitmask dp · approximation
A fulfilment order has L lines, L up to 25. After filtering by capability, service time and stock, N candidate nodes remain; N is usually under 14 but can reach 200. Each node covers a known subset of the lines in full. Return a minimum-size set of nodes covering every line, tie-broken by total distance. Give an exact algorithm for the small case with its exact cost in operations and bytes, say precisely why enumerating node subsets stops working, and give what you ship when N is 200.
Approach
- Name it: this is minimum-cardinality set cover, which is NP-hard, so the useful move is choosing which parameter you are exponential in rather than hunting for a polynomial exact algorithm. Enumerating subsets of nodes is 2^N and correct — at N = 14 that is 16,384 subsets and finishes instantly, which is exactly why it survives review and then fails in production at N = 200, where 2^200 is about 1.6e60.
- Be exponential in L instead, because L is the parameter you bound. Index the DP by the set of lines still uncovered:
dp[rem]is the cheapest way to cover the lines inrem, withdp[0] = (0, 0)and the value a(node_count, total_distance)pair compared lexicographically. Letbbe the lowest set bit ofrem; any cover must contain some node that stocks lineb, so the transition ranges only over those nodes:dp[rem] = min over v with b in cover(v) of dp[rem & ~cover(v)] + (1, dist(v)). Worst case O(2^L * N) — at L = 25 and N = 14 that is 33,554,432 states times 14, about 4.7e8 transitions — and the lowest-bit restriction cuts the real branching factor to the nodes stocking one specific line. - Quote the memory for what you actually store, not for a count array alone. Over 2^25 masks you need three dense arrays:
uint8node count (33.5 MB),uint32total distance (134.2 MB, integer metres so the tie-break compares exactly and the array stays fixed-width), anduint8chosen node (33.5 MB, since N <= 200 fits a byte) — about 201 MB, roughly six times the 33.5 MB that a bare count array would suggest. The same layout at L = 20 is 6.3 MB, which is why this is comfortable up to about 20 lines and needs a deliberate decision at 25. - Reconstruction is free in this formulation and needs no parent-mask array: because every transition eliminates the lowest uncovered line, the predecessor of
remunder the stored choicevis exactlyrem & ~cover(v), so walkremfrom the full mask down to 0 emittingchoice[rem]. If the reachable state count is far below 2^L — coverage sets are usually highly correlated — a top-down memoized recursion over a hash map trades the 201 MB for roughly 40-60 bytes per visited state; measure the visited count first, because a hash map that ends up touching most masks is strictly worse than the dense arrays. Add a branch-and-bound cut ofceil(popcount(rem) / max_v popcount(cover(v) & rem)), which prunes long before the theoretical bound bites. - When exact is out of reach, ship greedy with a stated guarantee: repeatedly take the node covering the most currently-uncovered lines, using bitset popcounts, O(N * ceil(L/64)) word operations per pick and at most min(N, L) picks. Greedy is within H(L) <= ln L + 1 of optimal (Chvatal), and no polynomial algorithm achieves (1 - o(1)) ln L unless P = NP (Dinur and Steurer, 2014) — so effort belongs in the pre-filter and the tie-break, not in a cleverer heuristic. At L = 25 the bound is H(25), about 3.82x worst case, while the realistic gap on correlated coverage sets is zero or one node.
- Close on the objective, because fewest nodes is not cheapest. Two shipments from distant nodes routinely cost more than three from near ones, and a split also costs a second box, a second carrier pickup and a worse customer experience. If cost is the real objective it is weighted set cover — greedy by cost divided by newly-covered lines, same logarithmic bound — and if a node can cover a line only partially, the line mask is no longer sufficient state, since you must track remaining quantity per line; that is the point at which you stop hand-rolling and either restrict splits or hand an MILP to a solver with a time budget.
Worked solution (40 min)
- Write
cover(v)as an L-bit mask per node and confirm that the union of all masks equals the full mask, otherwise the order is unfillable and the answer is a backorder, not a cover. - Implement the DP over uncovered masks with the lowest-uncovered-line transition, the
(node_count, total_distance)value and thechoicearray, then measure resident memory and wall time at L = 20 and L = 25 against the quoted 6.3 MB and 201 MB. - Implement greedy separately and run both on 10,000 random instances with L = 12, N = 10, comparing sizes.
- Construct the textbook instance where greedy is strictly worse than optimal — nested sets of geometrically decreasing size plus two disjoint halves — and confirm your greedy reproduces the gap.
- Add the distance tie-break to both and confirm the exact path returns the cheapest minimum-size cover, not merely a minimum-size one.
Expected result: The DP and greedy agree on the large majority of random instances; on the adversarial instance greedy returns more nodes than the DP, by a margin consistent with the H(L) bound; the DP at L = 25 holds about 201 MB across its three arrays and runs in seconds rather than milliseconds, with cache misses over that working set dominating the 4.7e8 transitions.
Verify it yourself
- Measured resident memory at L = 25 is near the quoted 201 MB — if it comes out near 33 MB you have dropped either the distance component or the reconstruction array, and the reported figure is describing a program you did not write.
- Every returned set actually covers the full mask — assert it on the result, not just on the transitions.
- Reconstruction from
choicealone reproduces a set whose node count and total distance equaldp[full], which is what proves the predecessor identityrem & ~cover(v)holds. - The DP's answer never exceeds greedy's on any instance.
- Doubling N multiplies DP runtime by about two, while subset enumeration would square its work — measure it rather than asserting it.
Follow-up
- The pre-filter is what keeps N small. What is in it, and what happens to your exact path the day someone loosens it?
- One node can cover a line only partially. Show where the bitmask formulation breaks and what state replaces it.
- You have 40 milliseconds inside a checkout call. Which of these runs there, and what runs asynchronously afterwards?
Size dock doors from overlapping appointment windows
Medium · sweep line · intervals · time zones
For one node and one local calendar day you have up to 200,000 planned dock appointments from shipment_leg: leg_id, planned_arrive_at, planned_depart_at (both TIMESTAMPTZ), plus the node's IANA zone. A door is occupied over [arrive, depart). Return the minimum number of doors that lets every appointment start on time, and the maximal interval over which that peak is sustained. Target O(n log n). Say how you derive the day's boundaries and how you break ties between an arrival and a departure at the same instant.
Approach
- Name the result before computing it: with one interchangeable door type, the minimum door count equals the maximum number of simultaneously occupied intervals. That equality is not a heuristic — interval graphs are perfect, so their chromatic number equals their clique number, and greedy left-to-right assignment achieves it. Say this, because it is what licenses solving a scheduling question with a counter.
- Emit 2n endpoints,
(t, +1)at each arrival and(t, -1)at each departure, sort byt, and at equaltorder the-1before the+1. Half-open occupancy means a trailer leaving at 10:00 frees the door for one arriving at 10:00; ordering arrivals first inflates the answer by exactly the number of back-to-back handoffs, which on a well-packed schedule is most of them. - Scan once with a running counter, tracking the maximum and the endpoint index where it was first reached. The peak is attained on a half-open interval between two consecutive endpoints,
[t_i, t_{i+1}), not at an instant — report it that way or the operations team cannot act on it. Extend the interval while the counter stays at the maximum. - Complexity: O(n log n) dominated by the sort, O(n) space. If rows already arrive ordered by
planned_arrive_atfrom an index, use the min-heap variant instead — push each departure, pop all departures at or before the current arrival, and the heap size is the current occupancy. Same time bound, O(peak) space rather than O(n), which matters when peak occupancy is 40 and n is 200,000. - Derive the day's boundaries from the zone, not from arithmetic on instants. Convert local midnight and the next local midnight to instants in the node's IANA zone; on a transition day that span is 23 or 25 hours, so adding 86,400 seconds silently drops or duplicates an hour of appointments. Decide explicitly what a zero-length dwell means — under half-open semantics it occupies nothing — and state it.
Follow-up
- Doors are typed: some break pallets, some are parcel-only. Does max-overlap still equal the door count?
- Appointments have a tolerance — a trailer may start up to 20 minutes late without penalty. How does that change the objective, and is it still solvable by a sweep?
- Half the appointments are actuals and half are plans. Which do you sweep for tomorrow's staffing, and which for last week's utilisation report?
Fold late, out-of-order scan events into one status per leg
Medium · window functions · event time · monotonic fold
observation_event holds event_id, subject_type, subject_id, observed_status, occurred_at, received_at, dedupe_key (UNIQUE) and processing_status. A lookup table maps observed_status to status_rank SMALLINT along a progress lattice. Write one query returning the current status of every shipment leg that received an event in the last hour, such that a picked_up event arriving after a delivered event never changes the answer. Then explain exactly which part of your ordering makes a shuffled replay of the same batch produce identical output.
Approach
- Separate the two jobs. Deduplication is the UNIQUE on dedupe_key at ingest, because partner feeds resend whole windows; ordering is the fold. Either alone leaves a defect: dedupe without a lattice still lets a stale event win, and a lattice without dedupe double-counts a replayed window in anything that aggregates.
- Select the affected legs in one CTE by received_at >= now() - interval '1 hour', then fold over every event for those legs, not only the recent ones. Restricting the fold's input to the recent window is the failure: a lone late picked_up would be the only row considered and the leg would regress.
- Fold as a maximum over the lattice: SELECT DISTINCT ON (subject_id) subject_id, observed_status FROM ... ORDER BY subject_id, status_rank DESC, occurred_at DESC, event_id DESC. The equivalent form is ROW_NUMBER() OVER (PARTITION BY subject_id ORDER BY status_rank DESC, occurred_at DESC, event_id DESC) = 1.
- The determinism comes from the final event_id tiebreak. Rank and occurred_at alone leave two equal-rank events at the same instant to be resolved by heap order, which changes when rows are re-inserted, so the shuffled replay differs in exactly the cases nobody tests. With a total order over the tuple the fold is a max, which is commutative and associative and therefore order-insensitive.
- Do not advance a leg on occurred_at alone. The timestamp is device-reported and can be wrong, which is why the lattice rank leads the ORDER BY and why clock_offset_ms is worth storing: a future-dated event with a low rank then cannot promote itself past a delivered.
- Keep superseded rows and mark processing_status = 'superseded' rather than deleting them. The discarded event is usually the one that explains a delivery dispute, and deleting it makes the trace unanswerable.
Follow-up
- An event arrives a week late for a leg that is already invoiced. Does the fold apply it, reject it as late, or apply it and emit a correction?
- The stream is partitioned by source_id rather than by subject_id. What ordering guarantee do you actually have, and what breaks?
- How would you materialise this fold so the customer-facing status read is not running a window function per request?
Model the movement ledger so a retried write is a no-op
Easy · append-only ledger · idempotency · audit history
inventory_movement is append-only: movement_id, item_id, node_id, lot_id, state, delta_qty (signed BIGINT), uom_code, reason_code, ref_type, ref_id, idempotency_key, occurred_at, recorded_at, reversed_by. inventory_position is keyed (item_id, node_id, lot_id, state) with qty, version, last_movement_id. Give the DDL constraints and the exact statements a pick confirmation runs so that a client which times out after the commit and retries changes nothing. Then say how you correct a pick recorded against the wrong lot two days ago, and why the position carries CHECK (qty >= 0) rather than clamping at zero.
Approach
- Put the guarantee in the schema, not the handler: UNIQUE (idempotency_key) on inventory_movement. A SELECT-then-INSERT cannot work, because two concurrent retries both read nothing before either commits and both then insert.
- Claim with INSERT ... ON CONFLICT (idempotency_key) DO NOTHING RETURNING movement_id, and branch on rowcount. Zero rows means the first attempt already committed, so the handler returns success without touching the position. This branch is the whole point: applying the position update on the conflict path is what makes the ledger and the shelf disagree by one pick forever.
- Keep the insert and the position update in one transaction: UPDATE inventory_position SET qty = qty + :delta, version = version + 1, last_movement_id = :id WHERE item_id = :i AND node_id = :n AND lot_id = :l AND state = :s AND version = :v, then test the rowcount and retry the transaction on zero.
- Use lot_id BIGINT NOT NULL DEFAULT 0 rather than NULL. In a default unique index NULLs are distinct from each other, so a NULL lot would let two position rows exist for the same untracked item and split the running sum in half.
- Correct the mis-keyed pick with two compensating movements, never an UPDATE or a deleted_at flag: one reversal restoring the wrong lot, one fresh pick against the right lot, both with reason_code = 'reversal' or 'pick' and ref pointing at the original movement_id. The original row keeps its values; reversed_by is a back-pointer written in the same transaction so a reader does not need a reverse scan to see that the row was undone.
- CHECK (qty >= 0) aborts the transaction and surfaces a real defect. GREATEST(qty + :delta, 0) writes a plausible number instead, so the missing receipt that caused the negative is never investigated and the first symptom is a picker at an empty location.
Worked solution (20 min)
- Write the two CREATE TABLE statements including UNIQUE (idempotency_key), the composite primary key on the position, CHECK (qty >= 0) and lot_id NOT NULL DEFAULT 0.
- Write the handler as one transaction: the ON CONFLICT DO NOTHING RETURNING insert, the rowcount branch, then the versioned conditional UPDATE with its own rowcount test.
- Send the same pick twice with one idempotency key and assert one movement row and a position that moved by delta once, not twice.
- Apply the lot correction as two new movements and show that summing delta_qty per lot now matches the shelf while both the wrong and the right lot retain a legible history.
Expected result: One movement row per idempotency key, a position equal to the signed sum of its movements, and a corrected pick represented as three rows (original, reversal, re-pick) with no row ever updated except reversed_by and the position aggregate.
Verify it yourself
- SUM(delta_qty) per (item_id, node_id, lot_id, state) equals inventory_position.qty after the double-send and after the correction.
- Drop the UNIQUE constraint and confirm the double-send test now fails; if it still passes, the retry is not actually racing.
- Force a negative by replaying a pick against an empty position and confirm the transaction aborts rather than storing zero.
- Query the history of the mis-keyed pick and confirm the original quantity and its reason code are still readable.
Follow-up
- Two retries of the same pick arrive concurrently on different application instances. Walk through both transactions statement by statement and say which one commits a position change.
- occurred_at from a handheld is five minutes ahead of recorded_at because the device clock drifted. Which of your queries break, and which column should each of them actually be using?
- How long do you keep idempotency_key unique for, given the table grows by tens of millions of rows a month?
Allocate through a regional failover with unknown replication state
Hard · failover · replication lag · consistency · durability
The inventory primary sits in one region with an asynchronous standby in another, typically 2 seconds behind. The region becomes unreachable at peak, with 3,000 allocations and 3,000 ATP reads per second in flight. Decide whether to promote, what happens to allocations that may have committed without replicating, and what ATP answers meanwhile. Then quantify the cost of making the allocation write synchronous instead, given today's 4 ms transaction and about 42 allocations per second on the hottest item-node pair.
Approach
- Split the decision by operation class before touching the failover mechanics. ATP reads can move to the standby at once with a stated staleness, because a 2-second-old read only mis-advertises and the allocation write re-checks anyway. Allocations cannot: promoting an asynchronous standby discards up to 2 seconds of committed allocations, several thousand at this rate, and each one is a promise already made to a customer.
- Recover those from the requester's durable intent, not from the database that just vanished. If the orchestrator wrote its saga step and an outbox row in one local transaction before calling, every lost allocation can be re-driven after promotion under its original idempotency key, and allocation's partial unique index makes a re-drive that did replicate fail closed instead of double-claiming. Without that intent record the loss is simply unrecoverable, and the honest design refuses allocations until the primary returns rather than pretending otherwise.
- Price synchronous replication in the currency that actually binds, which is hot-row throughput rather than average latency. A cross-region synchronous commit adds tens of milliseconds inside the transaction, turning a 4 ms hold into roughly 65 ms and dropping one row from about 250 to about 15 allocations per second, below the 42 demanded - so the promotion tail becomes an outage every promotion. A synchronous standby in a second zone of the same region costs 1-2 ms, leaves the hold near 6 ms and the ceiling near 170 per second, still four times demand.
- So the configuration is: synchronous in-region for zero data loss on a node or zone failure, asynchronous cross-region for the regional case, and promotion of the cross-region standby as a deliberate human decision with a written procedure. Automatic promotion of an asynchronous standby is how a regional blip becomes two primaries and a permanently split ledger that reconciliation cannot repair, because both halves are internally consistent.
- Define the degraded contracts explicitly for the gap. ATP serves the conservative bucketed answer from the standby with staleness stated; allocate returns a typed unavailability so the orchestrator holds lines in sourcing rather than cancelling or retrying them into a storm. After promotion, replay the outbox, then run the reconciliation re-sum before any node's availability is trusted again, because the re-summed positions are the only evidence of what actually survived.
Worked solution (40 min)
- Classify every operation as safe or unsafe on a 2-second-stale replica and write that classification down before choosing a failover posture.
- Trace one allocation that committed a second before the outage through promotion, and name the exact durable record that lets you re-drive it.
- Recompute the hot-row ceiling under in-region and cross-region synchronous commit and set both against the 42 per second demand.
- Write the degraded contracts: what ATP returns, what allocate returns, and what the orchestrator does with each.
- Write the post-promotion sequence - outbox replay, reconciliation re-sum, then re-enable - and say what is not trusted until it completes.
Expected result: Reads promoted immediately with stated staleness and writes not; lost allocations recovered by re-driving from the orchestrator's durable intent under their original idempotency keys; synchronous commit placed in a second in-region zone (hold about 6 ms, ceiling near 170 per second against 42 demanded) rather than cross-region (about 65 ms, ceiling near 15); and promotion as a deliberate decision followed by outbox replay and a reconciliation re-sum before availability is trusted.
Verify it yourself
- Every allocation a customer was told about is either present after promotion or re-drivable from a record held outside the failed database.
- The cost of synchronous replication is expressed as a change in the hot-row ceiling, not only as added average latency.
- Nothing in the design permits the old primary and the promoted standby to accept allocations at the same time.
- The degraded ATP answer errs toward under-promising and says so explicitly.
Follow-up
- The old primary returns holding writes the standby never saw. What do you do with them, and what does the reconciliation job report about them?
- How do you rehearse this? Describe a game day that exercises the promotion path without risking an oversell in production.
- Which of the two regions' clocks do you trust for occurred_at ordering during the switch, and does it matter here?
Fold late, duplicated scan events into leg status
Medium · event time · partitioning · deduplication · late data
Observations arrive from handhelds, fixed readers and partner feeds: 9,000 per second average, 40,000 at peak. Partner feeds resend whole windows nightly and offline devices deliver six hours late. Each event carries dedupe_key, subject_id, observed_status, occurred_at and received_at. Design ingestion and the fold that maintains shipment_leg.status: the stream partition key, the deduplication mechanism, the ordering rule, and the handling of an event that is a week late or stamped in the future. Size observation_event's storage and partitioning.
Approach
- Separate the two defences, because each alone leaves a defect. Deduplicate on dedupe_key with a unique constraint or a keyed store, then fold with a monotonic status lattice. Deduplication alone still lets a stale picked_up overwrite delivered; monotonic folding alone still double-counts a resent window in any per-event aggregate such as dwell or scan productivity.
- Partition the stream by subject_id, the leg, so every event for one leg lands in one partition and is handled by one consumer. Broker ordering is per-partition only, so keying by source or device puts two events for one leg on different partitions to be processed concurrently, and the lattice would then be applied by two writers racing. Keying by leg spreads well because a single leg's event rate is physically bounded by scanning; the residual risk is a partition carrying several thousand legs of one large load, which is a throughput problem to absorb, not an ordering one.
- Write the fold as: order candidates by occurred_at, compare status_rank, take the maximum, never assign by arrival. Keep superseded events with processing_status superseded instead of discarding them, because the discarded event is usually the one that explains a dispute. Since occurred_at is device-reported and sometimes wrong, the rank lattice plus clock_offset_ms is what makes the rule safe - an event whose occurred_at is in the future beyond tolerance still folds on its rank but is flagged and not trusted for timing.
- Set the lateness policy in hours, not seconds. Hold a per-partition watermark at received_at minus the allowance; events behind it are still applied to the leg, because the lattice makes that safe, but any aggregate already published from them - on-time performance, dwell, carrier scorecards - must be restated rather than silently mutated, so a restatement path is part of the design. Beyond the retention horizon the event is stored with processing_status rejected_late and folded into nothing.
- Size the store from the average, not the peak: 9,000 per second is about 780 million rows per day, roughly 150 GB per day at 200 bytes. Daily range partitions on occurred_at give 30 partitions for a 30-day hot window rather than hundreds, which keeps planning time sane. A late event writes into an older partition, so partitions cannot be detached on age alone - they close only after the lateness allowance. Index (subject_id, occurred_at) for the fold and the trace; a block-range index on occurred_at degrades here because insert order stops correlating with event time.
Follow-up
- A partner replays a 24-hour window every night. What does that cost in the dedupe store, and how long must a dedupe key be retained to stay correct?
- On-time performance was published yesterday and a late pickup event arrives today. Restate it or leave it, and what does the consumer of that number need to know?
- Where does the fold's state live, and what happens when a consumer restarts in the middle of a partition?
Cut-off misses and on-time drift after a clock rule change
Medium · timezones · dst · event time
Two reports arrive the same week. On the Sunday one region changed its UTC offset, 1,900 lines at nodes in that region were released an hour after the node's 17:00 local shipping cut-off and missed the trailer. Separately, on-time percentage for legs whose dest_zone differs from the origin's reads four points below the carrier's own figure, every week. promised_ship_at is a TIMESTAMPTZ computed at order creation, and on-time compares the delivery instant to the promised day in UTC. Find the single root cause and give the storage and comparison rules you would adopt.
Approach
- Test whether each symptom is zone-dependent before theorising. Group the cut-off misses by node time zone and by date: clustering on one date and one set of zones points at an offset rule, while a uniform spread across zones and dates would point at a scheduler delay. Group the on-time disputes by whether the UTC day and the dest_zone local day of the delivery instant differ; if every dispute falls inside that group, the comparison is the defect. The containment runs one way only: a leg delivered days early also has differing UTC and local dates and is on time under either rule, so the differing-dates group is much larger than the disputed set and finding members of it that are not disputed proves nothing.
- Re-derive the stored promise for the affected lines today and diff it against the stored value. A difference of exactly 3,600 seconds on rows created before the rule change is the signature of a future local time that was resolved to an instant at write time using the offset then in force. The instant did not move; the local wall time it represents did.
- State the distinction the fix rests on. Comparing two instants needs no zone at all, so if the promise really were an instant, on-time would be a plain comparison. The zone matters because both of these promises are statements about local wall time: a 17:00 cut-off at a node, and delivery within the promised local day at the stop.
- Adopt the storage rule by tense. A past event is an instant, so occurred_at, recorded_at, actual_depart_at and actual_arrive_at stay TIMESTAMPTZ. A future local policy is local wall time plus an IANA zone identifier resolved at read time, so the cut-off becomes (17:00, node.time_zone) and the promise carries promise_zone, which the schema already has. Evaluate on-time in the dest_zone local calendar.
- Handle duration arithmetic separately, because it breaks in the other direction. A leg departing 18:00 local and arriving 18:00 local the next day spans 23 elapsed hours across a spring-forward boundary and 25 across a fall-back, so planned transit must be expressed in one domain and converted deliberately rather than by subtracting instants and assuming 24.
- Close the ingestion hole and the deployment hole. Partner feeds that send a bare date with no zone must have the zone attached at ingestion from the node or stop, because it cannot be recovered later, and the tzdata version baked into the image must be refreshed on a schedule, since a stale database reproduces exactly this bug for a rule that changed after the image was built.
Follow-up
- Not every offset change is a whole hour and some zones sit at :30 or :45. Which parts of your fix assume an hour?
- A node in a zone that does not observe daylight saving ships to one that does. Where does the transit duration have to be computed?
- How do you backfill on-time for the last two quarters without restating figures that were already invoiced?
Suggested practice checklist
Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.
Day 1 — Fix the scope and take a cold baseline
- Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
- Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
- Set the week's rule: one thing finished every night, including the night you only have forty minutes.
Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.
Day 2 — One pattern, written three times from blank
- Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
- On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
- Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.
Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.
Day 3 — One design, only to the depth you can defend
- Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
- Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
- Write the one tradeoff you are choosing against and the observation that would make you reverse it.
Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.
Day 4 — Only the fundamentals you will have to defend
- Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
- Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
- Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.
Deliverable: Three written answers, each under two hundred words and each timed aloud.
Day 5 — Your own work, timed
- Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
- Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
- Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.
Deliverable: Two timed narratives with one defensible number in the opening line.
Day 6 — The one full rehearsal, in the weekend block
- Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
- Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
- Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.
Deliverable: Mock notes naming three failure moments with a specific fix written under each.
Day 7 — Taper
- Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
- Re-read only your own notes from this week and open no new material.
- Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.
Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.
Disagreement needs an outcome you did not choose
Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.
What was a specific lesson you learned from a past project?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Where do you see yourself in five or ten years?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
What has been your biggest work obstacle recently and how did you deal with it?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Tell me about a time when you had a conflict with a coworker or teammate.
Medium · behavioural and engineering judgement
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
- How did you know your change caused the improvement?
- What did you decide not to do, and why?