cswork / libraryOpen practice workspace

Interview guides · Avient

Avient Software Engineer Interview Questions & Guide 2026

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

The 60-second brief

A Software Engineer at Avient serves as a critical bridge between advanced material science and operational efficiency. In this role, you are not just writing code; you are building the digital infrastructure that supports a global leader in specialized polymer materials and sustainable solutions. Your work directly impacts how the business monitors production, manages supply chains, and delivers high-performance products to a diverse range of industries.

Browser-facing seats are not covered by algorithm practice. Scope in state ownership, what the page does on a slow or failed request, and how you would diagnose something that renders correctly but feels slow.

Focus areas

  • Fold late, out-of-order scan events by event time
  • Make every external side effect safe to retry
  • Reserve inventory without overselling under concurrent allocation

Overview

This position demands a balance of technical precision and practical problem-solving. Whether you are automating workflows, supporting plant-level systems, or developing tools that enhance manufacturing output, your primary objective is to drive reliability and innovation. You will operate in an environment where your technical contributions are expected to translate into tangible, real-world results, making this an ideal role for engineers who enjoy seeing the direct impact of their software on physical products and business outcomes.

Because Avient is a leader in materials science, take the time to research their primary product lines and the specific manufacturing processes of the facility or department you are interviewing with. Demonstrating industry awareness is a significant differentiator.

The Avient interview process

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

Initial Screening

Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.

What to demonstrate

  • Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
  • Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
  • Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not

How to prepare

  • Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
  • Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
  • If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window

Technical Discussions

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

Behavioral Assessments

This round is deciding whether a change you make without supervision can be allowed to reach production. It is scored on what you knew at the moment you decided, not on how it turned out, so a story that opens with the result and works backwards reads as luck retold as judgement. Say what the options were, what you did not know, what you did to shrink the unknown before committing, and what you accepted as the worst plausible case. The detail that separates answers is a bound: how many users, how much data, and for how long, if you had been wrong.

What to demonstrate

  • Whether the reasoning you give was available at the time you decided rather than after the result came in, since a story whose deciding evidence arrived later describes an outcome and not a judgement
  • Whether you can put units on the exposure (users, rows, minutes of degraded service) and whether the containment you chose actually bounded it: a canary bounds the request path it fronts, while a background job writing to a shared table reaches every user regardless of which version served their requests
  • Whether the reversal path existed before you shipped or was improvised during the incident, and whether it restores state or only stops further damage

How to prepare

  • For your three largest changes, write down the one thing you would have had to be wrong about for it to fail, and what your best estimate of it was on the day you shipped. If you never held an estimate, that is the gap the follow-up questions will find
  • Write the undo procedure for one of those changes as it existed at the time, then mark which steps restore data and which only stop new damage. Turning a flag off or reverting a deploy ends the new writes; rows already written come back only from a copy you kept, and a dropped column comes back empty unless something outside the schema holds the values
  • Rehearse one story from the decision point forward and stop before the outcome, then have someone ask what you would do next. If the story only works with the ending attached, it is an anecdote rather than a decision you can defend

Where candidates lose points

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.

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.

A queue or buffer with no bound

Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.

Listing technologies instead of trade-offs

Name the property the design needs first, such as ordered range scans, multi-entity transactions, cheap appends, or a predictable p99, then pick something that provides it and say what it gives up in exchange. Almost any component is defensible once you state the requirement it satisfies and the one it sacrifices.

Questions & practice

What did you do and learn from your internships?

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • How does this change if the input no longer fits in memory?

How did you do troubleshooting?

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Choose the data structure from the access pattern, not from familiarity.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • How does this change if the input no longer fits in memory?

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?

Stop two allocators promising the same unit of stock

Medium · isolation levels · lock contention · compare-and-set

inventory_position is keyed (item_id, node_id, lot_id, state) and holds qty BIGINT, which is on-hand stock and is moved only by the movement ledger; available_qty BIGINT, maintained on the same row; and version. The declared invariant is available_qty = qty - SUM(allocation.qty) over that key in status held or committed, enforced by CHECK (available_qty >= 0) and CHECK (available_qty <= qty). allocation holds allocation_id, order_line_id, item_id, node_id, lot_id, qty, tier, status (held, committed, consumed, released, expired) and expires_at. Twelve allocator threads try to claim the last eight units of one item at one node inside the same 50 ms. Give the exact statements that let at most eight units be claimed, then say what changes if identical code runs under PostgreSQL REPEATABLE READ versus InnoDB REPEATABLE READ, and what a retried allocate for the same order line must do.

