cswork / libraryOpen practice workspace

Interview guides · Kratos Defense & Security Solutions

Kratos Defense & Security Solutions Software Engineer Interview Questions & Guide 2026

Caching practice for Kratos Defense & Security Solutions software engineer prep: settle the staleness budget before choosing an invalidation strategy.

The 60-second brief

As a Software Engineer at Kratos Defense & Security Solutions, you work on mission-critical technology. You will be contributing to complex, high-stakes systems that support defense, satellite communications, and aerospace initiatives. Your work directly affects the reliability and performance of systems that Kratos customers depend on for national security and global operations.

Treat capacity estimation as a conversion skill rather than a table to memorise: turn a user count and an action rate into requests per second and bytes per day, then name the component that number breaks first. The figure only matters if it changes the design.

Focus areas

  • Version rules by statutory effective date, not release
  • Enforce audit writes at the data-access boundary
  • Key idempotency on business facts, not requests

Overview

This role is not merely about writing code; it is about engineering robust solutions in environments where precision and security are paramount. You will collaborate with multidisciplinary teams—including systems engineers, hardware specialists, and project managers—to navigate the full lifecycle of product development. Whether you are working on network programming, real-time control systems, or specialized software interfaces, your contributions will be central to the technological edge that Kratos Defense & Security Solutions provides.

The Kratos Defense & Security Solutions interview process

Kratos Defense & Security Solutions candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screen

The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.

What to demonstrate

  • Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
  • Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
  • Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year

How to prepare

  • Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
  • Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
  • Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter

Hiring Manager Discussion

Part of what is being decided is what your manager's week looks like once you are on the team: how they find out that something you shipped is broken, and whether they hear it from you or from a customer. The material that moves this round is therefore not the launch, it is the week after. Describe how you knew the change was working, which number you watched and for how long, and what you would have reverted to. A project that ends at the word shipped leaves the manager guessing about the part they care about most.

What to demonstrate

  • How a change of yours was verified in production, and whether that check existed before the deploy or was assembled afterwards once something looked wrong
  • Whether the rollback story is specific, including the case where a revert is not enough: once a migration has dropped a column or rewritten data, going back is its own change with its own risk
  • Whether you can describe an incident you caused, how it was found, and how much time passed between it starting and anyone noticing
  • Whether bad news in your account travels early and from you, or consistently arrives via somebody else

How to prepare

  • For your last significant change, write down what you watched after the deploy, for how long, and the number that would have made you revert. If nothing was watched, say that plainly instead of inventing a dashboard.
  • Work out the rollback answer for a change that touched stored data, and be able to say what made it reversible or what you would have had to do instead. That distinction is a level signal on its own.
  • Write the two-minute version of an incident you owned: what broke, how it surfaced, what you did in the first ten minutes, and the change that stopped it recurring. Get the timeline straight, because this is the story most likely to be interrupted with questions.

Technical Panel

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

Onsite Visit

Nobody in the room with you decides this. Interviewers typically write their rounds up separately, often before seeing anyone else's, and the outcome is settled later from those write-ups. A split panel gets resolved by whichever note carries specific evidence, so what you want out of each room is one concrete thing that person could write down: a bug you caught yourself, a trade-off you named, a decision you owned. The rest is arithmetic. The project you describe in a behavioural conversation is often the same system you sketched an hour earlier, and the two accounts have to agree.

What to demonstrate

  • Whether the scale, team size and timeline you attach to a project hold steady when that project resurfaces in a different round
  • Whether each interviewer leaves with a specific thing to cite rather than a general impression of competence
  • Whether a trade-off you defended in one round survives a challenge in another, instead of being quietly swapped for the answer the new interviewer seemed to want
  • Whether a question you have already answered earlier in the day gets the same answer at the same depth, without visible impatience

How to prepare

  • Write a one-page sheet per project fixing the figures you will quote — request volume, data size, team size, elapsed time, what broke — and say them aloud from the sheet until they come out identical every time
  • For each round on the schedule, decide in advance the one sentence you want in that person's notes, then check in a mock that you said it outright instead of leaving it to be inferred
  • Have someone ask you the same project question twice, an hour apart, and diff the two answers for numbers that moved or a trade-off that reversed

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.

