cswork / libraryOpen practice workspace

Interview guides · Arclin

Arclin Software Engineer Interview Questions & Guide 2026

Caching practice for Arclin software engineer prep: settle the staleness budget before choosing an invalidation strategy.

The 60-second brief

A Software Engineer at Arclin plays a pivotal role in bridging the gap between advanced chemical manufacturing and digital operational efficiency. You are responsible for developing, maintaining, and optimizing the software systems that support Arclin’s diverse production environments. Your work directly impacts how the company monitors quality, manages process data, and scales its industrial output.

Ask how many rounds there are and what each one is before you plan your weeks, because the composition is what your hours should follow. A loop with three coding rounds and one short design conversation deserves a different split from the reverse, and whoever schedules it will usually say plainly which it is.

Focus areas

  • Model units, lots and state machines precisely
  • Make every external side effect safe to retry
  • Fold late, out-of-order scan events by event time

Overview

This position is particularly significant because Arclin operates in a highly specialized, technical space where software must be robust enough to handle real-time manufacturing demands. You will move beyond simple application development, often collaborating with cross-functional teams to integrate digital solutions into physical production plants. It is a role that requires both technical precision and a deep understanding of operational workflows.

The Arclin interview process

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

Recruiter 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

Virtual Interviews

Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.

What to demonstrate

  • Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
  • Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
  • Whether you can be redirected in the middle of an answer without restarting it from the beginning
  • Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it

How to prepare

  • Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
  • Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
  • Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing

In-Person Interviews

When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.

What to demonstrate

  • Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
  • Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
  • Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
  • Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened

How to prepare

  • Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
  • Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
  • Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure

Leadership Assessment

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Where candidates lose points

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.

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.

Finishing a solution without stating its complexity

Give time and space in the same breath as the code, and define n explicitly when there are two sizes, since n nodes and m edges are not interchangeable. Space is the half that gets skipped: count the auxiliary structures you allocate and the recursion stack at its deepest, not only the answer you hand back.

Starting work without saying what you are about to spend time on

State the plan before executing it: the approach, roughly how long it will take, and what you intend to leave hand-waved. That gives the interviewer a chance to redirect you in ten seconds rather than watching you spend fifteen minutes on the wrong sub-problem.

