cswork / libraryOpen practice workspace

Interview guides · HX5

HX5 Software Engineer Interview Questions & Guide 2026

HX5 software engineer prep on partial failure: a timed-out request has an unknown outcome, not a failed one.

The 60-second brief

As a Software Engineer at HX5, you play a vital technical role supporting complex aerospace, defense, and federal government initiatives. This position involves designing, developing, and maintaining software systems that drive critical mission operations, testing infrastructure, and hardware-software integration. Your work directly impacts high-stakes environments such as NASA research centers and military installations, where reliability and precision are paramount.

Prepare in one language you know well enough to debug in rather than the one you think reads best. Under a clock an unfamiliar language costs you standard-library lookups and iteration mechanics, and that time comes out of your thinking budget, not your typing budget.

Focus areas

  • Make every determination replayable from a frozen snapshot
  • Treat accessibility conformance as a functional requirement
  • Model legal holds before modelling record destruction

Overview

The scope of work spans diverse domains, including DevSecOps, LabVIEW-based control systems, IT infrastructure, and specialized mission support applications. You will collaborate closely with multidisciplinary engineering teams, program managers, and government stakeholders to translate operational needs into robust digital solutions. The environment demands rigorous engineering standards, adaptability, and an ability to navigate complex system requirements.

Expect a mission-driven culture where technical excellence meets public service impact. While processes can move quickly from initial contact to offer, the technical expectations require a deep understanding of engineering fundamentals and practical problem-solving. Success in this role means combining strong coding capabilities with an appreciation for the unique operational constraints of government and aerospace clients.

The HX5 interview process

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

Phone 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

Technical Interviews

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 Interviews

Your first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.

What to demonstrate

  • Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
  • Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
  • Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you

How to prepare

  • Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
  • Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
  • Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits

Where candidates lose points

Leaving accessibility conformance to a review at the end of the project

The obligation is usually statutory and expressed against WCAG success criteria at AA, and those criteria cut straight through component architecture rather than sitting on top of it. Custom select menus, modals and date pickers have to manage focus and expose correct roles and names; 2.4.11 Focus Not Obscured (AA) constrains sticky headers and cookie bars; 2.5.8 Target Size (Minimum) (AA) requires a 24 by 24 CSS pixel target unless a listed exception applies; 3.3.8 Accessible Authentication (Minimum) (AA) rules out a login step that depends on a cognitive function test with no alternative; and 2.2.1 Timing Adjustable (Level A) collides directly with the short session timeout a security review asked for, since the user must be able to extend it. Retrofitting these means rewriting the component layer and the session design, so they belong in the component contract and in the definition of done from the first sprint.

Using soft delete (deleted_at IS NOT NULL) as the answer to a records-retention requirement

It fails both directions at once. The obligation to destroy is unmet because the bytes remain in the primary table, the backups, the search index, the analytics extract and any vendor queue that received a copy, and a competent disclosure request or audit will find them. Meanwhile the record is invisible to staff who legitimately need it, so the organisation loses the ability to answer questions it is still required to answer. Retention needs a destruction pipeline that enumerates derived copies, checks legal hold at execution time under lock, emits a receipt, and fails closed when a copy is unreachable - and the tombstone row that proves destruction happened has to survive the destruction.

Tests that assert on the implementation rather than the behaviour

Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.

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

Find every determination a corrected reference table can change

Medium · graph-traversal · blast-radius · versioning

A reference table (a rate schedule, threshold list or jurisdiction list) is corrected: version v is superseded by v' governing [p1, p2]. You have a graph of about 1,800 rule_version nodes, 40,000 clause nodes and 5,000 reference-table-version nodes, with roughly 90,000 reads edges from clause to table version; each rule_version has a governing date range. Determinations number 60 million, each carrying rule_version_id and effective_from. Return the determination set that could change. Name the traversal, its complexity, and what you do if the graph contains a cycle.

Approach

  1. Traverse backwards from the corrected table version, not forwards from every rule. Reverse the reads edges once into an adjacency list keyed by table version, then breadth-first from v' to collect the clauses that read it and the rule_versions that own those clauses. One BFS is O(V + E), about 135k nodes and edges; the forward alternative asks reachability per rule_version and is O(V times E).
  2. Filter each edge by period intersection as you traverse. A rule_version only reads the corrected text if its governing range intersects [p1, p2]; an edge to a rule whose period ended before p1 is a historical fact, not a dependency, and following it inflates the replay set with determinations that provably cannot move.
  3. Intersect at the determination level with a second predicate, not by scanning 60 million rows: the affected rule_version_id set drives an index range scan on (rule_version_id, effective_from), and effective_from must fall inside [p1, p2]. The graph part is milliseconds; the determination part is what the index has to be built for.
  4. Detect cycles rather than assuming a DAG. A reference table derived from aggregate determination output and read back by a rule is a real configuration, and it is a defect, but a naive BFS with a visited set will terminate quietly and give an answer that hides it. Mark the cycle, report it, and cap the traversal depth so a malformed edge cannot stall the job.
  5. Report the set with its provenance: which clause chain connects each rule_version to the corrected table. A blast radius nobody can explain is a number an auditor will not accept, and the same chain is what lets a reviewer overrule an over-broad inclusion.

