cswork / libraryOpen practice workspace

Interview guides · Agile Defense

Agile Defense Software Engineer Interview Questions & Guide 2026

Agile Defense 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 Agile Defense, you will play a pivotal role in designing, developing, and maintaining critical technology solutions that support defense and federal government missions. This position sits at the intersection of modern software development practices and high-stakes national security environments, requiring you to build robust, scalable applications that users rely on under demanding conditions. Your contributions directly impact mission readiness, operational efficiency, and the seamless integration of enterprise cloud and security systems.

Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.

Focus areas

  • Key idempotency on business facts, not requests
  • Version rules by statutory effective date, not release
  • Treat accessibility conformance as a functional requirement

Overview

The scope of this role spans multiple technical domains, including full-stack development, cloud systems engineering, enterprise Java applications, and secure network architectures. Whether you are optimizing backend data pipelines, implementing fine-grained authorization models like OpenFGA, or engineering resilient cloud infrastructure, you will tackle complex technical challenges that demand both creative problem-solving and disciplined execution. You will frequently collaborate with cross-functional teams comprising project managers, business analysts, systems engineers, and defense stakeholders to translate operational requirements into secure, high-performing software products.

Expect a fast-paced yet mission-focused environment where technical excellence and adaptability are paramount. Agile Defense values engineers who can navigate ambiguous requirements, communicate effectively across technical and non-technical boundaries, and deliver reliable code on schedule. Success in this role requires a balance of core engineering craftsmanship and a deep respect for security, compliance, and user-centric design principles.

The Agile Defense interview process

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

Recruiter Screening

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

What to demonstrate

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

How to prepare

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

Technical Interviews

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

Final Interviews

A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.

What to demonstrate

  • Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
  • Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
  • Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
  • Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing

How to prepare

  • Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
  • Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
  • Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one

Where candidates lose points

Implementing eligibility rules as ordinary application code that ships on the release train, with thresholds as constants and reference tables edited in place

Rules change on statutory effective dates, are frequently backdated, and are sometimes corrected after decisions have already been issued under them. If the only record of a threshold is the value currently deployed, every historical determination becomes unreproducible the moment it changes, and an appeal cannot be adjudicated because nobody can demonstrate what the rule was. The fix is bitemporal: a rule_version keyed on the period it governs, plus the transaction time at which that text was recorded, so you can ask both 'what applied to this case' and 'what did we believe applied when we decided it'. Reference data the rules read has to be versioned the same way, because an UPDATE to a rate table is a silent rewrite of history.

Treating an integration partner as if it were a synchronous API when the system of record is a nightly batch

The effective consistency model is 'as of last night', so a screen that shows a constituent their updated status immediately after a submit is telling them something the authoritative system will not agree with for hours, and a caseworker who acts on it creates a conflict that surfaces as a reconciliation exception nobody owns. Read-after-write expectations also encourage retry-until-consistent loops that hammer a mainframe window sized for one pass. Make staleness explicit in the model and in the interface - show the as-of time, keep the local record in a pending state until the batch confirms, and design the reconciliation job as the place where disagreement is resolved rather than as an exception report.

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

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

Writing code before the input contract is pinned down

Before the first line, state the types, the size bounds, whether duplicates, negatives or an empty input are possible, whether the input is sorted, whether you may mutate it, and what the function returns when nothing matches. Every one of those answers changes the code, and discovering one at minute twenty costs a rewrite you no longer have time for.

Questions & practice

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?

Detect bulk-write bursts in a backdatable case event stream

Medium · sliding-window · two-pointer · event-time

One day of case_event holds 1.4 million rows, of which about 210,000 carry actor_type='staff' with a non-NULL actor_id. Each row has occurred_at (the asserted time of the act, which staff routinely backdate by days or weeks) and recorded_at (when the row was written). Flag every actor_id that records more than 200 transitions inside any rolling 10-minute window, so an unannounced bulk script can be told from caseworkers clicking. Choose which clock the window slides on, justify it, and state the time and memory bounds.

