cswork / libraryOpen practice workspace

Interview guides · GKN Aerospace

GKN Aerospace Software Engineer Interview Questions & Guide 2026

API design practice for GKN Aerospace software engineer candidates: compatibility, versioning, and changes you cannot take back.

The 60-second brief

As a Software Engineer at GKN Aerospace, you are not just writing code; you are contributing to the backbone of global aviation and defense. You will work at the intersection of high-precision manufacturing and advanced digital systems, developing tools and software that support the creation of complex aero-structures and engine components. Your work directly affects how efficiently GKN Aerospace's global teams design, build, and maintain the next generation of flight technology.

Scope in the operational half of the job when the seat carries on-call. Rollout, rollback and what you would put on a dashboard belong inside a design answer rather than after it, and an answer that never reaches them reads as someone who has built systems but not run them.

Focus areas

  • Make every external side effect safe to retry
  • Model units, lots and state machines precisely
  • Reserve inventory without overselling under concurrent allocation

Overview

This role requires a unique blend of technical rigor and operational awareness. You will often collaborate with multidisciplinary teams—including manufacturing, design, and quality engineers—to solve complex problems in a highly regulated, safety-critical environment. Whether you are optimizing CAD/CAM/CAE workflows or building systems to streamline production, your contributions are vital to maintaining the high performance and reliability that GKN Aerospace is known for globally.

The GKN Aerospace interview process

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

Initial Screening

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

What to demonstrate

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

How to prepare

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

Technical Discussions

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.

Behavioral Assessments

What you say here is written down by each interviewer and compared afterwards, so the unit of evaluation is a claim someone else could check, not a well-told narrative. Two things make a story checkable: detail only a participant would hold, and a clean line around which part was yours. Vague ownership is the usual failure and it is usually accidental, because engineers say we about the team's work and we about their own, so the thing they personally built disappears into the plural. Name the part you wrote, and name who did the rest.

What to demonstrate

  • Whether your details are ones a participant would hold and an observer would not: the constraint that ruled out the obvious approach, the first attempt that failed, the person who objected and on what grounds
  • Whether ownership survives a direct question, since a follow-up to we decided is routinely who decided, and an answer that stays plural at that point is read as the work belonging to someone else
  • Whether the numbers you quote are ones you would say identically to a former colleague with the dashboard open

How to prepare

  • Go through each story replacing every we with either I or a named role (the on-call engineer, the reviewer, the other team) and check the story still holds together. Wherever it stops making sense you have found a part you cannot actually speak to
  • Open the artefacts for two of your stories, the pull request, the design doc, the incident notes, and read them for dates and figures you have been rounding in the retelling. Correct your version to match
  • For each story write the single sentence you would least want repeated to a former teammate, then either make it accurate or take it out

Where candidates lose points

Ordering events by arrival rather than by event time

Offline handhelds, batch partner feeds and store-and-forward gateways deliver observations out of order as a matter of course, so a pipeline that folds whatever arrived last will flap a delivered shipment back to in transit and recompute on-time performance from the wrong facts. Message-broker ordering guarantees do not rescue this: per-partition ordering only holds within a partition, so unless the producer keys by the entity being tracked, two events for one leg can land on different partitions and be processed concurrently. The defence has two halves that are often confused - deduplicate on a stable key, then fold with a monotonic status lattice ordered by occurred_at - and both are needed, because deduplication alone still lets a stale event win. Note also that occurred_at is device-reported and therefore sometimes wrong, which is why storing received_at and a measured clock offset beside it is what makes event-time logic auditable instead of merely plausible.

Judging delivery windows and cut-offs in UTC

A delivery appointment, a receiving cut-off and a same-day promise are all statements about local wall-clock time at a specific stop, so evaluating them in UTC misclassifies on-time performance for any leg that crosses a zone, and misses cut-offs by an hour on the two days a year a region changes offset. The subtlety that catches experienced engineers is that converting a future local time to a UTC instant at write time bakes in the offset rules as they were known then: when a jurisdiction changes its rules, an appointment stored that way silently moves, which is why a future window is better stored as local wall time plus an IANA zone identifier and resolved at read time, while a past event is correctly stored as an instant. Duration arithmetic needs the same care, because a transit that spans a DST boundary is genuinely 23 or 25 hours long in local terms even though the elapsed instant count is unchanged. Storing only a date with no zone - common on partner feeds - is the version of this that cannot be fixed downstream, so the zone has to be captured at ingestion.