Follow-up

  • The corrected table is read by a clause that is itself behind a feature guard never enabled in that period. Do you keep those rule_versions in the set?
  • Two reference tables are corrected in the same maintenance window. Is the combined blast radius the union of the two, and when is it larger?
  • The reads edges are inferred from a static scan of clause source rather than declared. What does that do to the soundness of your answer?

Parse a fixed-width record with packed-decimal amount fields

Easy · parsing · encoding · legacy-integration

A nightly file from a system of record arrives as fixed-length 180-byte records in EBCDIC code page 037. Per the copybook, bytes 1-40 are a character name field, bytes 41-48 a character date, bytes 49-52 an amount declared PIC S9(5)V99 COMP-3, and the rest character. The trailer record carries a record count and the sum of every amount field. Write the parser and state, in minor currency units, what a whole-record EBCDIC-to-ASCII text conversion does to a packed amount before it ever reaches your validation.

Approach

  1. Parse by field against the copybook offsets, never by transcoding the record. Slice bytes 1-40 and 41-48 and decode those with cp037; slice bytes 49-52 and hand them to a packed-decimal unpacker that never sees a codec.
  2. Unpack COMP-3 explicitly: two BCD digits per byte, and in the final byte the high nibble is the last digit while the low nibble is the sign. Nibble C and F are positive and D is negative under the common convention, so assert the sign nibble rather than defaulting an unrecognised one to positive. S9(5)V99 is seven digits plus a sign nibble, which is eight nibbles, which is exactly the four declared bytes.
  3. Return the value as an integer count of minor units. V99 is an implied decimal position with nothing stored for it, so the seven unpacked digits already are the minor-unit amount; converting to a float to reinsert a decimal point reintroduces the representation error the packed format exists to avoid.
  4. Reject the batch on the trailer, not per record. Accumulate the record count and the integer sum of every amount, compare to the trailer, and fail the whole file rather than post a partial batch. A corrupted amount that still parses is invisible to per-record validation and visible only to the control total.
  5. Complexity is O(total bytes) with O(1) working memory beyond one record buffer and two accumulators; the file is read as a byte stream, so nothing here needs the file resident.

Follow-up

  • The copybook gains an OCCURS DEPENDING ON clause, so record length now varies with a count field. How does your offset arithmetic change?
  • A record contains a sign nibble of 0xA. Is that valid, and what does your parser do with it?
  • The sender starts padding numeric character fields with EBCDIC zeros in a field you decode as text. What breaks and where do you catch it?

Replay sixty million determinations after a retroactive rule correction

Hard · bulk-replay · inverted-index · determinism

A regulator corrects three clauses of a ruleset with effect six years back. Sixty million determination rows fall under the affected rule family, each carrying rule_version_id, evidence_snapshot_hash and a trace of the clauses that fired. Evaluation is a pure function costing about 8 ms of CPU, but each replay must rehydrate the frozen evidence snapshot from a content-addressed store capped at 500 fetches per second shared with live traffic. Replaying everything is correct. Explain why it is not viable, and give the algorithm that is.