Approach

  1. Slide on recorded_at, and separate the two independent reasons. Mechanically, the two-pointer invariant requires the stream key to be non-decreasing so the left pointer only ever advances; rows arrive in recorded_at order, so windowing on occurred_at while consuming them in arrival order breaks that invariant and you would first have to sort the day by occurred_at, an O(n log n) precondition the streaming form does not need. Semantically, even after that sort the occurred_at answer is the wrong one: a script that backdates its writes spreads its asserted times over weeks and fills no window, while the rows it wrote in one second are adjacent in recorded_at. The question is who wrote these rows in a short span, and that is a recorded_at question.
  2. One pass with a deque of (recorded_at, actor_id) and a hash map from actor_id to in-window count. Advance the right pointer, increment; advance the left pointer past anything older than 10 minutes, decrement, and delete a key at zero so the map stays proportional to active actors. Each event is pushed and popped once: O(n) amortised.
  3. Emit on threshold crossing, not on every event above it. A single actor holding 900 events in the window would otherwise produce hundreds of near-identical alerts; latch the actor on crossing and clear the latch only when its count falls back below the threshold.
  4. Memory is the widest window, not the day. Filtering to actor_type='staff' first keeps the deque at the staff rate rather than the 1,800 rows per second the overnight batch reaches; storing 16 bytes per entry rather than the row keeps the worst realistic window inside tens of megabytes. State the bound as a function of peak staff rate times 600 seconds so it survives a change in traffic.
  5. Refine the discriminator beyond count. Rows written by one statement share a recorded_at to the microsecond, while a caseworker's clicks land seconds apart, so the intra-window timestamp dispersion separates a bulk script from a fast human far more cleanly than volume alone, and it does not need a threshold tuned per unit.
  6. Keep occurred_at for the separate report it actually answers: the gap between occurred_at and recorded_at is entry lag, and it spikes predictably at reporting boundaries when staff enter a backlog.

Follow-up

  • recorded_at defaults to now(), which in this database is transaction start time, so commit order and recorded_at order can differ. What does a reader tailing by recorded_at miss, and how do you bound it?
  • A legitimate nightly reassignment job runs under a staff actor_id because it was given a service account with a person's role. How does your detector avoid alerting on it, without an allowlist that grows forever?
  • Run this over an unbounded stream rather than a day. What is the new memory bound and what is the first thing that breaks?

Match a rail return file onto payment instructions in one pass

Easy · hashing · reconciliation · idempotency

You hold 400,000 disbursement_instruction rows for a 90-day window (instruction_id, determination_id, benefit_period_start, amount_minor, rail_reference, status) and a return file of 12,000 rows carrying rail_reference, amount_minor, return_code and returned_at. The rail guarantees rail_reference is unique only within 90 days and recycles values afterwards, and it may resend a return it already sent. Produce three buckets: returns matched to exactly one instruction, returns matching none, and returns matching more than one. One pass over each input. State your time and space bounds.

Approach

  1. Build one hash map from rail_reference to a small list of instruction_ids in a single pass over the instruction rows. Lists, not scalars: recycling means a reference can legitimately collide across the window edge, and you need to observe that rather than let the last writer win. O(n) time, O(n) space for n=400k references.
  2. Stream the return file and probe. Zero hits goes to unmatchable, one hit to matched, more than one to ambiguous. Do not disambiguate an ambiguous hit by amount_minor: a returned payment and a recycled reference can carry the same amount, and guessing turns a reportable exception into a wrong ledger entry.
  3. Deduplicate resends on the business fact, not on the file row. A return is uniquely identified by (instruction_id, return_code, returned_at) once matched; a second identical row applies the same compensating state and must be a no-op, which is what makes replaying a redelivered file safe.
  4. Do not filter the instruction side by rail_reference IS NOT NULL. A NULL reference means the acknowledgement has not been processed, not that nothing was sent, so those rows belong in the map's absence set and drive the separate unacknowledged-but-submitted report.
  5. Memory: 400k entries of a ~20-byte reference key plus an 8-byte id is roughly 11 MB before hash overhead, comfortably in process. If the window ever widens past what fits, range-partition by the reference prefix and run k passes at k times the read cost rather than reaching for a database join per return row.