Running an EBCDIC-to-ASCII text conversion across a whole fixed-width record coming from a legacy system of record

Those records mix character fields with binary and packed-decimal (COMP-3) numeric fields, where two digits are packed per byte and the sign occupies the low-order nibble of the final byte. A character transcode rewrites those bytes, so an amount does not fail to parse - it parses into a different, entirely plausible number, and the corruption is discovered in reconciliation weeks later or not at all. Parse by field using the record layout: transcode only the character fields, unpack the numeric fields explicitly, and add a checksum or control-total record to the file contract so a mangled batch is rejected rather than posted.

Answering a debugging question with a guess instead of a bisection

Give a procedure that halves the search space at each step: confirm the symptom reproduces, establish the last known-good version, input or timestamp, then bisect over commits, over the data, or over the layers of the request path. A plausible cause with no way to confirm it is the same move whether it happens to be right or wrong, which is why it scores nothing.

Retrying a write that is not safe to repeat

A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.

Questions & practice

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.

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?

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.

Worked solution (25 min)

  1. Build a fixture of 3,000 staff events: actor A writes 240 rows sharing one recorded_at microsecond, actor B writes 260 rows spread evenly across nine minutes, actor C writes 180 rows across eight minutes, and 2,320 unrelated rows scattered across the day.
  2. Backdate only actor A's occurred_at values, spreading the 240 rows evenly across the previous three weeks — 21 days over 240 rows is one every 126 minutes — and leave recorded_at untouched. Leave occurred_at equal to recorded_at for B, C and the unrelated rows.
  3. Run the detector on recorded_at. Then sort the same rows by occurred_at, so the second run is a well-defined window rather than an artefact of arrival order, run it on that key, and diff the two flagged sets.
  4. Compute, for each of A, B and C, the maximum count in any 10-minute window under each key, and the intra-window timestamp dispersion.

Expected result: The recorded_at window flags A (240 in one microsecond) and B (260 inside nine minutes) and not C (180, below the threshold of more than 200). The occurred_at window flags B and not A: B's occurred_at was never backdated, so B's 260 rows still sit inside a nine-minute span, while A's asserted times are 126 minutes apart and no 10-minute occurred_at window holds more than one of them. Sliding on occurred_at therefore loses exactly the bulk writer and retains only the actor that either key would have caught. Dispersion separates A (zero spread, one statement) from B (seconds apart, a human or a paced script).

Verify it yourself

  • Every event enters and leaves the deque exactly once, verifiable by counting pushes and pops against the input row count.
  • The per-actor map is empty at end of stream, which proves no decrement was missed.
  • A's maximum occurred_at window count is 1, computable by hand from the 126-minute spacing, so the miss under occurred_at is plain arithmetic on the fixture and not an artefact of how the pointers were implemented.
  • Shuffling the input and re-sorting by recorded_at reproduces the flagged set exactly; shuffling without re-sorting produces a different set, which is the demonstration that the algorithm depends on a monotone key rather than tolerating one.

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?

Rebuild an award timeline as of a past decision date

Medium · intervals · bitemporal · sweep-line

One case has up to 400 determination rows across all reopen cycles: determination_id, reopen_cycle, supersedes_determination_id, decided_at TIMESTAMPTZ, effective_from DATE, effective_to DATE (NULL meaning open-ended, both endpoints inclusive), award_amount_minor. Given a set of queries (benefit_date D, as_of decision time T), return the award in force for D according to what had been decided by T, plus any benefit dates inside the covered span that no determination covers. Later-decided wins on overlap. Give the complexity for building the answer and for each query.