Approach

  1. Cost the naive run before optimising it. CPU is not the constraint: 60 million evaluations at 8 ms is about 133 CPU-hours and parallelises freely. The store is: 60 million fetches against a 500 per second ceiling is 120,000 seconds, roughly 33 hours of wall clock while consuming the entire budget live traffic also needs. The run is correct because replay is deterministic and unaffected determinations converge on the existing row through the partial unique index, so it is purely a cost problem.
  2. Build an inverted index from clause to determination. Either a separate determination_clause table indexed on clause_id, or a GIN index over the trace array. The three corrected clauses then yield a candidate set by index scan instead of a full scan, typically well under one percent of the 60 million rows.
  3. Recognise that the fired-clause filter alone is unsound. A correction that widens a guard makes a clause newly reachable, so determinations that never reached it before can change now, and those are precisely the denials the correction was issued to reverse. If the trace only records clauses that fired, the filter silently omits the population the regulator cares about.
  4. Close the gap with what the trace can actually support, and be explicit that the target is a sound superset, not an exact set. If the trace records evaluated-and-false clauses as well as fired ones, include both: every determination that can change is then selected, along with some that cannot — a determination that already passed the widened guard is selected and replays identical, because widening a guard cannot exclude an input it already admitted. That asymmetry is the correct trade. A false positive costs one rehydration; a false negative is an uncorrected determination. If the trace records only fired clauses, fall back to a wider superset: every determination under the affected rule_versions whose effective period intersects the corrected period, which is far larger than the clause filter and still orders of magnitude under 60 million.
  5. Deduplicate the rehydration, not the evaluation. Distinct evidence_snapshot_hash values are the unit of store work, and a single snapshot attached to several cases in a household or an appeal chain is fetched once and evaluated per determination. Order the candidate scan by hash so the cache hits are adjacent rather than scattered across the run.
  6. Make the run resumable and rate-limited by construction: a durable cursor over the candidate set, a token bucket sized to the share of the 500 per second ceiling you are permitted, and writes that rely on the uniqueness key rather than a read-then-insert, so a restart after a crash converges instead of producing a second current determination.

Worked solution (40 min)

  1. Write out the naive budget explicitly: 60,000,000 divided by 500 fetches per second, converted to hours, next to the 133 CPU-hours, and state which number is the constraint.
  2. Construct a ruleset with a guard clause G and a downstream clause C, where G is a pure reachability guard — it admits or blocks, and changing it changes nothing C reads. Build 1,000 determinations: 60 passed G and fired C, 90 evaluated G and failed it so C was never reached, and 850 took an earlier branch and never evaluated G at all.
  3. Apply a correction that widens G so all 90 previously-failing determinations now pass it and reach C. Establish ground truth first by replaying all 1,000 and recording which results differ; then run the fired-clause filter and count how many of that changed set it selects.
  4. Re-run with the filter extended to evaluated-and-false clauses, and compare its candidate set against the control's changed set in both directions separately.

Expected result: The control run changes exactly the 90 that the widened guard now admits. The fired-clause filter selects 60 and none of them changed, so it selects 60 useless candidates and misses all 90 real ones. The extended filter selects 150: a sound superset of the changed set, not a match for it. It contains all 90 changed determinations, and its other 60 candidates are the ones that already fired C — they are selected and replay byte-identical, which is the expected cost of soundness, not a defect. The 850 that never evaluated G are outside both filters and also replay identical, confirming they were correctly excluded rather than luckily unchanged.

Verify it yourself

  • Every determination the control run reports as changed appears in the extended filter's candidate set: no false negatives on this fixture. The converse does not hold and must not be asserted — 60 of the filter's 150 candidates replay identical — so the filter is verified as a superset, and the run is sized for 150 rehydrations to correct 90 determinations.
  • Re-running the extended filter twice produces the same candidate set and writes no second current determination for any case, which exercises the uniqueness key rather than trusting it.
  • Killing the run at a random point and restarting from the cursor produces the same final state as an uninterrupted run.

Follow-up

  • The correction changes an amount rather than an outcome. Does your candidate filter change, and does the notice obligation?
  • A replayed determination comes back identical. Do you write anything at all, and what does an auditor need to see that you checked it?
  • Halfway through the run the regulator issues a correction to the correction. What happens to the work already done?

Add a uniqueness rule to a live payments table with duplicates

Hard · online migration · concurrent index build · lock queue · compensating rows

disbursement_instruction(instruction_id BIGSERIAL PK, determination_id BIGINT, benefit_period_start DATE, amount_minor BIGINT, status TEXT, reversal_of_instruction_id BIGINT NULL) has 40M rows and takes writes continuously. It was built without UNIQUE (determination_id, benefit_period_start); a retried batch created 11,400 duplicate pairs, and 3,900 of those duplicates reached settled. Money that moved is never deleted and never voided — it is unwound with a compensating reversal row carrying reversal_of_instruction_id, which repeats the same business key. status values include voided. Produce the migration: the query that finds the duplicates, what you do with the ones that settled, the DDL that adds the constraint without blocking writers, and the deploy ordering. Name the lock each statement takes and what happens when it cannot be acquired.