Worked solution (20 min)

  1. Build an eight-row instruction fixture: six with distinct references, and two sharing reference R because the rail recycled it 91 days apart.
  2. Build a five-row return file: three matching distinct references, one naming reference R, one naming a reference never assigned.
  3. Run the two passes and record which bucket each return lands in.
  4. Append a byte-identical copy of one already-matched return row and re-run, confirming the applied state is unchanged.

Expected result: Three returns matched, one ambiguous (reference R, two candidate instructions, no amount-based tiebreak attempted), one unmatchable. The duplicated row changes no instruction state and increments a duplicate-return counter.

Verify it yourself

  • The three bucket sizes sum to the return file row count, with no row counted twice.
  • Shuffling the return file changes nothing in the bucket membership.
  • Removing the instruction rows whose rail_reference is NULL does not change any bucket, which proves those rows were never being used as match candidates in the first place.

Follow-up

  • The rail sends a partial return for less than amount_minor. What does your matcher do, and what does the instruction row become?
  • A return arrives for an instruction that has already been reversed by a compensating row. How do you tell a duplicate return from a genuine second event?
  • You must run this continuously rather than per file. What bounds the map now, and what is the eviction rule?

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?

Model a ruleset that is corrected after decisions were issued

Hard · bitemporal modelling · exclusion constraint · range types · replay

A regulator republishes a rate table with a correction backdated eleven months. Determinations already issued cite rule_version_id. Design rule_version so the system can answer both 'what governed a case submitted on date D' and 'what did we believe governed it when we decided at instant E', and so two rows may govern the same date only when they were recorded at different times. Give the DDL including the constraint that rejects a bad insert, plus the query that lists determinations needing replay.

Approach

  1. Separate the two time axes explicitly and name them: valid time, the statutory period the text governs, and transaction time, the period during which the system believed it. A retroactive correction is precisely a new row whose valid time overlaps an old row's while its transaction time does not, so any model with a single date range cannot represent the situation at all.
  2. Write the DDL with range types so the database can police it: rule_version(rule_version_id INT PRIMARY KEY, program_code TEXT NOT NULL, governs_period daterange NOT NULL, recorded_period tstzrange NOT NULL, text_hash BYTEA NOT NULL, EXCLUDE USING gist (program_code WITH =, governs_period WITH &&, recorded_period WITH &&)), with CREATE EXTENSION btree_gist so the equality operator on a scalar can participate in a GiST exclusion constraint.
  3. Be deliberate about bounds. daterange defaults to [), so an inclusive statutory end date must be written as daterange(d_from, d_to + 1, '[)') or the last governed day falls outside the range — an off-by-one that produces a plausible wrong determination rather than an error. An open-ended current version is an unbounded upper bound, closed by an UPDATE to the transaction-time range when it is superseded.
  4. Write the two queries and show they differ only in one predicate: current belief is governs_period @> $D AND recorded_period @> now(); belief as of the decision is governs_period @> $D AND recorded_period @> $E. Deriving the case's civil date is part of the query, not an afterthought — (c.submitted_at AT TIME ZONE j.time_zone)::date, because casting a timestamptz straight to date silently uses the session's TimeZone and will move a submission across midnight.
  5. State the rule that makes replay honest: neither query is what you replay against. A determination is replayed against the rule_version_id recorded on it, and these queries exist to find determinations whose recorded version is no longer what current belief says should have governed — the work list. Replaying against 'the current rule' answers a different question and destroys the only evidence of what was decided.
  6. Write the work-list query as the join between belief and record, and size it: SELECT d.determination_id FROM determination d JOIN "case" c USING (case_id) JOIN jurisdiction j ON j.code = c.jurisdiction_code JOIN rule_version rv ON rv.program_code = c.program_code AND rv.governs_period @> (c.submitted_at AT TIME ZONE j.time_zone)::date AND rv.recorded_period @> now() WHERE d.is_current AND rv.rule_version_id <> d.rule_version_id. Eleven months of backdating over this data set is a bulk job, so it is chunked on determination_id and each replay lands on the existing determination through the partial unique index rather than inserting a second one.