Approach

  1. Convert every interval to half-open [effective_from, effective_to + 1 day) at the boundary of the system, once. Keeping inclusive ends in the algorithm produces an off-by-one at every adjacency, and the error is a single uncovered or doubly-covered day that a reviewer reads as a gap in benefit.
  2. For a given T, filter to determinations with decided_at <= T. Do not use is_current: that column answers what is in force now, and the whole point of the as-of query is that a determination decided after T must be invisible even though it exists today.
  3. Paint the surviving intervals onto a timeline in ascending decided_at order so later decisions overwrite earlier ones where they overlap. Equivalently, sweep the sorted endpoints with a max-heap keyed on decided_at and lazily discard heap entries whose interval has already ended. O(n log n) build.
  4. Answer each query by binary search over the painted, non-overlapping interval list: O(log n). Gaps fall out of the same structure as the elementary intervals with no covering determination between the minimum effective_from and the maximum effective_to of the surviving set. Note what that definition implies: deleting one determination only produces a gap where nothing else still covers those dates, so an overlapping earlier award absorbs the deletion silently and correctly.
  5. With n at most 400, a quadratic paint over elementary intervals is about 160k comparisons and is genuinely fine; say so rather than reaching for the heap by reflex. The heap earns its place when the same routine runs across a program-wide cohort rather than one case.
  6. If queries carry many distinct T values, the painted timeline is per-T and rebuilding it per query is O(Qn log n). Sort queries by T and advance one sweep through decided_at order, materialising each version as you pass it, or accept the rebuild when Q is small.

Follow-up

  • A determination is decided with an occurred-earlier effective_from that reaches back behind an already-settled disbursement. What does the timeline say, and what does that imply for the payment side?
  • Two determinations in different reopen cycles share a decided_at to the microsecond. What is your tiebreak and why must it be total?
  • How do you answer the same question for 200,000 cases in a nightly report without rebuilding 200,000 heaps?

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.

Worked solution (45 min)

  1. Create the table with btree_gist and the three-column exclusion constraint, then insert an original version and confirm a second row overlapping in both axes is rejected.
  2. Insert the retroactive correction — overlapping governs_period, disjoint recorded_period — and confirm it is accepted, closing the prior row's transaction-time range in the same transaction.
  3. Write both point-in-time queries for a case submitted inside the corrected window and check they return different rule_version_id values.
  4. Build a jurisdiction whose time zone differs from the session's and a submission near local midnight, then compare the civil-date conversion against a naive ::date cast.
  5. Run the work-list query over a seeded set of determinations and confirm it returns exactly those decided under the superseded text.
  6. Replay one of them and confirm it lands on the existing determination chain through the partial unique index rather than creating a parallel current row.

Expected result: The exclusion constraint rejects a genuine double-booking in both axes and accepts a retroactive correction. The two point-in-time queries return different versions for the same case. The work-list query returns exactly the determinations whose rule_version_id differs from current belief, and replaying one produces a superseding row rather than a duplicate current row.

Verify it yourself

  • Inserting two rows overlapping in both governs_period and recorded_period raises an exclusion violation (SQLSTATE 23P01), not a silent accept.
  • For a case submitted on the last governed day, the @> test is true — the [) bound was handled rather than assumed.
  • The naive ::date cast and the jurisdiction-aware conversion disagree for at least one seeded submission, proving the conversion is load-bearing.
  • Re-running the work-list query after the replay returns an empty set, and running it a third time still returns empty — the job is convergent, not oscillating.

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?

Measure time in status from an append-only case event log

Medium · window functions · event log · frame semantics · backdating

case_event(case_event_id, case_id, reopen_cycle, seq BIGINT, event_type TEXT, occurred_at TIMESTAMPTZ, recorded_at TIMESTAMPTZ) is append-only with UNIQUE (case_id, seq). Staff backdate occurred_at; recorded_at is when the row landed. A case can be reopened, so submitted and determined repeat. Write a query returning one row per decision episode with the interval from its submitted to its determined, plus a flag for episodes still open. State which column you order the window by and why the other one gives wrong answers, and say what your query returns for a cycle whose determined carries an occurred_at earlier than its submitted.