Approach

  1. Find the duplicates with a window rather than a self-join: SELECT instruction_id, status, ROW_NUMBER() OVER (PARTITION BY determination_id, benefit_period_start ORDER BY instruction_id) AS n FROM disbursement_instruction and inspect every row with n > 1 together with its status, because the remediation differs entirely by status. The n = 1 row is the one that stays live; the rest are what you unwind.
  2. Do not make the data fit the constraint by deleting rows. A duplicate that reached settled moved real money and the rail's own ledger will not forget it; deleting the row makes your records disagree with the counterparty. The 7,500 unsent duplicates may be set to voided. The 3,900 settled ones get compensating reversal rows referencing them via reversal_of_instruction_id — and stay.
  3. That remediation is what rules out the obvious predicate. After it, each settled duplicate key still holds three non-voided rows: the original, the settled duplicate and its reversal. CREATE UNIQUE INDEX CONCURRENTLY ... WHERE status <> 'voided' therefore aborts with 23505 on the first of the 3,900. The fact the predicate needs — 'this row has been unwound' — cannot be a subquery either: a partial index predicate may reference only columns of the row being indexed and must be IMMUTABLE. So the fact has to be written onto the row.
  4. Add it as a nullable column with no default, which is a catalog change and not a table rewrite (PostgreSQL 11 extended that to non-volatile defaults as well): ALTER TABLE disbursement_instruction ADD COLUMN reversed_by_instruction_id BIGINT. It still takes ACCESS EXCLUSIVE for the catalog write, so it carries the same lock_timeout guard as any other ACCESS EXCLUSIVE step; add the self-referencing foreign key separately as NOT VALID and then VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE.
  5. The constraint is then a partial unique index over the rows that are live originals: CREATE UNIQUE INDEX CONCURRENTLY uq_instr_business_key ON disbursement_instruction (determination_id, benefit_period_start) WHERE status <> 'voided' AND reversal_of_instruction_id IS NULL AND reversed_by_instruction_id IS NULL. Each conjunct does one job — never sent, is itself an unwind, has been unwound. Accept the consequences: a partial index cannot back an ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX, so there is no named constraint and no foreign key can reference this key, and INSERT ... ON CONFLICT must repeat all three conjuncts in its inference clause.
  6. Prove the data fits before you build. Run SELECT determination_id, benefit_period_start FROM disbursement_instruction WHERE <the index predicate> GROUP BY 1,2 HAVING count(*) > 1 and require zero rows. A unique build that hits a violation is not free: CREATE INDEX CONCURRENTLY detects it in the second pass, aborts after scanning 40M rows twice, and leaves an index with indisvalid = false that must be dropped with DROP INDEX CONCURRENTLY before retrying.
  7. Name the locks. CREATE INDEX CONCURRENTLY takes SHARE UPDATE EXCLUSIVE, so readers and writers continue; it makes two passes plus waits for concurrent transactions and cannot run inside a transaction block. A plain CREATE INDEX or an ALTER TABLE variant takes ACCESS EXCLUSIVE, which queues behind any running query and blocks every reader that arrives behind it in the queue — a five-millisecond statement becomes an outage while one long report finishes. Guard every ACCESS EXCLUSIVE step with SET lock_timeout = '2s' and retry in a loop, so a failed acquisition ends as a failed migration step instead of a stalled table.
  8. Order the deploy so the window is never open: add the column; ship the application code that derives the idempotency key from (determination_id, benefit_period_start), treats 23505 as a converged retry, and stamps reversed_by_instruction_id whenever it writes a reversal; remediate the existing duplicates in chunks; re-run the residual-duplicate check; build the index concurrently; verify indisvalid in pg_index. Building the index before the code can handle a violation converts every legitimate retry into a user-visible error.

Worked solution (45 min)

  1. Reproduce at reduced scale: load a few million rows with an injected duplicate rate and a mix of statuses, and keep a writer running throughout the migration.
  2. Run the ROW_NUMBER query, bucket the duplicates by status, and write the two remediation statements — voided for unsent, a compensating reversal plus reversed_by_instruction_id on the duplicate for settled.
  3. Attempt CREATE UNIQUE INDEX CONCURRENTLY ... WHERE status <> 'voided' against the remediated data and watch it abort on 23505; inspect pg_index.indisvalid and drop the leftover index concurrently.
  4. Add reversed_by_instruction_id, backfill it for the remediated duplicates, and re-run the residual-duplicate check under the full three-conjunct predicate until it returns zero rows.
  5. Attempt a plain CREATE UNIQUE INDEX with a long-running SELECT open in another session and observe the writer behind it stall; roll it back.
  6. Run CREATE UNIQUE INDEX CONCURRENTLY with the full predicate while the writer is still running, and time it.
  7. Replay the original duplicating batch and confirm the second attempt raises 23505 and the application converges on the existing instruction.