Saying 'eventually consistent' without naming the anomaly a user would see

Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.

Choosing a schema before the access patterns are known

Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.

Questions & practice

Re-sum a movement ledger and report positions that disagree

Easy · aggregation · reconciliation · append-only ledger

inventory_movement holds 3 billion immutable rows: item_id, node_id, lot_id (NULL when the item is lot-untracked), state, delta_qty (signed int64 in the item's smallest transacting unit), uom_code, and reversed_by. inventory_position holds one row per (item_id, node_id, lot_id, state) with qty, version and last_movement_id, using lot_id = 0 as the sentinel for untracked. Up to 80 million distinct keys. Report every key whose summed movements disagree with the stored qty, while writes continue. State your time and space bounds and how you bound the comparison.

Approach

  1. Fix the key first. inventory_movement.lot_id is NULL for untracked items and inventory_position.lot_id is 0, so the group key is (item_id, node_id, COALESCE(lot_id, 0), state). Getting this wrong does not error — it silently produces two groups that each look like a variance, and the report becomes noise nobody reads.
  2. Cut the ledger at a watermark, but do not take W = max(movement_id) at the start of the scan. A sequence hands out an id before the inserting transaction commits, so at the instant you read that maximum there are ids below it still in flight and invisible to your snapshot. Summing movement_id <= W misses them on this run, and advancing reconciled_through_movement_id to W makes the miss permanent: every later incremental run starts above those ids and they are never summed again. That is a hole in the ledger's own re-derivation, not a transient skew.
  3. Take a watermark that is provably settled instead. Bound write transactions with a statement or transaction timeout so 'the longest write' is a number T, record (observed_at, max_movement_id) samples periodically, and use as W_safe the largest sampled id whose observed_at is older than T — every id at or below it has committed or rolled back. Sum rows with movement_id <= W_safe, compare only against position rows with last_movement_id <= W_safe, treat anything newer as a write that raced you rather than a variance, and advance reconciled_through_movement_id only to W_safe so the next run starts there instead of re-reading 3 billion rows. If the ledger carries a commit timestamp, cutting on that is the same guarantee without the sampling table.
  4. Sum delta_qty as int64, and include reversal rows. reversed_by is a back-pointer for audit, not an exclusion filter: an original of +10 and its reversal of -10 must both be summed to reach 0. Excluding the original while keeping the reversal produces -10 and a false variance on every corrected key.
  5. Guard the denomination rather than trusting it. uom_code is stored per row because pack factors change over time, so reject — do not sum — any row whose uom_code is not the item's smallest transacting unit, and report those keys separately. A mixed-denomination sum is arithmetically meaningless and looks exactly like a real variance.
  6. Hash aggregation is O(n) time and O(distinct keys) space: 80 million entries at roughly 40-56 bytes each in a typical runtime is 3-5 GB, so quote the number. The bounded-memory alternative is an external sort-merge on the group key — O(n log n) comparisons, resident memory bounded by the merge fan-in rather than by key count, and it streams — or hash-partition by hash(item_id) % P and run P independent passes for 1/P of the peak.

Worked solution (25 min)

  1. Write the group key expression down explicitly, including the NULL-to-0 mapping, before any aggregation code.
  2. Derive W_safe from a sample older than the transaction-duration bound T, and state in one sentence which rows on each side are in scope — this is the part that makes the report trustworthy under live writes, and the part that decides whether an in-flight row is re-read next run or lost forever.
  3. Implement the hash aggregation with an int64 accumulator and a uom_code assertion per row, emitting rejected rows to a second output rather than into the sum.
  4. Build a fixture with four keys: one clean, one with a movement missing from the position, one with an original plus its reversal, one with a row in cases where the item transacts in eaches.
  5. Run it, then re-run with the fixture rows shuffled, with a concurrent writer appending past the watermark, and with a writer that reserved an id below max(movement_id) and commits only after the scan finishes — then run the incremental pass and confirm that row is summed there.

Expected result: Exactly one key is reported as a quantity variance; the reversed key reconciles to zero difference; the mixed-unit key appears in the rejected-rows output rather than the variance list; rows appended past the watermark change nothing; the row whose id was reserved below max(movement_id) but committed late still sits above W_safe, so the next incremental run sums it instead of skipping it permanently.

Verify it yourself

  • Summing the full ledger for a clean key equals its inventory_position.qty exactly, with no tolerance — a tolerance here would hide the defect the job exists to find.
  • Two runs at the same W_safe over the same data produce byte-identical output regardless of row order.
  • Setting the watermark to max(movement_id) instead of W_safe makes the late-committing fixture row vanish from this run and from every run after it — the failing test that shows the sampled watermark is load-bearing.
  • Deleting the reversed_by filter (if you wrote one) changes the result, which tells you the filter was load-bearing and wrong.

Follow-up

  • A key shows a variance of exactly one pick, repeatedly, at one node. What do you look at first, and what would distinguish a duplicate movement from a missed one?
  • The job takes six hours and the variance report is stale by the time anyone reads it. How would you make it incremental without losing the guarantee that it re-derives from the ledger?
  • Who writes the correcting movement, and what reason code does it carry?

Find the hottest write keys under a fixed memory budget

Medium · top-k · heap · streaming sketches

One day of writes to inventory_movement is 200 million rows; each carries (item_id, node_id). Distinct pairs reach 60 million. Return the 200 pairs with the most writes so they can be moved behind a single writer before the next peak. First give the exact answer assuming you may hold 60 million counters, then give an answer under a 200 MB budget, and state the error guarantee that second answer carries. Both must be one pass over the day's rows unless you argue for a second.

Approach

  1. Exact version: one hash pass to build (item_id, node_id) -> int64, then a single scan of the map maintaining a min-heap of size K = 200, pushing and popping when the incoming count exceeds the heap root. O(n + d log K) with n = 2e8 and d = 6e7, versus O(d log d) if you sort all 60 million entries — a factor of roughly log d / log K, about 3.4x, for no benefit. The heap must be a min-heap: the root is the weakest survivor, so eviction is O(log K).
  2. Cost the map honestly. 60 million entries at a 16-byte key, an int64 count and per-entry overhead is 2-3 GB in most runtimes, which is the whole reason for the second version rather than a theoretical aside.
  3. Bounded version: Misra-Gries with m counters. Keep at most m keys with counts; on a key already present increment it; on a new key when fewer than m are held, insert at 1; otherwise decrement every held counter and drop those reaching zero. With m = 1,000,000 at roughly 40 bytes per counter, that is about 40 MB, comfortably inside 200 MB alongside the heap.
  4. State the guarantee as a number, not as 'approximate'. Misra-Gries returns an estimate satisfying f - n/(m+1) <= f_hat <= f, so with n = 2e8 and m = 1e6 every count is an undercount by at most 200. If the 200th hottest pair has hundreds of thousands of writes, a 200-count error cannot reorder the top of the list — and if the counts near rank 200 are within 200 of each other, the ranking there was never meaningful for the decision being made. The sketch also merges: run one per shard, combine, prune back to m, and the bound holds with n the combined length.
  5. If exactness is genuinely required, argue for two passes rather than more memory: the Misra-Gries pass yields a candidate superset of size m, and a second pass counts only those candidates exactly in O(m) memory. That is the standard way to get an exact top-K under a memory bound, and the cost is one extra read of the day's rows.

Follow-up

  • The decision downstream is which keys get a single writer. Does a rank error at position 190 change that decision, and what does the answer tell you about how much accuracy to buy?
  • Write volume on one key is bursty — a promotion concentrates it into twenty minutes. Does a daily top-K find it, and what window would?
  • How do you keep the sketch across a worker restart without replaying the day?

Fold late out-of-order status events into a monotonic leg state

Hard · semilattice fold · event time · idempotency

Leg statuses form the chain planned < tendered < accepted < picked_up < in_transit < delivered, with exception and cancelled outside it. You ingest 300 million observation_event rows a day carrying subject_id (leg), observed_status, occurred_at, received_at, clock_offset_ms and dedupe_key; five percent arrive over an hour late and a tail arrives a day late. Write the merge that produces each leg's current status, prove the result depends only on the set of events and not their order, and state the per-event cost.

Approach

  1. Define the per-leg state as a tuple ordered totally: (status_rank, occurred_at, source_priority, event_id), and define merge(a, b) as the maximum under that order. Rank leads; occurred_at and the rest only break ties. The proof is then one line: maximum over a total order is idempotent, commutative and associative, so the state space with merge is a join-semilattice and the fold over any permutation of the event set yields the same value. That is also why no global sort is needed — the O(n log n) disappears and the fold is O(1) per event, O(distinct legs) state.
  2. Rank before time, deliberately. occurred_at is device-reported and sometimes wrong; if it led the comparison, the leg's status would be handed to whichever device has the most badly skewed clock. Ranking first means a bad clock can only reorder events within one rank. Correct occurred_at by clock_offset_ms where it was measured, and where the offset exceeds a threshold keep the observation but bar it from winning a tie — the event is still evidence, it just stops being authority.
  3. Make the lattice total, because exception and cancelled are not points on the chain and max over a partial order is undefined for incomparable pairs. Model cancelled as an absorbing top element, and carry exception as a boolean flag merged by OR alongside the rank rather than as a rank of its own — otherwise an exception at rank 4 either blocks delivered at rank 6 or is erased by it, and both are wrong. A product of semilattices is a semilattice, so the order-insensitivity argument still holds over the tuple.
  4. Deduplicate on dedupe_key even though the merge is idempotent. Idempotence covers a byte-identical redelivery; a partner replaying a window with a fresh event_id is not identical, and the tie-break would let the replay displace the original for no reason. Dedupe on the stable key, then fold — both halves are needed, and conflating them is the usual defect.
  5. Partition the stream by subject_id so all events for one leg land on one partition: broker ordering guarantees are per-partition only, and while a commutative merge makes interleaving harmless, a concurrent read-modify-write on the same leg row is still a lost update. Where that cannot be guaranteed, make the write a compare-and-set on a version column, or a conditional update that only raises the state. Because the state is mergeable, shard maps combine cleanly and replaying the whole day converges to the same answer — which is what makes reprocessing safe.
  6. Say where genuine regressions live: a delivered shipment that is refused and returns is a new leg, not a rank decrease. Monotonicity is only sound if real reversals are modelled as new objects, and keeping every superseded observation is what lets you answer the dispute later, so the fold discards nothing — it marks losers superseded.

Follow-up

  • An event arrives a week late for a leg already closed and invoiced. Does it change the status, the on-time metric, both, or neither?
  • Two events for one leg have the same rank and the same occurred_at from different sources. What decides, and is that decision stable across a replay?
  • How would you test order-insensitivity rather than assert it?

Decide whether available-to-promise is derived or stored

Hard · denormalisation · read path · cache staleness

Available to promise at one node equals inventory_position.qty in state on_hand minus the sum of allocation.qty in status held or committed for that key. The read sits inside checkout: 4,000 reads per second at peak, p99 budget 20 ms. inventory_position holds 40 million rows; during a promotion a single hot item-node pair carries about 2,000 open allocations. Decide whether to compute this per read or to maintain available_qty on the position row. Give both paths, the failure mode of each, and what the browse read may do that the allocate read may not.

Approach

  1. Cost the derived read in tuples, not in plans. One index probe into the position is trivial; the aggregate over allocation touches one tuple per open claim on that key. Even if only a tenth of peak lands on the hot pair, 400 reads per second times 2,000 rows is 800,000 index tuples per second for one item at one node, and it gets worse exactly as the key gets hotter. A partial index on status IN ('held','committed') does not help, because the row count is the cost, not the selectivity.
  2. Maintaining available_qty on the position row is nearly free on the contention axis, which is the point most candidates miss. The allocator already takes a row lock or a compare-and-set on that same position row, so the extra column costs no lock that was not already held, and the read collapses to a single primary-key probe well inside 20 ms.
  3. It is not free on the correctness axis. available_qty becomes a second derived value that can drift, so it joins qty under the same reconciliation job with its own re-sum against the allocation table, and a drift alert on that comparison is the operational price of the read speed.
  4. Split the two reads rather than arguing about one. The browse read may serve from a replica or a short-TTL cache and must err low, showing less than is there. The allocate read must be on the primary and inside the same transaction as the conditional update, because replication lag peaks during the write burst that made the key hot. Conflating them is what makes a replica read look safe in a benchmark.
  5. Pick the TTL against the drain rate, with a number. If a promotion burns 2,000 units in a minute, a 30-second cache is a thousand units stale, so the displayed figure should be a banded signal such as in stock or low stock rather than an exact count that the allocate step will then contradict.
  6. Do not denormalise availability across nodes into a single network number. A yes-somewhere answer cannot be allocated against, so sourcing will disagree with the page, and the customer sees a promise retracted after checkout rather than an honest out-of-stock before it.

Worked solution (40 min)

  1. Load a position with 2,000 open allocations and measure p99 for the derived read at 400 requests per second against the same read from a maintained available_qty column.
  2. Add available_qty, backfill it per key under the same lock the allocator takes, and write the re-sum query that proves it equals qty minus held and committed.
  3. Run both read paths in shadow for a fixed window, logging every disagreement with its key and magnitude.
  4. Point the browse read at a replica with a 30-second TTL and the allocate read at the primary, then measure how often the browse figure is higher than the allocatable truth.

Expected result: The maintained column serves the read in a single primary-key probe inside the 20 ms budget at 4,000 reads per second, the derived path misses it on the hot key, and shadow comparison shows zero disagreements on the primary path while the replica path is stale in the low-availability direction only.

Verify it yourself

  • p99 for the maintained read is flat as open allocations on the key grow from 10 to 2,000; the derived read's is not.
  • The re-sum finds zero drift after a sustained allocate-and-release workload.
  • Every observed browse-versus-primary disagreement is an under-report, never an over-report.
  • Killing an allocator mid-transaction leaves available_qty and the allocation rows consistent after the expiry sweeper runs.

Follow-up

  • Roll this out on a live system. How do both paths run side by side, what do you compare, and what result gates the cut-over?
  • The reconciliation job finds available_qty drifted by 3 on one key. What do you do, and what must you not do?
  • Lot-tracked items multiply the position rows per item-node. Does your answer change, and where does the read now spend its time?

Attribute multi-stop freight cost without fanning it out

Medium · many-to-many joins · window functions · integer money

shipment_leg(leg_id, shipment_id, sequence_no, linehaul_cost_cents, accessorial_cost_cents, actual_arrive_at) joins to fulfilment_order_line through leg_order_line(leg_id, order_line_id, allocated_weight_g). One multi-stop truckload leg carries 40 order lines. The current report SUMs linehaul_cost_cents after joining all three tables and returns roughly forty times the real freight spend. Write the query that returns cost per order for one month, explain the fan-out in terms of the join's row count, and make the per-order amounts sum exactly to the leg totals in integer cents.

Approach

  1. Name the cardinality: joining shipment_leg to leg_order_line produces one row per (leg, line) pair, so the result has SUM over legs of lines-per-leg rows rather than one row per leg. SUM(linehaul_cost_cents) over that result counts each leg's cost once per line it carries, which is why 40 lines gives 40 times the spend. The bug is not the SUM, it is aggregating a parent measure across a child-expanded row set.
  2. Compute a share before aggregating anything. In one pass: leg.linehaul_cost_cents * b.allocated_weight_g / SUM(b.allocated_weight_g) OVER (PARTITION BY b.leg_id), which keeps the denominator local to the leg and needs no self-join.
  3. Do the division in integer cents and settle the remainder deliberately. Floor each share, then distribute the leftover cents one each by descending remainder, ordered by a stable tiebreak such as order_line_id. Floating-point shares leave a residue that makes the freight accrual miss by a cent per leg, which costs more to investigate than the freight.
  4. Aggregate to the order only after the share exists: GROUP BY order_id over the per-line allocated cents. Restrict the month on shipment_leg.actual_arrive_at, not on the order date, so a leg is attributed in the period it actually ran.
  5. Index for the shape of the query: leg_order_line needs (leg_id) for the window partition and (order_line_id) for the reverse lookup, and shipment_leg needs an index on actual_arrive_at to bound the month without a full scan.
  6. Apply the same treatment to accessorial_cost_cents separately. Accessorials such as a detention charge often belong to one stop rather than to the whole leg, so weight-sharing them across every line on the load misattributes cost even though the total ties out.

Follow-up

  • A line is cancelled after the leg ran. Does its share of the freight disappear, move to the remaining lines, or stay as a cost with no revenue?
  • Weight is the wrong basis for a light bulky pallet. What would you use instead, and does the reconciliation check still hold?
  • The carrier invoice arrives three weeks later and differs from the rated cost. Where does the correction land, and does it re-run this allocation?

How would you describe your strengths, with an example scenario from your previous work?

Medium

Approach

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

Follow-up

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

What root cause analysis techniques have you used?

Medium

Approach

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

Follow-up

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

Specify the idempotency key contract for allocation requests

Medium · idempotency · concurrency · api contract

The Fulfilment Orchestrator calls POST /allocations with order_line_id, item_id, node_id, lot_id, qty, uom_code, tier and expires_at, and retries from a durable saga step after any timeout. Specify the Idempotency-Key contract: key ownership and derivation, key scope and lifetime, what is fingerprinted, the response when a retry arrives while the first request is still in flight, and what a replay returns given that allocation rows move through held, committed, consumed, released and expired. Relate it to the partial index UNIQUE (order_line_id, node_id, lot_id) WHERE status IN (held, committed).

Approach

  1. Scope the key to (client_id, endpoint, key) and insert the key row in the same transaction as the allocation, with a unique constraint doing the work. A read-then-write check against a cache is not idempotency - it is a race with a smaller window.
  2. Fingerprint the semantically significant fields (canonical serialisation of item, node, lot, qty, uom_code, tier) and store the hash. Same key with a different fingerprint is a caller defect, not a retry: answer 422 with its own code, following the convention in the IETF Idempotency-Key header draft, and change nothing.
  3. Define the in-flight case rather than letting the database decide. A second request arriving while the first holds the key row either waits on the lock - bounded, and a 50 ms caller budget makes that unattractive - or fails fast with 409 plus Retry-After. Starting a second allocation is never an option.
  4. Make the replay return the current representation of the referenced allocation with the first call's status code, not a frozen body. A soft claim that expired between the original call and the retry must not be replayed as live. This is a deliberate departure from byte-identical replay, and the contract must name the fields the caller re-checks: status and expires_at.
  5. Size the lifetime against the business object, not a round number: the key row has to outlive the longest soft expiry plus the orchestrator's maximum retry horizon, or a late retry allocates a second claim against the same line. Retiring keys early is the failure that looks like double allocation months after ship.
  6. Place the partial unique index correctly in the story: it is the backstop that stops two concurrent held claims for one (line, node, lot), not the mechanism. It cannot stop a re-allocation after the first was released, which is exactly why the key contract exists above it.

Worked solution (30 min)

  1. Write the key table DDL: (client_id, endpoint, key) primary key, request_fingerprint, allocation_id, response_status, created_at, plus the retention rule.
  2. Write the happy-path transaction in order: insert key row, insert allocation, decrement the position under its version CAS, commit.
  3. Derive the key deterministically from persisted saga state (order_line_id plus the sourcing attempt number) and confirm a crashed process regenerates the same value instead of minting a fresh UUID.
  4. Enumerate four retry timings - during, after commit, after soft expiry, after release - and write the exact response for each.
  5. Check the fingerprint path by resending the key with qty changed and confirming nothing is written.

Expected result: A contract where the key is caller-derived from durable state, the key row is written in the allocation transaction, a mismatched fingerprint is a 422 that writes nothing, an in-flight duplicate is a 409 with Retry-After, and a replay returns the allocation's current status rather than a stale success.

Verify it yourself

  • N concurrent retries of one key produce exactly one allocation row: one call returns the create status, the other N-1 return 409 with Retry-After, and a retry sent after that call commits replays its status code and allocation_id instead of a 409.
  • A retry arriving after the soft claim expired does not present an expired allocation as held.
  • The partial unique index, dropped in a test, changes nothing about the duplicate-retry outcome - proving the key is the mechanism and the index is the backstop.

Follow-up

  • A line short-ships and is re-sourced to a second node. Does that call reuse the key, and what is the key derived from now?
  • The key store and the allocation rows are in different shards. What does the write path look like, and what have you given up?
  • What does a request carrying an expired key do, and how does the caller tell that apart from a key it never sent?

Availability p99 spikes in a sawtooth locked to the cache TTL

Medium · cache stampede · single-flight · load shedding

Available-to-promise reads sit inside checkout with a 12 ms p99 behind a 60-second per-key cache. During a promotion, p99 spikes to 900 ms in a sawtooth exactly on 60-second boundaries and is normal between spikes. The inventory database shows bursts of thousands of identical single-row position reads inside 50 ms across about 40 item-node keys, and the connection pool saturates during each burst. Request throughput is unchanged. Diagnose, fix, and state which callers may read a cached availability and which may not.

Approach

  1. Separate a stampede from saturation by the shape. Latency that rises smoothly with request rate is capacity; a sawtooth whose period equals the TTL, with normal service between peaks, is expiry-driven. Confirm by overlaying spike timestamps on key expiry times and watching the hit ratio fall to near zero for a few milliseconds each cycle.
  2. Size the herd with Little's law rather than adjectives. Concurrency equals arrival rate times service time, so a key taking 1,200 requests per second with a 30 ms origin read admits about 36 simultaneous origin reads at each expiry, and 40 keys that expire together produce roughly 1,440 against a 100-connection pool. That fourteen-fold oversubscription is the 900 ms, and the arithmetic matching is what confirms the diagnosis.
  3. Explain why the 40 keys expire in lockstep: they were all populated in the same second when promotion traffic arrived, so their TTLs are synchronised and stay synchronised. Jitter of plus or minus twenty percent on the TTL desynchronises them; it does not reduce the per-key herd.
  4. Bound the per-key herd with single-flight. One in-flight origin read per cache key, with concurrent callers awaiting the same result, takes origin load to one read per key per TTL regardless of request rate. Add serve-stale-while-revalidate so the waiters get the previous value immediately and staleness is bounded and measured rather than incidental.
  5. Draw the correctness boundary explicitly, because it is not a performance question. Browse and product pages may read a cached or stale availability. The allocation path may not: the check and the write must be one atomic step against the primary, so a cached number can inform a display but must never authorise a reservation. Checkout sits in between and should either read through with a short TTL or accept that its number is advisory and can fail at allocation.
  6. Decide the degradation direction in advance. If the origin is saturated, the read path should shed to a conservative answer, showing limited availability rather than a precise count and never rounding upward, because an optimistic stale read converts a latency incident into oversells and short-ships.

Follow-up

  • What does your single-flight do when the origin read times out: fail the waiters, or serve them the stale value, and what bounds the staleness?
  • One key gets hot enough that even one origin read per TTL per replica is too much across 200 application instances. What changes?
  • How would you invalidate a key on a movement write without opening a second stampede on the invalidation itself?

Suggested practice checklist

Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.

Day 1 — Diagnostic, scored before you study anything

  • Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
  • Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
  • Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.

Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.

Day 2 — Largest gap: find the boundary rather than the subject

  • Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
  • Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
  • Re-attempt one of them from blank four hours later with nothing open.

Deliverable: A sub-skill map with the two blocking sub-skills circled.

Day 3 — Drill the blocking sub-skill by repeating the shape

  • Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
  • State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
  • Have someone else read your one-sentence rule and find the precondition you left out.

Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.

Day 4 — Second gap, plus maintenance on the strongest area

  • Run the same sub-skill decomposition on the second-largest gap in half the time.
  • Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
  • Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.

Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.

Day 5 — The gap that is not a skill

  • Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
  • Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
  • Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.

Deliverable: Two recordings with a counted reduction in time-to-first-question.

Day 6 — Retest under day-one conditions

  • Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
  • For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
  • Write down which single block you would still lose the offer on.

Deliverable: A second scored rubric placed beside the first, with one named remaining risk.

Day 7 — Full loop under interview conditions

  • Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
  • Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
  • Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.

Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.

'We rewrote it in a better framework' is not a story

A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.

What gives you motivation to work?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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

Where do you see yourself in the next five years?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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

How do you handle working under uncertainty?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Give the blast radius: what could have broken, and what you measured.
  3. 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?

Describe a difficult situation you were in and how you resolved it.

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Close with what you would do differently, concretely.

Follow-up

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