Approach

  1. Collapse the check and the write into one statement whose WHERE clause is the business rule: UPDATE inventory_position SET available_qty = available_qty - :q, version = version + 1 WHERE item_id = :i AND node_id = :n AND lot_id = :l AND state = 'on_hand' AND available_qty >= :q. qty is deliberately untouched: a hold is a promise, not a physical move, so only a movement may change on-hand and the ledger-equals-position invariant survives. Rowcount zero is a refusal, not an error, and the allocation row is inserted in the same transaction only when the rowcount is one.
  2. If the deployment carries no available_qty column and availability must be derived, the alternative is SELECT ... FOR UPDATE on the position row followed by the SUM over held and committed allocations inside the same transaction. That is correct but holds the row lock for two statements instead of one, which matters on a key taking hundreds of writes a second.
  3. Isolation is not a substitute for the atomic step, and it changes the error contract. Under PostgreSQL READ COMMITTED the losing UPDATE blocks, then re-evaluates its predicate against the newly committed row version and returns rowcount 0, which the handler already handles. Under PostgreSQL REPEATABLE READ the same statement instead raises SQLSTATE 40001 and the whole transaction must be retried, so a handler that only tests rowcount fails closed on an exception it never expected. InnoDB REPEATABLE READ does neither: a locking read or UPDATE performs a current read of the latest committed row, so the loser blocks and then sees rowcount 0, with a deadlock rather than a serialization failure as its exceptional case.
  4. Make the retry a no-op with a partial unique index: UNIQUE (order_line_id, node_id, lot_id) WHERE status IN ('held','committed'). Then a retried allocate conflicts instead of double-claiming, and the handler reads the existing allocation and returns it.
  5. Never source the availability number from a replica or a cache for this path. Replication lag is largest during the write burst that made the key hot, so the guarantee fails precisely when it is load-bearing.
  6. For the hottest item-node pairs, shed load without overselling: route allocations for that key through a single writer with a bounded queue and refuse with a conservative not-available when the queue is full. Splitting the position into N sub-rows cuts contention N-fold but strands the remainder across sub-rows and produces false out-of-stock when the last units are spread thin.

Follow-up

  • One order line needs two units from two different nodes, atomically. What do you do when those positions live in different shards and no distributed transaction is available?
  • Soft-tier allocations expire. Show the sweeper statement, and say what stops it from releasing an allocation that is being committed at that instant.
  • Under sustained contention your CAS retries. What is the retry budget, and what does the caller see when it is exhausted?

Two scan-event queries that stopped using their indexes

Medium · indexing · partition pruning · brin correlation

observation_event is range-partitioned monthly on occurred_at, with a btree on (subject_type, subject_id, occurred_at), a BRIN on occurred_at and a GiST on geo. Rows are inserted in received_at order and events are routinely hours late. Two queries regressed: (A) WHERE occurred_at::date = current_date AND observed_status = 'delivered'; (B) a backfill scanning WHERE received_at >= now() - interval '1 day'. For each, say why no index is used and no partition is pruned, and give the fix.

Approach

  1. Query A wraps the column in a cast, so the predicate is over occurred_at::date and not over occurred_at. A btree or BRIN on the bare column cannot be probed, and the planner has no range to prune partitions with, so it plans every monthly partition. Fix it by rewriting to a half-open range: occurred_at >= :start AND occurred_at < :start + interval '1 day', with :start computed by the caller from the relevant IANA zone.
  2. Do not reach for an expression index as the first fix here. The timestamptz-to-date cast is STABLE rather than IMMUTABLE because it depends on the session TimeZone, so PostgreSQL refuses to index it; indexing ((occurred_at AT TIME ZONE 'UTC')::date) is accepted but silently pins the day boundary to UTC, which is the wrong day for any stop outside it.
  3. Query B filters a column that is neither indexed nor the partition key, so it is a full scan of every partition by construction. The subtler half is why the BRIN cannot rescue it: BRIN stores a min and max per block range and only pays when physical order correlates with the column. Rows arrive in received_at order while the indexed column is occurred_at, and late events interleave hours-old timestamps with current ones, so each range's min and max span days and almost every range qualifies.
  4. Measure the correlation rather than assuming it: SELECT attname, correlation FROM pg_stats WHERE tablename = 'observation_event'. Near 1 means BRIN earns its keep, near 0 means it is a full scan with extra steps. Expect received_at near 1 and occurred_at well below it on this insert pattern.
  5. Fix B with a btree on received_at, which stays compact because it is correlated with insert order, or better, drive the backfill from a monotonic cursor it already has such as event_id, which needs no new index on a table taking tens of thousands of inserts per second.
  6. Confirm with EXPLAIN (ANALYZE, BUFFERS) and read the plan, not the clock: check how many partitions appear, whether rows removed by filter dwarfs rows returned, and whether shared buffers read matches the size of one partition or of the table.