Expected result: The residual-duplicate check under the index predicate returns zero rows before the build starts, and the concurrent build then completes with the writer uninterrupted and indisvalid = true. Settled duplicates survive as rows: the n = 1 original stays inside the predicate, the duplicate carries reversed_by_instruction_id and its reversal carries reversal_of_instruction_id, so both fall outside it. Unsent duplicates carry status = 'voided' and are outside it too. A replayed batch pays once, and the duplicate attempt is visible as a counted 23505 rather than a second payment.

Verify it yourself

  • SELECT indisvalid FROM pg_index WHERE indexrelid = 'uq_instr_business_key'::regclass returns true — a failed concurrent build leaves an index that exists and enforces nothing.
  • The GROUP BY ... HAVING count(*) > 1 check under the index predicate returns no rows both before and after the build: the build enforces the invariant, it is not how you discover whether the data satisfies it.
  • Leaving one settled duplicate unremediated reproduces the abort — 23505 during the build and indisvalid = false afterwards — which is the direct proof that the predicate, not the status column alone, is what makes the data fit.
  • Net settled amount per (determination_id, benefit_period_start) after remediation equals the intended single payment, with the reversal accounted for.
  • During the concurrent build, the background writer's error count stays zero and its latency percentile does not step change; and ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX on the partial index fails, confirming you understood what you traded away.

Follow-up

  • The concurrent build fails at 90% because of a deadlock with a nightly batch. What is on disk now, and what exactly do you run before retrying?
  • A duplicate settled and then the original was returned by the rail. What does the compensating row look like, and what is the account's net position?
  • Once the partial index exists, how does the batch writer's ON CONFLICT inference clause have to be written against a three-conjunct predicate, and what happens if the predicate is omitted?

Destroy an expired record without soft-deleting it

Easy · retention · soft delete · legal hold · tombstone

document(document_id BIGINT PK, content_hash BYTEA, storage_uri TEXT, case_id BIGINT NULL, retention_class TEXT, retention_expires_on DATE NULL, legal_hold_id BIGINT NULL, destroyed_at TIMESTAMPTZ NULL, destruction_receipt_id BIGINT NULL) stores one row per attachment; identical bytes are shared across cases through content_hash. retention_expires_on stays NULL until the case closes. Write the query that selects attachments eligible for destruction, and the second predicate that decides whether the stored bytes at storage_uri may also be deleted. Say what the row looks like after destruction and why it is not deleted.

Approach

  1. Separate the two decisions. Whether an attachment's retention obligation has ended is a property of the row's context: WHERE retention_expires_on IS NOT NULL AND retention_expires_on <= CURRENT_DATE AND legal_hold_id IS NULL AND destroyed_at IS NULL. Note that a NULL retention_expires_on means the triggering event has not happened, so the clock has not started — three-valued logic already excludes it, but say so out loud rather than leaving it to luck.
  2. Whether the bytes may go is a property of the blob, shared across attachments: NOT EXISTS (SELECT 1 FROM document o WHERE o.content_hash = d.content_hash AND o.document_id <> d.document_id AND o.destroyed_at IS NULL). This needs an index on content_hash; without one it is a sequential scan per candidate. Evaluate it after this batch's tombstones are written and inside the same transaction: if two siblings expire together and you test before writing them, each still sees the other as live and the bytes survive a destruction you have already receipted.
  3. Make the hold check happen at execution, not at planning. Run the sweep as two phases: select candidate ids, then in a second transaction SELECT ... FROM document WHERE document_id = ANY($1) AND legal_hold_id IS NULL AND destroyed_at IS NULL FOR UPDATE and act only on the rows that survive the re-read. A hold placed between the phases must stop the destruction, and the row lock is what makes that check binding.
  4. Describe the post-destruction row precisely: the same document_id, destroyed_at set, destruction_receipt_id pointing at a receipt row, storage_uri retained as a dangling reference or nulled, and content_hash kept so a later disclosure request can be answered with 'these bytes existed and were destroyed on this date under this schedule'. The tombstone is the proof and must outlive the payload.
  5. State what soft delete fails to do: setting deleted_at leaves the bytes in the primary table, the backups, the OCR text layer, the search index, the analytics extract and the print vendor's queue, so the obligation to destroy is unmet while staff who could legitimately answer a question can no longer see the record. It fails both halves at once.
  6. Fail closed. If a derived copy cannot be reached, the sweep records the attachment as attempted-and-incomplete rather than writing destroyed_at, because a receipt that claims destruction the system did not achieve is worse than no receipt.

Follow-up

  • The OCR text layer and the search index both hold extracts of a destroyed document. How does the destruction job know they exist, and what does it do when the search cluster is unreachable?
  • A restore from a backup taken before the sweep reinstates destroyed rows. What in your model detects that, and what does it do?
  • Two attachments share content_hash but carry different sensitivity_label values. Which one governs what a viewer is allowed to see?