Questions & practice

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

  1. 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.
  2. 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 in rem, with dp[0] = (0, 0) and the value a (node_count, total_distance) pair compared lexicographically. Let b be the lowest set bit of rem; any cover must contain some node that stocks line b, 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.
  3. Quote the memory for what you actually store, not for a count array alone. Over 2^25 masks you need three dense arrays: uint8 node count (33.5 MB), uint32 total distance (134.2 MB, integer metres so the tie-break compares exactly and the array stays fixed-width), and uint8 chosen 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.
  4. Reconstruction is free in this formulation and needs no parent-mask array: because every transition eliminates the lowest uncovered line, the predecessor of rem under the stored choice v is exactly rem & ~cover(v), so walk rem from the full mask down to 0 emitting choice[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 of ceil(popcount(rem) / max_v popcount(cover(v) & rem)), which prunes long before the theoretical bound bites.
  5. 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.
  6. 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.

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?

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

  1. 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.
  2. Parse the decimal as an integer mantissa plus a scale. 4.500 becomes (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.
  3. Convert by integer arithmetic and demand exactness: numerator = mantissa * factor; emit numerator / 10^scale only when numerator % 10^scale == 0, otherwise reject the line with a reason code. Worked: 4.500 CS at 12 eaches per case gives 54000/1000 = 54, exact. 0.125 PL at 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.
  4. 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.
  5. 20260317 carries 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.

Worked solution (20 min)

  1. Write the integer conversion as a pure function (mantissa, scale, factor) -> Result<int64> and unit-test it before touching the file reader.
  2. Table-drive the cases: exact conversion, non-dividing conversion, a mantissa near int64 overflow, a zero quantity, and a unit not in the ladder.
  3. Wire the scanner, emitting one rejection row per bad line with the raw segment attached so the partner can be shown what was sent.
  4. Feed a fixture file containing a date on a day the destination zone changes offset, and assert the resolved instant against a hand-computed value.
  5. Re-run the whole file and confirm identical output, including identical rejections.

Expected result: 4.500 CS at factor 12 yields 54 eaches; 0.125 PL at factor 36 is rejected as non-exact rather than stored as 4 or 5; every emitted row carries both the eaches integer and the source unit; the date-only field resolves through the node's zone, not the server's.

Verify it yourself

  • No float type appears anywhere between the file bytes and the stored quantity — grep the parser for it.
  • Summing the emitted eaches and converting back to the source unit reproduces the source decimal exactly for every accepted line.
  • Changing the process time zone leaves every resolved occurred_at unchanged.

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 +0100 on some dates. Does that change your storage decision for a future appointment as well as a past event?

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?

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.

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?

Add a required unit column and uniqueness to a live ledger

Hard · online migration · lock timeouts · unique constraints

inventory_movement holds 4.2 billion rows on PostgreSQL 15 and takes about 2,000 inserts per second around the clock. Two changes are required with no downtime: add uom_code TEXT NOT NULL, whose truthful value must come from the conversion factors in force at each row's occurred_at rather than from today's item master; and make idempotency_key UNIQUE, although duplicates predating the retry fix already exist. Give the ordered steps, the lock each one takes, its expected duration and its rollback.

Approach

  1. ADD COLUMN with a non-volatile DEFAULT has been metadata-only since PostgreSQL 11, so no rewrite, but it still takes ACCESS EXCLUSIVE for an instant. That lock queues behind any long-running reader while blocking every statement behind it, so one 10-minute analytics scan turns a millisecond change into a 10-minute outage. Set lock_timeout to 2s and retry in a loop rather than waiting.
  2. Reach NOT NULL without a scan under an exclusive lock: ADD CONSTRAINT ... CHECK (uom_code IS NOT NULL) NOT VALID, which is instant, then VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE and runs concurrently with inserts, then SET NOT NULL, which PostgreSQL 12 and later proves from the validated check and so skips its own scan. Drop the redundant check afterwards.
  3. The truthful backfill is the long pole and must read the versioned conversion factor as of each row's occurred_at, not the current item master, or a supplier moving a case from 12 units to 10 retroactively reinterprets last quarter. Batch by primary-key range, a few thousand rows per transaction, with a sleep between batches and a resumable cursor in a control table. At 5,000 rows a second, 4.2 billion rows is roughly ten days of throttled work: that is a schedule, not a failure, and it must not block the deploy.
  4. The UNIQUE index cannot be built until the duplicates are gone. CREATE UNIQUE INDEX CONCURRENTLY aborts on the first violation and leaves an INVALID index behind that is ignored by queries but still carries update overhead on every insert, and it can only be removed with DROP INDEX CONCURRENTLY. So: find the duplicates, resolve each one as a compensating reversal rather than a DELETE, since every duplicate key is a double-applied movement and deleting it fixes the constraint while leaving the position wrong.
  5. Then CREATE UNIQUE INDEX CONCURRENTLY, which makes two passes over the table and waits out concurrent transactions, so budget hours here, and note it cannot run inside a transaction block: a migration framework that wraps every step in BEGIN will fail on this one. Promote it with ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX, which is a brief ACCESS EXCLUSIVE.
  6. Question whether the constraint should cover all history. At roughly 40 bytes per key plus overhead, a btree over 4.2 billion rows is in the hundreds of gigabytes, while a retry never arrives a year later. A partial unique index bounded by a fixed occurred_at literal, rotated forward on a schedule, gives the same protection for a fraction of the size, and the predicate must be a literal because now() is not immutable.

Worked solution (45 min)

  1. Write the migration as numbered steps with the lock mode, expected duration and rollback beside each, including the lock_timeout retry wrapper on every ACCESS EXCLUSIVE step.
  2. On a copy at reduced scale, run ADD COLUMN while a long SELECT is open and observe writers queueing behind the lock request, then repeat with lock_timeout set and confirm the migration backs off instead.
  3. Run the batched backfill against a versioned conversion table, kill it mid-run and restart it, confirming the control-table cursor resumes without redoing or skipping a batch.
  4. Seed duplicate idempotency keys, attempt CREATE UNIQUE INDEX CONCURRENTLY, and confirm the INVALID index left behind, then drop it concurrently, resolve the duplicates as reversals and rebuild.
  5. Compare the full unique index size against the partial one bounded by a fixed occurred_at literal.

Expected result: A step list where no statement holds ACCESS EXCLUSIVE for more than the lock_timeout, uom_code reaches NOT NULL without a table rewrite or a blocking scan, and idempotency_key carries a valid unique constraint with every pre-existing duplicate resolved by a compensating movement rather than a delete.

Verify it yourself

  • pg_stat_activity shows no statement blocked on this table for longer than lock_timeout during any step.
  • Row counts and SUM(delta_qty) per key are identical before and after the whole migration, except where a duplicate was reversed.
  • The failed-index rehearsal leaves an index marked invalid in pg_index, proving the cleanup step is needed rather than theoretical.
  • Every backfilled uom_code matches the factor in force at that row's occurred_at, verified on a sample spanning a known pack-size change.

Follow-up

  • Sequence the application deploy against these steps. When exactly may the code start writing uom_code, and when may it start requiring it?
  • The backfill is on day six and the conversion-factor table is itself corrected. What happens to the rows already written?
  • How do you roll back after SET NOT NULL is in place and half the traffic depends on the column?

How would you approach a project that requires integration between legacy systems and new software?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. State your assumptions explicitly before working the problem.
  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?

How do you ensure quality control when implementing new software solutions?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. Say what you would check first and why it is the highest-information step.
  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?

Allocate stock on hot item-node pairs without overselling

Medium · lock contention · concurrency control · sharding · hot keys

Allocation peaks at 3,000 writes per second and 70 percent of it lands on about 50 (item, node) pairs during a promotion, roughly 42 per second against a single position row. The measured allocate transaction is 4 ms. Design the call so the availability check and the allocation insert cannot interleave: name the mechanism, state its throughput ceiling on one row, and say what the losing caller sees. Then say what changes when one order line must claim quantity at two nodes atomically.

Approach

  1. Put the check and the write in one atomic step, in one of two shapes. Pessimistic: SELECT the position FOR UPDATE, verify on_hand minus outstanding held and committed allocations covers the request, insert the allocation and bump version. Optimistic: UPDATE inventory_position ... WHERE key = ... AND version = ? AND qty - reserved >= ?, and test the affected-row count. A read followed by an unconditional insert is the defect, and no isolation level repairs it.
  2. Do the ceiling arithmetic. The row lock is held from the moment it is taken until commit, so a 4 ms transaction supports roughly 250 allocations per second on one row against the 42 demanded, about six times headroom. That headroom is the entire budget, which is why nothing remote may sit inside the transaction: a 200 ms carrier or rating call drops the same row to about 5 per second, eight times under demand, and the pair queues until the pool is exhausted.
  3. Be exact about isolation rather than waving at it. Under PostgreSQL REPEATABLE READ a second writer to the same row aborts with a serialization failure the application must catch and retry; under InnoDB REPEATABLE READ a locking read sees the latest committed row and blocks instead, so identical code is safe on one engine and not the other. READ COMMITTED with a locking read or a version-predicated UPDATE is the portable form.
  4. Define the loser's experience. A serialization failure or a zero-rowcount CAS is an expected outcome, not an error to log and swallow: retry with jittered backoff a bounded number of times, then return a typed insufficiency so sourcing picks another node. Uncapped retries against one hot row are the real outage, so cap attempts and shed.
  5. Shed without overselling by making the hot key single-writer: route allocations through a log partitioned on hash(item_id, node_id) so one process touches the row and contention becomes queueing instead of lock waiting. The costs are head-of-line blocking per key and duplicate delivery on rebalance, so the consumer still needs the partial unique index on allocation to make a replay fail closed rather than double-claim.
  6. For two nodes atomically: inside one database it is one transaction taking both position rows in a deterministic order, ascending by primary key, so two lines claiming the same pair cannot deadlock. Across shards no such transaction exists, so it becomes a saga - hold at A, hold at B, release A by a compensating status change on failure - and the pair is briefly held but uncommitted, which is a state the order line's machine must represent.

Worked solution (30 min)

  1. Write both implementations against inventory_position and allocation: SELECT ... FOR UPDATE, and UPDATE ... WHERE version = ? with a rowcount test.
  2. Compute the single-row ceiling from the 4 ms hold, compare it to 42 per second, then recompute it with a 200 ms external call inside the transaction and state what breaks first.
  3. Write the concurrency test that fails without protection: N threads allocating against a position of 1, asserting exactly one allocation in status held and exactly one version increment.
  4. Extend to two nodes in one shard with the lock-ordering rule, then write the cross-shard saga with its compensating release and the line status that represents the half-held state.

Expected result: An atomic check-and-claim - a row lock or a version-predicated UPDATE whose affected-row count is tested - with allocation's partial unique index making a retry fail closed; a stated ceiling near 250 allocations per second on one row at a 4 ms hold against 42 demanded; no remote call inside the transaction; and a cross-shard split expressed as a saga with an explicit compensating release rather than a distributed transaction.

Verify it yourself

  • With 200 concurrent allocators against qty 1, exactly one succeeds, no position goes negative, and no second allocation row exists.
  • Every failure mode maps to a named caller behaviour: retry with jitter, or re-source.
  • The transaction contains no network call other than to the database.
  • The throughput ceiling claimed for the chosen mechanism is derived from a measured hold time rather than asserted.

Follow-up

  • Two lines claim the same two nodes in opposite order and the engine kills one. What is the lock ordering rule, and what does the survivor's retry look like?
  • Soft allocations expire. What does the sweeper do, and why is it a correctness component rather than housekeeping?
  • Demand on one pair reaches 400 per second. Which of the two mechanisms do you move to, and what do you give up?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.

Day 1 — Numbers before diagrams

  • Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
  • Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
  • Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.

Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.

Day 2 — One system, from requirements to schema

  • Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
  • Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
  • Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.

Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.

Day 3 — The consistency you are actually buying

  • Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
  • Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
  • Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.

Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.

Day 4 — Failure is the design

  • For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
  • Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
  • Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
  • Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.

Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.

Day 5 — Scaling the hot path

  • Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
  • Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
  • Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
  • Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.

Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.

Day 6 — Keep the coding hand in, at the bar that applies to you

  • Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
  • Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
  • Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.

Deliverable: One problem hardened to review standard, with its test list and one written review comment.

Day 7 — Defend it while being interrupted

  • Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
  • Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
  • Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.

Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.

Scope is read from what you decided

Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.

Where do you see yourself within 5 years?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Pick a story where you made the decision, not one where you watched it.
  3. 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?
  • What did you decide not to do, and why?

Would you be willing to relocate for a different opportunity in the future?

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. Give the blast radius: what could have broken, and what you measured.

Follow-up

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

What does your first week look like if offered a job here?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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

You have a strong understanding of engineering principles; would you be open to working at a different production plant that could better utilize your talent?

Medium · behavioural and engineering judgement

Approach

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