Follow-up

  • The correction changes an amount for 200,000 settled determinations. What does the disbursement side owe, and is it one reversal per determination or one per benefit period?
  • A second correction is issued to the first correction's text. What does the exclusion constraint permit, and what does the transaction-time range of the middle version become?
  • Reference data the rules read — a rate table, a threshold list — is stored as ordinary rows. What breaks on replay, and what is the minimum change that fixes it?

What steps do you take when troubleshooting a distributed system failure across multiple cloud environments?

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

Follow-up

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

Software Testing questions and best practices for ensuring code reliability.

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Clarify what is being asked and what a complete answer contains.
  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?

Design the error taxonomy for an eligibility evaluation API

Easy · error taxonomy · retries · api contract

The caller is the Intake Service, which posts a frozen evidence snapshot to the Eligibility Service behind a 2 s timeout and retries any 5xx twice. Design the error contract for POST /evaluations: the media type, the fields every error carries, and the class a caller switches on. Cover schema validation, an unknown rule_version_id, an expired delegated authority, a snapshot hash that does not match the stored evidence, quota exhaustion, and a denial. For each, give the status, whether retry is safe, and whether the retry may reuse the evaluation key. Deliver the schema and the classification table.

Approach

  1. Fix the envelope: application/problem+json per RFC 9457 (which obsoletes RFC 7807) with type, title, status, detail and instance, plus two extension members — a stable machine code and a retry_class of never | after_backoff | same_key. Callers switch on code; the prose is for humans and must be free to change without a version bump.
  2. Separate the business outcome from transport failure. denied, partially_approved and referred are completed evaluations and belong in a 200 response carrying outcome and ordered reason_codes. Behind a 4xx or 5xx they both trigger the caller's retry policy against a deterministic refusal and strip the reason codes the notice is later required to cite.
  3. Classify the rest on two axes: is the resulting state known, and is the caller at fault. Known and caller's fault gives 400 or 422 for schema, 422 for an unknown rule_version_id (it is a data reference, not a missing route), 403 for expired delegated authority — none retryable. Known and ours gives 409. Unknown gives 502 or 504, retried with the same evaluation key. Overload gives 429 with Retry-After (RFC 9110: delay-seconds or an HTTP-date).
  4. Give the evidence-snapshot mismatch its own code rather than folding it into generic validation. Re-evaluating against a different evidence_snapshot_hash answers a different question, and reporting that as a denial or a 400 is how a replay quietly becomes a new decision.
  5. Pin the taxonomy with a test asserting every error path emits a code from the enumerated set. Without it the catch-all handler produces an uncoded 500 and the caller's switch falls through to its default branch, which in every retry library is retry.

Worked solution (20 min)

  1. Write the problem+json schema: type, title, status, detail, instance, code (stable enum), retry_class (never | after_backoff | same_key).
  2. Enumerate ten concrete failures from the real path: malformed snapshot, missing jurisdiction_code, unknown rule_version_id, superseded rule_version, expired delegation, snapshot hash mismatch, evaluator quota exceeded, evaluator timeout, evaluator unreachable, and a denial on a substantive reason code.
  3. Place each in a table with status, code, retry_class and the caller's action in one line.
  4. Re-read the denial row and confirm it is a 200 carried in the determination representation, not an entry in the error envelope.
  5. Write the caller's switch in pseudocode and check every branch is reachable from the table and nothing falls through to a default that retries.

Expected result: A table in which denied, partially_approved and referred are 200 responses, exactly two classes are retryable (429 and an indeterminate 5xx), both retry with the evaluation key unchanged, and no 4xx other than 429 is marked retryable.