Approach

  1. Partition on the real grain, (case_id, reopen_cycle). With case_id alone, an open cycle's submitted takes the next cycle's submitted as its LEAD, so a cycle that never closed is reported closed with an interval spanning a cycle boundary. Keep LEAD(event_type) in the output so that is detectable rather than assumed.
  2. Order by seq, not occurred_at. seq is unique per case by constraint, so the ordering is total and the result is deterministic. occurred_at is asserted by staff: a determined backdated below its own submitted swaps the two rows, the submitted row becomes last in its partition, LEAD returns NULL, and a cycle that was decided is reported still open — it leaves the completed-cycle statistics entirely rather than showing up as a bad number.
  3. Use LEAD over the filtered event set: SELECT case_id, reopen_cycle, occurred_at AS submitted_at, LEAD(occurred_at) OVER w AS determined_at, LEAD(event_type) OVER w AS next_type FROM case_event WHERE event_type IN ('submitted','determined') WINDOW w AS (PARTITION BY case_id, reopen_cycle ORDER BY seq), then keep the rows where event_type = 'submitted'. WHERE is evaluated before window functions, so filtering to two event types first is both correct and cheap.
  4. Separate pairing from measurement, because seq only fixes the first. Ordering by seq guarantees you paired the right two rows; it cannot repair a timestamp a human asserted, so a backdated cycle still yields determined_at - submitted_at below zero. Emit it as computed alongside (determined_at < submitted_at) AS asserted_interval_negative. Do not clamp with GREATEST(..., interval '0') and do not swap the endpoints: the negative value is the only evidence the entry is wrong, and the flag is what keeps it out of the published average without hiding it from the people who can correct it.
  5. Carry the system-time measure beside the asserted one: recorded_at - LEAD(recorded_at) inverted, i.e. LEAD(recorded_at) OVER w - recorded_at. Precondition for calling it non-negative: recorded_at is assigned by the database at insert and appends for one case are serialised — a per-case seq issued under the case row lock gives you both properties at once. If a case can be appended to concurrently, recorded_at can invert against seq too and the flag must cover both measures.
  6. Handle the open episode as data, not as a default: when LEAD returns NULL there is no determined yet, so emit determined_at IS NULL and a still_open flag. Coalescing to now() silently converts a censored observation into a completed one and drags every average down.
  7. Be explicit about frames. LAG and LEAD are not frame-sensitive — they address the partition directly — but the moment you add a running aggregate, the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, where CURRENT ROW in RANGE mode means the last peer. Ordering on a tie-prone timestamp therefore makes peers of distinct events and sums them all at once; ordering on unique seq makes RANGE and ROWS coincide.

Follow-up

  • The published service standard is in business days in the case's jurisdiction. What does this query have to become, and where does the holiday calendar live?
  • Two determined events exist in one cycle because a correction was recorded. Which one does your query pair, and which one should it pair?
  • Rewrite the still-open flag so a cycle with withdrawn between submitted and determined is excluded rather than counted as open.

How would you handle multithreaded programming in a high-performance environment?

Medium

Approach

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

Follow-up

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

Can you explain how you would troubleshoot interface issues between your code and a teammate’s module?

Medium

Approach

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

Partial form save that survives a three-day gap

Easy · write-heavy · optimistic concurrency · retention

Intake renders versioned form definitions and must persist partial submissions that constituents resume days later from a different device, a library machine, or a contact-centre agent's screen. Steady state is 80,000 form sessions a day; the final day of an enrolment window is eight times that, with 30 percent of it in the closing hour. Assume a 25-field form autosaved once per section. Specify the storage grain and key, the peak write rate you design for, how a resumed draft binds to a rule version, and when an abandoned draft is destroyed.