Why do you feel you are suited for this position, and what specific skills can you bring to do the job right?

Medium

Approach

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

Follow-up

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

Can you discuss your background, experience, and professional expectations for this role?

Medium

Approach

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

Follow-up

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

What is your name again? (Note: Be prepared for conversational, introductory validation where interviewers verify resume details.)

Medium

Approach

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

Follow-up

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

What is the difference between working with a standard commercial co-worker and a government customer in this domain?

Medium

Approach

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

Follow-up

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

Size and partition an append-only access log

Medium · partitioning · append-only · write amplification · retention

audit_access_log takes one row per access to a record carrying personal data, from web UI, internal API, batch jobs, report extracts, and support tooling: roughly 35 million rows a day, peaking at 3,000 writes a second, retained six years because audit retention outlives the records it describes. Rows are never updated. Investigators query by subject_id over a date range and by actor_id over a date range; nobody queries by request_id at scale. Choose the partitioning key, the indexes, the write path, and the destruction mechanism, and state what each costs.

Approach

  1. Do the arithmetic first, because it eliminates most designs. At roughly 200 bytes a row including the heap tuple header, 35 million rows a day is about 7 GB a day before indexes, 12.8 billion rows a year, and near 77 billion rows and 15 TB of heap over six years. No single table with a btree on subject_id survives that, so tiering is part of the original design rather than a later migration.
  2. Partition by range on occurred_at, daily or weekly. It is the only column that turns retention into DETACH plus DROP instead of a DELETE that leaves 35 million dead tuples a day for vacuum to chase. Note the constraint this imposes: in Postgres declarative partitioning a unique index on the partitioned table must include every partition key column, so audit_id alone cannot be the global primary key. Use (occurred_at, audit_id), or accept per-partition uniqueness and say so.
  3. Index only the two queries that exist: (subject_type, subject_id, occurred_at) and (actor_id, occurred_at), and keep them on hot partitions. On an append-only table whose physical order correlates with occurred_at, a BRIN index on that column costs kilobytes where a btree costs gigabytes, but it prunes ranges only and is no substitute for the subject lookup, which is a point query.
  4. Specify the write path as a bounded queue, not a fire-and-forget call. Batch 100 to 500 rows per statement, bound the queue, and decide explicitly what happens when it fills: blocking the read is fail-closed and couples read availability to the audit store, while an outbox keeps reads available and needs its own liveness alarm, because a stalled publisher looks exactly like a quiet afternoon.
  5. Age detached partitions into an immutable, content-hashed columnar archive with the same investigator queries available, and keep retained_until on the partition. Audit retention runs on its own schedule and routinely outlives the case it describes, so it cannot inherit the business record's clock.

Worked solution (30 min)

  1. Compute daily and six-year row counts and raw heap bytes from 35 million rows a day, and name the point at which a single unpartitioned table stops being viable.
  2. Write CREATE TABLE ... PARTITION BY RANGE (occurred_at) with the two investigator indexes, and state why the primary key must include occurred_at.
  3. Write the retention step as DETACH PARTITION, archive, then DROP, and compare its cost against DELETE in dead tuples and vacuum work.
  4. Specify the write path: batch size, queue bound, the behaviour when the queue fills, and the alarm that distinguishes a stalled publisher from low traffic.

Expected result: About 12.8 billion rows a year and roughly 77 billion over six years at around 15 TB of heap; daily or weekly range partitions on occurred_at; a composite primary key including the partition key; (subject_type, subject_id, occurred_at) and (actor_id, occurred_at) on hot partitions with BRIN for range pruning; retention by DETACH and DROP; and a stated availability decision on the audit write.

Verify it yourself

  • A subject query over four years touches only the partitions in range, verifiable in the plan.
  • Dropping a partition frees space immediately with no vacuum backlog.
  • A batch job reading 2 million subject records produces 2 million audit rows, and you can state the investigator question that an aggregate row cannot answer.

Follow-up

  • An investigator asks for every access to one constituent across four years. What does that query touch, and what do you do to make it finish?
  • A report extract reads 2 million subject records in one job. How many audit rows, and why would an aggregate row not satisfy the obligation?
  • The outbox publisher stalls for an hour. How do you find out, and what exactly was lost?

A slow scanner takes down every document read

Medium · cascading failure · timeouts · bulkhead

An external malware scanner's p99 goes from 80 ms to 30 s while returning no errors; its health check stays green. Within four minutes Document Service answers nothing — including reads of documents already marked scan_status = clean — and Intake Service, which calls it synchronously when a constituent attaches a file, stalls too. Upload handlers call the scanner with no timeout, and a single 200-worker pool serves every endpoint. Offered load is 50 uploads per second. Deliver an ordered checklist, the arithmetic that predicts the collapse, and the fix ordered by blast radius.