Worked solution (25 min)

  1. Load a few million rows across three monthly partitions with occurred_at deliberately shuffled against insert order.
  2. EXPLAIN (ANALYZE, BUFFERS) query A as written and record the partitions scanned and the buffers read.
  3. Rewrite A as a half-open range and re-run, comparing partitions scanned and buffers.
  4. Query pg_stats for the correlation of occurred_at and received_at, then add a btree on received_at and re-plan query B.

Expected result: Query A plans one partition and an index scan after the rewrite, versus every partition before. Query B moves from a full scan of all partitions to an index scan on received_at, and the measured correlation explains why the existing BRIN never helped.

Verify it yourself

  • The rewritten A returns byte-identical rows to the cast version for a day fully inside the chosen zone.
  • Buffers read for A drops by roughly the ratio of one partition to the whole table.
  • pg_stats shows occurred_at correlation well below received_at, matching the insert pattern.
  • Forcing enable_seqscan = off does not make the original A use the index, confirming the cast and not the planner is the cause.

Follow-up

  • Where would you put the partition boundary given events arrive up to a week late, and what happens to a row whose occurred_at falls in a partition that has already been detached?
  • The delivered-today query is asked per node in local time. Does that change your partitioning, your index, or only the caller?
  • Adding a btree to this table costs write amplification. How would you decide whether the backfill is worth it?

What do you want to be doing in the next 5 to 10 years?

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Work from the requirement backwards to the design.
  3. Clarify what is being asked and what a complete answer contains.

Follow-up

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

Do you know what we make at this plant?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  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?

Evolve an availability contract without breaking floor devices

Hard · versioning · backward compatibility · units

GET /availability currently returns {item_id, node_id, qty}, where qty is denominated in whatever the item master says today - eaches for some items, cases for others. Three clients consume it: a checkout service you deploy, a partner integration on a twelve-month release cycle, and handheld firmware on the warehouse floor you cannot force-upgrade. You must expose the unit explicitly, split qty into on_hand, available and reserved, and add a new inventory state. Deliver the field plan, the version negotiation mechanism, the deprecation path, and how you prove nobody still reads the old field.

Approach

  1. Start from the rule that survives contact with un-upgradable clients: never change the meaning or the denomination of a live field. qty keeps precisely its current semantics forever, and the new truth ships as new fields - available_qty with an explicit uom_code, plus the on_hand and reserved split - so the handheld keeps reading exactly what it always read.
  2. Be precise about which additions are safe. New fields are safe only if readers ignore unknown ones, so state tolerant-reader expectations in the contract. Adding a value to an existing enum is not safe: a client switching exhaustively over inventory state breaks on the new state. Either declare the enum open in the current version with a mandated fallback branch, or carry the new state in a parallel field that old clients never see. Choose one and write it down.
  3. Pin versions per client credential rather than per deploy - an account-level default, overridable per request by a version header - so nobody is upgraded by your release. Name the cost honestly: you now serve N shapes from one code path, so cap N with a published support window and a rule that a frozen version gets no changes, not even helpful ones.
  4. Prove the dependency instead of assuming it. Count reads of the deprecated field per client credential, then send Sunset (RFC 8594) with a date and a deprecation link on a horizon longer than the slowest client's release cycle - a twelve-month partner cycle sets the floor, and the handheld fleet may not clear it at all, which is a planning input rather than a surprise.
  5. Guard it in CI with consumer-driven contract tests that fail on any field removal, type change or enum widening in a frozen version. A review checklist does not survive a rushed release; the test does.
  6. Close the loop on the unit trap explicitly: when a supplier changes a case from twelve to ten, the bare qty must not silently re-denominate. Carry the conversion factor's version alongside the number, and state that values already served keep the factor in force when they were computed.

Worked solution (40 min)

  1. Write the current and target payloads side by side and mark every field as unchanged, added or frozen - with nothing in a fourth category.
  2. For the new inventory state, write what each of the three clients does when it encounters an unrecognised value today, and pick open-enum or parallel-field accordingly.
  3. Specify the negotiation: the credential-level default, the override header, the error for an unknown or retired version, and the support window.
  4. Design the usage telemetry that tells you which credential still reads qty, and state the threshold at which you would send Sunset.
  5. Write the CI contract test that fails on a removal, a type change or an enum widening in a frozen version, and run it against the proposed change.

Expected result: An additive plan where qty is frozen rather than redefined, the unit travels on every new quantity field, enum evolution is settled explicitly, versions are pinned per credential, and the deprecation date is derived from measured usage plus the slowest client's release cycle.