Verify it yourself

  • Every error carries a stable code distinct from its HTTP status, so two different 409s are distinguishable by a caller.
  • A caller implementing only the retry_class column never re-evaluates a denial.
  • The snapshot-hash mismatch has its own code and is never reported as a validation error or as an outcome.

Follow-up

  • A rule correction forces re-evaluation of four million historical snapshots. Same endpoint or a different one, and what changes in the error contract at that volume?
  • An appeals portal renders reason codes to constituents. How do you version that enum so a portal built last year handles a code added this year without crashing?
  • Returning 403 for expired delegated authority tells the caller the case exists. When is that acceptable and when is it a disclosure?

Statutory due dates land one day late for evening submissions

Easy · timezone · business calendar · date arithmetic

An audit finds cases the system reported on time that were late by statute. Two clusters: submissions after 19:00 local in a jurisdiction at UTC-05:00, and the week following a spring daylight-saving transition. sla_due_on DATE is computed by taking submitted_at (TIMESTAMPTZ, stored as UTC), calling .date() on it, then walking ten business days against a holiday list hard-coded in the application. Both clusters move the deadline in the same direction. Deliver an ordered checklist, name both defects and the direction of the error, and give the fix with its test matrix.

Approach

  1. Bucket the wrong rows before theorising: convert submitted_at to the jurisdiction's civil time and group by local hour and by jurisdiction_code. If every bad row in cluster one falls between 19:00 and 23:59 local for a UTC-05:00 zone, the start date is the defect, not the walk.
  2. Name defect one precisely: .date() on a UTC instant yields the UTC calendar date. At 19:00 local in UTC-05:00 the UTC date is already tomorrow, so the ten-day walk starts a day late and the due date lands a day late. The error is permissive — the case reads as on time for an extra day, which is the direction that produces audit findings rather than complaints.
  3. Name defect two: any arithmetic done in elapsed seconds. Adding 10 x 86,400 seconds to 2026-03-05T23:30-05:00 gives 2026-03-16T04:30Z, which converts to 2026-03-16T00:30-04:00 after the spring transition — civil date the 16th where ten calendar days is the 15th. A civil day is not always 86,400 seconds, so day arithmetic must happen after conversion to civil time, never before.
  4. Audit the calendar itself: a hard-coded list has no effective dates, no observed-date shift when a holiday falls on a weekend, and no per-jurisdiction opt-out. It also rewrites history when amended, because last year's determinations would be recomputed against this year's list.
  5. Fix: store the instant, resolve the jurisdiction to an IANA zone identifier (a fixed -05:00 offset is wrong half the year), convert to a civil date, then walk business days over a holiday table keyed by (jurisdiction_code, observed_date) with effective_from and effective_to. Recompute the affected rows and report which cases are genuinely late.
  6. Test matrix, not a happy path: both DST transitions, a submission at 23:59 local, a holiday whose observed date shifts off a weekend, a jurisdiction that does not observe it, a leap day, and a case whose jurisdiction changed mid-flight. A test authored and run in July passes every one of these while broken.

Follow-up

  • The recomputation moves 40,000 due dates earlier and some cases are now already past due. What do you do with them, and who is told?
  • Why is persisting the UTC offset instead of the zone identifier wrong, and when is a fixed offset actually correct?
  • The holiday list is amended next year. What stops that amendment changing a due date already computed and noticed?

Suggested practice checklist

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

Day 1 — Diagnostic, scored before you study anything

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

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

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

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

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

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

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

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

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

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

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

Day 5 — The gap that is not a skill

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

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

Day 6 — Retest under day-one conditions

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

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

Day 7 — Full loop under interview conditions

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

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

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.

Describe your experience working with network engineering concepts and secure data transmission protocols.

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Pick a story where you made the decision, not one where you watched it.
  3. 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?

Tell me about a time you faced a challenge and how you overcame it.

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. Close with what you would do differently, concretely.

Follow-up

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

Walk me through your experience working alongside business analysts and project managers in an Agile environment.

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  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

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

How would you handle a conflict with a team member who has a differing technical opinion?

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

Follow-up

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