Approach

  1. Separate down from slow first. Error rate flat, latency up, health check green is the harder failure mode precisely because nothing fails fast enough for load shedding to trigger. Look at saturation — workers in use, queue depth, pool wait time — before looking at error rate, because saturation is where the signal is.
  2. Do the capacity arithmetic with Little's Law. Concurrency equals arrival rate times service time, so 200 workers each held for 30 s serves at most 200/30, about 6.7 requests per second. Offered load is 50 per second, so the pool is fully occupied roughly 200/50 = 4 seconds after the step change and the queue then grows without bound. The four minutes to visible collapse is the scanner's latency ramp, not a slow-building queue.
  3. Explain the blast radius separately from the cause. Reads of already-clean documents fail because they share one worker pool with uploads; that is a bulkhead defect that exists independently of the scanner and would have been triggered by any slow dependency.
  4. Fix in blast-radius order. First a per-dependency timeout set well below the caller's own deadline budget, so a stuck call returns a worker instead of holding it. Second, separate pools for read and ingest paths so a saturated ingest path cannot consume the read path's capacity. Third, a circuit breaker on the timeout-and-error ratio so repeated calls stop being attempted at all.
  5. Treat retries as load amplification: retrying into a saturated dependency multiplies offered load at exactly the wrong moment, so retries need a budget (a token bucket or a fraction-of-traffic cap) and jittered backoff, not just a max-attempts constant.
  6. Then remove the coupling structurally. Accept the upload, persist with scan_status = pending, scan asynchronously, serve nothing whose verdict is pending. The synchronous call was never required by the contract, and once it is gone, a 30 s scanner degrades throughput of one queue instead of taking down reads.

Follow-up

  • What number do you set the timeout to, and which measurement do you derive it from?
  • The circuit breaker opens. What does the upload endpoint return, and what does a constituent on a 3 Mbps connection actually see?
  • How would you have detected this in the first 30 seconds instead of the fourth minute?

Suggested practice checklist

For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.

Day 1 — Rebuild the primitives by implementing them

  • Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
  • Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
  • For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.

Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.

Day 2 — Arrays under an invariant: two pointers, sliding window, binary search

  • Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
  • Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
  • Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.

Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.

Day 3 — Sorting, heaps, and the greedy argument that has to be proved

  • Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
  • Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
  • Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.

Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.

Day 4 — Recursion, memoisation, and the step to a table

  • Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
  • Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
  • Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.

Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.

Day 5 — Graphs, where most of the work is choosing the traversal

  • Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
  • Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
  • Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.

Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.

Day 6 — One day for everything that is not an algorithm

  • Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
  • Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
  • Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.

Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.

Day 7 — Solve out loud, under time

  • Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
  • Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
  • Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.

Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.

Risky changes and the reversal path

For anything that touched live traffic, be ready to say how you would have undone it: a flag, a staged rollout, dual writes with the old path still authoritative. Once the old column is dropped or the source rows are overwritten there is no reverse, so name what you kept a copy of and for how long.

Reverse a decision to write audit rows asynchronously

Hard · auditability · transactional outbox · reversal · data-access boundary

You chose to write audit_access_log rows through a fire-and-forget asynchronous call so a read path would stay inside its latency budget. Months later the rows are missing in exactly the windows an investigator cares about. The table is audit_access_log(audit_id, occurred_at, actor_id, actor_role, delegation_id, subject_type, subject_id, action, purpose_code, source, client_ip, request_id, retained_until). Describe a reversal of this shape that you owned: what made the original choice defensible, the evidence that changed your mind, what replaced it, and what the migration cost.

Approach

  1. State the original reasoning without apologising for it. The read path had a budget, the audit write is not on the user's critical path, and asynchronous is the textbook answer. A reversal narrative that opens with 'it was obviously wrong' tells the interviewer you never understood why anyone chose it, which makes the reversal luck rather than judgement.
  2. Give the evidence in the form that actually moved people: not 'some rows are missing' but a measured drop ratio against a known denominator, counting audited reads at the data-access boundary versus rows landed, bucketed by hour, showing loss concentrated in the highest-load hours. Those are the hours an investigation will later want to reconstruct, which is what makes the loss non-random rather than merely unfortunate.
  3. List the bypasses separately from the drops, because they need different fixes: a read replica, a report extract, a cache hit that never touches the table, and a search that returned the record, since a search hit is an access even if nobody opened the result. Some of those can only be closed below the application, in a policy or a view, rather than in a controller.
  4. Choose between same-transaction and an outbox deliberately and price both. Writing the audit row inside the read's transaction turns every audited read into a write transaction, which rules out serving it from a read replica at all and couples read availability to the audit table. The outbox keeps the read path fast but is only trustworthy with a liveness alarm on the age of the oldest unpublished row, not on whether the publisher process is up.
  5. Cost the migration honestly. The gap in the historical record cannot be backfilled from anything, so part of the reversal is documenting that period as incomplete and saying so out loud, rather than quietly starting to be correct from a Tuesday.