Verify it yourself

  • A replay of captured handheld requests against the new build produces byte-compatible values in every field that firmware reads.
  • A client pinned to the old version never sees the new state value in any field it parses.
  • The contract test fails when someone changes qty's type or adds an enum value to the frozen version.

Follow-up

  • A client sends a version header you retired last quarter. What do you return, and what will their code do with it?
  • You need one breaking change on one endpoint. How do you ship it without versioning the whole API?
  • The Sunset date arrives and the handheld fleet still reads the deprecated field. What are your options, in order of cost?

Two committed allocations against a position holding one unit

Hard · concurrency · compare-and-set · isolation levels

Six oversells in a week, all on the twenty hottest item-node pairs, all inside promotion minutes. In one case inventory_position.qty was 1 and two allocation rows exist with qty 1 and status 'committed', created 4 ms apart; both transactions committed cleanly. The code reads availability, then issues UPDATE inventory_position SET qty = qty - :n, version = version + 1 WHERE item_id = :i AND node_id = :d AND lot_id = :l AND state = 'on_hand' AND version = :v, then inserts the allocation. A 200-thread load test across 50,000 SKUs never reproduces it. Find the defect and prove it.

Approach

  1. Reconstruct from the data before reading the code. Pull inventory_movement and allocation for that key ordered by identifier with the version values involved. Two commits that both proceeded from version v prove the compare-and-set did not gate anything; a single row correctly stepping v then v+1 with an oversell anyway would instead send you to the read path.
  2. Name the defect: the affected-row count of the conditional UPDATE is never tested. When version has moved the UPDATE matches nothing and affects zero rows, but control flow continues into the INSERT and the transaction commits, so the WHERE version = :v clause is decoration. A compare-and-set only protects you if the zero-row outcome changes what happens next.
  3. Explain why the load test is silent. With 200 threads spread over 50,000 SKUs, the chance that two allocators land on one row inside the UPDATE's execution window is negligible, while production traffic concentrates on twenty rows. Pin the same test to a single SKU with a position of 1 and N threads, assert that exactly one allocation survives, and it fails on the first run. That test is the proof, and it belongs in the suite.
  4. Rule out the decoy rather than ignoring it. If availability is read from a replica, lag peaks exactly when write volume does, so the read is often stale. But a stale read only mis-sizes the attempt: with a working compare-and-set the write still fails and retries, so the replica is a latency and retry-rate problem, not the oversell. Say that explicitly instead of fixing two things and learning nothing.
  5. Fix and then choose the mechanism deliberately. Test the rowcount; on zero rows re-read the position from the primary and retry with bounded attempts and jittered backoff, and never reuse the version captured before the failed attempt. Compare-and-set is lock-free but burns retries under contention on a single hot row; SELECT ... FOR UPDATE inside a short transaction queues writers predictably and is usually calmer for twenty hot keys; above that, funnel allocations for one item-node key through a single writer and accept the added latency.
  6. State what the isolation level does and does not do, since identical code is not equally safe everywhere. On PostgreSQL READ COMMITTED, which is the default and what this deployment runs, the second UPDATE blocks on the row lock, re-evaluates its predicate against the newly committed row, matches nothing and affects zero rows; control flow falls into the INSERT and the transaction commits, and that is the oversell. InnoDB's REPEATABLE READ ends the same way for this statement, because an UPDATE takes a current read rather than the snapshot read: it blocks, applies against the latest row, affects zero rows and does not abort. PostgreSQL REPEATABLE READ is the one that does not oversell, and not because the code is right: it raises SQLSTATE 40001 on a row a concurrent transaction updated after the snapshot, which aborts the whole transaction, so the unchecked INSERT can never commit. That is failing closed on an exception no handler anticipated, on one of the three configurations, at the cost of an error the caller has to catch and retry. Two of three commit the oversell, so the rowcount check is not optional, and the invariant belongs in a continuous monitor as well as in a test: assert per key that the sum of allocation.qty in status 'held' or 'committed' does not exceed the position quantity, and let the reconciliation job re-sum movements into positions and raise a variance when they disagree.

Follow-up

  • An allocation must span two positions atomically. What changes in your mechanism, and what if the two live in different shards?
  • Under the retry design, what stops a hot key from livelocking when fifty allocators all retry together?
  • The six oversells are already promised to customers. What is the compensating path, and what does it write to inventory_movement?

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.

Review and mentoring: what you let through

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

Name a time when you faced a challenge that you couldn't complete?

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. Name the disagreement and how you resolved it with evidence.

Follow-up

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

How would you describe your leadership style?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Name the disagreement and how you resolved it with evidence.

Follow-up

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

Describe leadership in one sentence.

Medium · behavioural and engineering judgement

Approach

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

Follow-up

  • What would you do differently if you ran that again?
  • What did you decide not to do, and why?