Approach

  1. Size it before designing it. 640,000 sessions at eight section saves each is 5.1 million writes; spread over a 10-hour active day that is about 140 saves a second, and the closing hour carrying 30 percent of volume is roughly 430 a second. That is a small write rate, so the binding constraint is durability and resumability, not throughput.
  2. Choose one row per draft holding the whole answer document as JSONB plus a row_version, not one row per field. A section save becomes a single UPDATE ... SET answers = $1, row_version = row_version + 1 WHERE draft_id = $2 AND row_version = $3, so a second open tab affects zero rows and is told it lost, instead of interleaving two versions of one person's answers.
  3. Make the resume key independent of the browser. Bind the draft to constituent_id when the session is authenticated; otherwise issue a claim code the constituent can write down or receive by SMS. A cookie does not survive a public terminal, and the population this form exists for is disproportionately on public terminals.
  4. Separate the two versions in play. Pin form_definition_version on the draft so the rendered questions stay stable across the gap, but do not freeze rule_version_id until submission, since the case is adjudicated against the ruleset in force at submission. If the ruleset changes mid-draft, re-render the affected sections and require re-confirmation rather than carrying answers to questions that no longer exist.
  5. Give the draft its own retention. An abandoned draft holds protected personal data, so assign a retention_class at creation with the clock running from last_touched_at, and destroy on schedule rather than leaving an unsubmitted form in the table forever.

Worked solution (20 min)

  1. Write the DDL for form_draft(draft_id, form_definition_version, constituent_id NULL, claim_code TEXT NULL, answers JSONB, row_version INT NOT NULL, last_touched_at TIMESTAMPTZ, retention_class) with a CHECK that constituent_id or claim_code is non-NULL.
  2. Derive the peak-day write rate from 640,000 sessions and eight section saves each, stating both the daily mean and the closing-hour figure.
  3. Write the conditional UPDATE with the row_version predicate and state explicitly what zero rows affected means to the caller.
  4. Write the submission step: freeze rule_version_id and the evidence snapshot hash, insert the case, and leave the draft in place under its retention class instead of deleting it.

Expected result: One row per draft with optimistic concurrency; roughly 140 saves a second mean and about 430 a second in the closing hour; a resume path that works with no cookie; form_definition_version pinned at draft creation while rule_version_id is frozen only at submission; and a retention class whose clock starts at last touch.

Verify it yourself

  • Two concurrent saves from two tabs: exactly one commits, the other sees zero rows affected and is not silently discarded.
  • A draft started before a rule change and submitted after it carries the post-change rule_version_id, and you can state the rule that makes that correct.
  • A resumed draft is reachable on a different device with no cookie and no prior session.

Follow-up

  • The form is open in two tabs and the constituent submits from the stale one. What does the server do, and what does the constituent see?
  • A draft is saved at 23:58:30 on the closing day and submitted at 00:00:04. Which rule version applies, and is that a code decision or a policy decision?
  • The answer document grows to 40 KB. What does each autosave cost in the storage engine, and would you split it?

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 fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.

Day 1 — Measure before reasoning

  • Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
  • Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
  • Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.

Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.

Day 2 — References, copies, and the bugs they produce

  • Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
  • Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
  • Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.

Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.

Day 3 — Types, once, in a language that checks them

  • Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
  • Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
  • Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.

Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.

Day 4 — Concurrency, starting with what actually runs at the same time

  • Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
  • Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
  • Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.

Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.

Day 5 — Debugging as a procedure rather than an instinct

  • Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
  • Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
  • Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.

Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.

Day 6 — Tests that catch the bug you are about to write

  • Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
  • Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
  • Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.

Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.

Day 7 — Debug something broken, out loud

  • Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
  • Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
  • Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.

Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.

Bounding a vague ticket before writing code

When the requirements were thin, the interesting part is how you fenced the problem off: the assumption you wrote down, who you got to confirm it, the narrow version you shipped first so the rest stayed cheap to change. Guessing and being right is luck. Guessing in writing, where someone could correct you, is method.

Describe your experience working in an Agile environment.

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Close with what you would do differently, concretely.

Follow-up

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

What motivated you to apply to Kratos Defense & Security Solutions?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Close with what you would do differently, concretely.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

Tell us about a project you are particularly proud of and your specific role in its success.

Medium · behavioural and engineering judgement

Approach

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

How do you bridge the gap when you are assigned a task involving a technology or domain you haven't mastered yet?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Close with what you would do differently, concretely.
  3. Name the disagreement and how you resolved it with evidence.

Follow-up

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