Follow-up

  • The outbox publisher is wedged for six hours. What does the read path do in the meantime, and who is told, and when?
  • How do you prove to an auditor that capture cannot be bypassed, instead of asserting that it is not?
  • purpose_code is an enum precisely so it is auditable. What stops staff from always selecting whichever value sorts first?

Ship an enrolment window with a hardcoded holiday calendar

Medium · technical debt · civil time · deadline load · instrumentation

An enrolment window opens in eleven days and traffic on its closing day runs eight to ten times steady state. Due dates must be civil dates walked over a jurisdiction's business-day calendar, which is versioned data amended annually. The calendar service that does this properly is three weeks of work. Describe shipping with known debt of this shape: exactly what you shipped, the set of inputs it is wrong for, how you made that wrongness visible to people who were not you, and the evidence that eventually retired it.

Approach

  1. Define the debt by its input domain rather than by adjective: one jurisdiction's holiday list for the current year, embedded as data with an explicit last-valid date, walking business days after converting the instant to the case's civil time zone. It is wrong for every other jurisdiction, for any date past that boundary, and for a holiday amended mid-year.
  2. Make it fail loudly exactly where your testing stops. Computing past the embedded list's last date raises rather than silently falling back to calendar days, because a wrong due date reports a late case as on time, and that direction produces audit findings rather than complaints. A hard failure at a known boundary is cheaper than a plausible wrong answer.
  3. Draw the line the deadline is not allowed to cross. A due date is a computed field you can recompute for every open case later; a determination written under the wrong rule version is not recoverable that way. Say which side of that line each shortcut you took sat on, and what you refused even under the eleven-day clock.
  4. Give the debt an owner and a trigger that is not a date on a calendar: a counter of due-date computations rejected at the boundary, and a ticket whose trigger condition is either that counter going non-zero or a second jurisdiction being onboarded.
  5. Close on retirement with evidence: what was measured when the calendar service landed, whether any due dates already written had to be recomputed, and the query that found those rows.

Follow-up

  • The closing day is eight to ten times normal load. What did you actually do about it, and what did you refuse to degrade on the submission path?
  • A holiday is amended after you have already written due dates against the old list. What is the repair, and how do you find the affected cases?
  • Which parts of the submission flow still work with JavaScript unavailable, and was that one of the corners you cut?

Explain a denied determination to an auditor and a caseworker

Easy · communication · provenance · appeals · evidence

A determination denied an application and the constituent has appealed fourteen months later. An auditor wants to know which rules were applied and why; a caseworker wants to know what to tell the constituent this afternoon. You have the determination row (rule_version_id, evidence_snapshot_hash, ordered reason_codes, clause trace, decided_by), the case_event history and the per-channel notice dispatch record. Write both explanations. State what you will not assert, and what you would check before asserting it.

Approach

  1. Answer the two audiences separately, because they need different objects. The caseworker needs the ordered reason codes turned into what the constituent can do next; the auditor needs provenance: which rule_version governed, which clauses fired, which evidence snapshot fed them, and who or what decided.
  2. Lead the auditor's version with the fact that decided_by being NULL means the determination was fully automated, which is a materially different fact from a human decision and is the first thing they will ask about. Papering over it costs you the rest of the conversation.
  3. Separate what the record proves from what you are inferring. The record proves which clauses fired against which evidence hash. It does not prove the constituent received the notice, because dispatch is recorded per channel and dispatch is not receipt.
  4. Distinguish procedural reason codes from substantive ones, since they lead to opposite advice: evidence not returned is curable by sending a document, while a determination over a threshold is not, and the caseworker will act on whichever you imply.
  5. Say what you would verify before asserting any of it: replay the determination against its own rule_version_id and evidence_snapshot_hash and confirm it reproduces byte for byte, and report a mismatch as a different question rather than as an answer.

Follow-up

  • The replay does not reproduce the original outcome. What do you do first, and what do you tell the auditor in the meantime?
  • The caseworker asks you to describe it as a system error so the conversation is easier. How do you handle that?
  • The rendered notice is itself evidence. What does that imply about template versions and fonts?