Interview guides · Astos Solutions
Astos Solutions Software Engineer Interview Questions & Guide 2026
Caching practice for Astos Solutions software engineer prep: settle the staleness budget before choosing an invalidation strategy.
The 60-second brief
As a Software Engineer at Astos Solutions, you are at the intersection of complex aerospace engineering and high-performance software development. You will be instrumental in building the tools and systems that enable mission-critical analysis, maneuver planning, and guidance, navigation, and control (GNC/AOCS) operations. This role is not just about writing code; it is about ensuring the reliability and precision required for space-bound missions.
Prepare in one language you know well enough to debug in rather than the one you think reads best. Under a clock an unfamiliar language costs you standard-library lookups and iteration mechanics, and that time comes out of your thinking budget, not your typing budget.
Focus areas
- Version an API you cannot force clients to upgrade
- Scope every query by tenant below application code
- Diagnose failures inside environments you cannot instrument
Overview
The work you do directly impacts the success of satellite operations and complex orbital mechanics projects. Whether you are developing interfaces for mission control or building the backend logic for trajectory optimization, your contributions affect the mission outcomes of Astos Solutions' clients. You will work within a specialized, high-stakes environment where technical rigor, mathematical precision, and a deep understanding of aerospace software requirements are essential for success.
The Astos Solutions interview process
Astos Solutions candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Application Review
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
Technical Screening
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Technical Discussions
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Where candidates lose points
Validating against a client sandbox and assuming production parity
Sandboxes typically carry smaller data, looser or absent rate limits, a schema version behind production, and sometimes synchronous behaviour where production is asynchronous. Code that passes there fails first in the client's production, during a change window you do not control and often cannot get a second one of. The mitigations are specific: assert the observed schema version at the boundary of every run, measure the production rate limit empirically rather than reading it from a document, and design the first production run to be a bounded, reversible slice rather than a full backfill.
Long-lived shared credentials for client systems
One static key used by the connector runtime, a debugging script and three engineers cannot be attributed to a person in an audit, cannot be expired at engagement close, and cannot be rotated without breaking an unknown number of consumers at once. The expensive part of the eventual incident is not the leak; it is that rotation requires finding every holder under time pressure, and nothing recorded who they were. Issue per-principal, per-target, short-lived grants from a broker so rotation is a no-op, attribution is automatic, and closure is a TTL rather than a search.
Sharing mutable state with no stated owner
Say which thread, request or task owns each mutable structure, and what protects it when the answer is more than one: a lock, a queue that hands ownership across, or an immutable copy per reader. A structure documented as safe for concurrent reads is usually not safe for a concurrent write alongside those reads.
Tests that assert on the implementation rather than the behaviour
Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.
Questions & practice
Resolve effective-dated engagement versions for two million access checks
Hard · binary search · effective dating · complexity budget
The engagement table holds 500,000 effective-dated rows: (engagement_id, version, effective_from, effective_to which is NULL for the open version). A database exclusion constraint guarantees that per engagement_id the tstzrange(effective_from, effective_to) values never overlap, and the ranges are half-open. You must answer 2,000,000 access checks of the form (engagement_id, at_time) in one process with no database, returning the version open at that instant or none. Also report, per engagement, any coverage gap between adjacent versions. Give the complexity of the scan-per-check approach and say why it is not an option.
Approach
- State the naive cost first: scanning the table per check is 2e6 x 5e5 = 1e12 range comparisons. Even at 1e9 simple comparisons per second that is roughly fifteen minutes of pure compare work, against a budget measured in seconds. It is correct, just unaffordable, and the fix is an index built once rather than a faster comparison.
- Build a hash map engagement_id -> array of versions sorted by effective_from, once. O(V log V) to sort, O(V) memory. The build is amortised over 2e6 lookups, which is what makes it worth doing at all.
- Answer each check by binary-searching for the last effective_from <= at_time, then confirming effective_to IS NULL OR at_time < effective_to. If the confirmation fails, the instant lies in a gap and the answer is none, not the neighbouring version. O(log v) per check, O(V log V + Q log v) total.
- Name the precondition: predecessor search is only valid because the ranges per engagement do not overlap, which the exclusion constraint enforces with btree_gist for the equality operator. Without that guarantee you need an interval tree returning all stabbed intervals, at O(log n + k). Use the same half-open convention as the database, where at_time equal to effective_to belongs to the next version.
- Find gaps with one O(V) pass over each sorted array comparing prev.effective_to against next.effective_from. The exclusion constraint forbids overlaps, not holes, so a hole is entirely possible and is where an access check fails closed for a reason nobody can see in the contract.
- If the checks can be buffered, sort them by (engagement_id, at_time) and merge against the version arrays: O(Q log Q + V log V), usually a large constant-factor win over 2e6 random binary searches because the merge walks memory in order.
Follow-up
- The open version has effective_to NULL. How do you keep both the sort and the comparison total without a sentinel that overflows your timestamp type?
- A gap exists for one engagement and a check lands in it. What should the access path return, and what should it emit so someone fixes the amendment?
- Amendments are being written while you answer checks. What read model keeps an answer self-consistent, and what does a check mean if the snapshot is a second stale?
Top-k longest connector runs from a stream too large to sort
Medium · heap · top-k · streaming
A day of connector_run rows sits in object storage: 200,000,000 rows, unsorted, far larger than memory, and streamable once. Each row has run_id, connector_id, started_at and finished_at, where finished_at is NULL for runs that never reached a terminal state. Return the 200 longest terminated runs by (finished_at - started_at), ties broken in favour of the lower run_id, plus the count of rows with a NULL finished_at. You may not sort the input or hold it in memory. State the time and space complexity you achieve.
Approach
- Keep a bounded min-heap of capacity k=200 ordered on (duration, -run_id). The root is then the weakest retained entry: smallest duration, and on a duration tie the largest run_id, which is exactly the one the stated tie-break should evict.
- Per row: if finished_at is NULL, increment a counter and skip; otherwise compute the duration and push when the heap holds fewer than k, else compare against the root and replace only if strictly better. O(n log k) worst case, O(k) space, and after the heap warms nearly every row costs a single comparison against the root.
- Reject the alternatives explicitly. A full sort is O(n log n) and at 2e8 rows means an external merge sort over hundreds of gigabytes to produce 200 rows. Quickselect is O(n) average but needs the whole array resident, which the constraint forbids.
- Handle the NULL as data, not as an edge case: depending on the language, subtracting from NULL either throws or yields a sentinel that lands at the top of the list. Count those rows and report them, because a spike of unterminated runs is its own signal.
- To parallelise, give each of p shard readers its own size-k heap and merge the p results. The union of per-shard top-k sets contains the global top-k, because an element beaten by fewer than k rows globally is beaten by fewer than k rows in its own shard, so the merge is exact at O(pk log k).
Worked solution (20 min)
- Stream eleven rows as (run_id, duration_seconds): (1,12) (2,405) (3,7) (4,88) (5,405) (6,3) (7,1200) (8,61) (9,NULL) (10,99) (11,405). Use k=3.
- Fill the heap with the first three terminated rows, then maintain it: after (2,405) and (5,405) and (7,1200) have been seen, the heap holds those three and the root is (405, -5), which is run 5.
- Row 9 has no finished_at, so it increments the unterminated counter and never reaches the heap.
- Row 11 has duration 405, equal to the root's duration, but run_id 11 > 5, so under (duration, -run_id) it is not strictly better than the root and is discarded on the tie-break rather than on duration.
- Drain the heap in descending order to produce the final list.
Expected result: Top 3 = run 7 (1200s), run 2 (405s), run 5 (405s). Unterminated count = 1 (run 9). Run 11, also at 405s, is excluded by the lower-run_id tie-break.
Verify it yourself
- The heap never holds more than k=3 entries at any point in the stream.
- The heap root is always the weakest retained entry, so every discarded row compared unfavourably against it.
- Brute-force sorting the 10 terminated rows and taking the first 3 gives the same set, including the same tie resolution.
- Splitting the same stream into two shards with independent size-3 heaps and merging yields the identical top 3.
Follow-up
- Now return the top 20 bindings by total run time instead of the top runs. What changes, and how many distinct keys must you hold to make it exact?
- The key space is too large to hold an exact aggregate. What does a Misra-Gries summary with m counters actually guarantee about the counts it returns?
- Rows now arrive continuously and you want a rolling one-hour top-k. Does the bounded heap still work, and if not, what breaks?
Compute the minimal re-run set that preserves idempotency keys
Hard · interval complement · idempotency keys · watermarks
One connector binding must cover [span_start, span_end). You have all its connector_run rows: (idempotency_key, attempt_no, status, watermark_from, watermark_to), with half-open ranges, where idempotency_key is derived deterministically from the connector and the range, so re-running a range reproduces its key. Only status='succeeded' counts as covered. Produce the minimal re-run plan: every instant in the span ends up covered, any sub-interval some prior run already claimed is re-run at exactly that claim's range, and genuinely unclaimed sub-intervals become new claims cut at the binding's chunk size. Explain what merging ranges would cost.
Approach
- Merge the succeeded ranges into a maximal disjoint union: sort by start, sweep extending the open segment while the next start is not beyond the current end. O(n log n) time, O(n) space, and this is the only status that contributes coverage.
- Take the complement of that union inside [span_start, span_end) to get the uncovered gaps, in O(u) over the merged segments. Clip at both ends so the plan never reaches outside the span.
- Sweep the non-succeeded claims (ambiguous, failed, abandoned) against the gap list with a two-pointer over both sorted lists. Any claim intersecting a gap is emitted at its original watermark_from and watermark_to, byte for byte, so the derived idempotency_key reproduces and the retry rejoins the same ledger row. O(n) after sorting.
- Subtract the emitted claim ranges from the gaps; whatever survives was never claimed by any attempt and becomes fresh ranges cut at the chunk size. Only these get new keys. O(u + c).
- Accept that a re-emitted claim may overlap time that is already succeeded. Range bookkeeping is not what makes the retry safe; the record-level dedupe key is, and re-reading a covered window costs one extra fetch against the client's quota, which is far cheaper than a key you can no longer reproduce.
- Exclude anything still in status 'running' with a live lease. Re-claiming it races the in-flight attempt, doubles the quota spend against the client endpoint, and produces two ledger writers for one key. Total plan cost is O(n log n) time and O(n) space.
Follow-up
- An ambiguous run may have applied every record before the timeout. What do you read to settle it before re-running, and what is left if the client's schema offers no natural key?
- Retention pruned the ledger rows for one of these keys. What does the re-run do now, and what bounds how long the ledger must be kept?
- Two planners run concurrently against the same binding. What stops them emitting and executing the same plan twice?
Return the latest attempt per connector idempotency key
Easy · distinct-on · indexing · idempotency
connector_run stores one row per attempt: run_id, connector_id, idempotency_key, attempt_no SMALLINT, status (queued, running, succeeded, failed, ambiguous, cancelled, abandoned), failure_class, watermark_from, watermark_to, started_at, finished_at, unique on (connector_id, idempotency_key, attempt_no). For one connector_id, write the query returning the highest-numbered attempt per idempotency_key, keeping only those keys whose latest attempt is not succeeded, ordered by watermark_from. Give the index that serves it, and say what the plan does without that index.
Approach
- Use DISTINCT ON (idempotency_key) with ORDER BY idempotency_key, attempt_no DESC. PostgreSQL requires the ORDER BY to lead with exactly the DISTINCT ON expressions, and that requirement is what makes the row picked deterministic rather than whichever the scan happened to reach first.
- Apply the not-succeeded filter outside, in a wrapping SELECT. Putting it in the inner WHERE changes the question from 'keys whose latest attempt failed' to 'keys that have any non-succeeded attempt', which reports a key that failed twice and then succeeded, and is the single most common wrong answer here.
- Know the portable alternative: row_number() OVER (PARTITION BY idempotency_key ORDER BY attempt_no DESC) in a subquery, filtered = 1 outside. A window function cannot be filtered in WHERE because WHERE is evaluated before the window, and PostgreSQL has no QUALIFY clause, so the subquery is mandatory rather than stylistic.
- Index (connector_id, idempotency_key, attempt_no DESC). With connector_id fixed by equality, the index supplies the exact order DISTINCT ON needs and the Sort node disappears. It does not reduce rows read: through PostgreSQL 17 there is no loose index scan, so the executor still walks every attempt in range rather than skipping to each next distinct key.
- Give the complexity honestly: without the index the plan is a scan plus an O(n log n) sort over the connector's attempts; with it, O(n) ordered reads and constant working memory, where n is attempts in range, not distinct keys. If the distinct keys are a tiny fraction of attempts, a recursive skip-scan is the next step.
Follow-up
- attempt_no is not monotonic in time because a retry was queued out of order. Does your query still pick the right row, and which column should it actually order by?
- How would you return the latest attempt per key across all connectors in one environment without the query degrading to a full sort?
- An ambiguous latest attempt means the target may or may not have been written. What does this query feed, and should ambiguous be treated as failed here?
Soft-delete environments without breaking uniqueness or audit history
Easy · soft deletes · partial indexes · audit history
environment holds one row per provisioned deployment: environment_id, client_id, engagement_id, env_name, region, status, destroyed_at TIMESTAMPTZ NULL. Decommissioned environments stay readable for seven years, and env_name must be unique per client among live environments only, because a client may reuse a retired name. Design the constraint set and the history table, write the query that lists a client's live environments, and say what your design does about connector_run rows whose foreign key points at a destroyed environment.
Approach
- Keep the tombstone as a nullable timestamp rather than a boolean: destroyed_at carries when as well as whether, and enforce live-only uniqueness with a partial index, CREATE UNIQUE INDEX env_live_name ON environment (client_id, env_name) WHERE destroyed_at IS NULL, which indexes only the live subset and so stays small.
- Get the NULL semantics right before you pick a spelling. In a B-tree unique index two NULLs compare distinct by default, so plain UNIQUE (client_id, env_name, destroyed_at) enforces nothing at all among live rows — every live row is unique on the third column purely by virtue of being NULL. PostgreSQL 15's UNIQUE NULLS NOT DISTINCT (client_id, env_name, destroyed_at) does satisfy the requirement: the NULL live rows now compare equal and collide with each other, while retired rows carry distinct destroyed_at timestamps and stay distinct, so a name can be retired and reused any number of times.
- Prefer the partial index anyway, and be able to say why rather than asserting it. It indexes only the live subset — tens of rows per client — instead of seven years of tombstones; it needs no version floor; and it keeps destroyed_at out of the key, so it cannot be defeated by two rows retired at the same instant. That last case is reachable: now() is transaction-start time, so retiring a name, recreating it and retiring it again inside one transaction writes two identical destroyed_at values and raises a duplicate-key error under the constraint form. clock_timestamp() avoids it if you take that route.
- Put history in a separate append-only table written by an AFTER INSERT OR UPDATE OR DELETE trigger holding environment_id, changed_at, changed_by, op and the before and after row images. PostgreSQL has no system-versioned tables, so this is a trigger or a logical-decoding consumer, not a DDL flag, and the soft delete arrives at that trigger as an UPDATE rather than a DELETE.
- State the foreign-key consequence rather than leaving it implicit: a soft delete leaves every referencing connector_run row resolvable, which is exactly what the audit requirement wants, but it means no read is protected by the database. Give the predicate one home, a view or an RLS-backed view that every caller uses, instead of repeating destroyed_at IS NULL across hundreds of call sites.
- Size the retention: seven years of history plus tombstoned rows in the hot table. Partition the history table by month so expiry is a DETACH PARTITION rather than a DELETE that has to be vacuumed, and decide separately whether a residency-driven erasure request can be honoured against an append-only store at all.
Worked solution (20 min)
- Write the DDL with destroyed_at TIMESTAMPTZ NULL plus the partial unique index on (client_id, env_name) WHERE destroyed_at IS NULL.
- Insert two live rows sharing (client_id, env_name) and confirm the second is rejected; set destroyed_at on the first and confirm the insert now succeeds.
- Build the same table a second time under UNIQUE NULLS NOT DISTINCT (client_id, env_name, destroyed_at) and confirm it passes the same two tests, so you are choosing between two working designs rather than rejecting a broken one.
- Add environment_history (history_id, environment_id, changed_at, changed_by, op, row_before JSONB, row_after JSONB) and the AFTER trigger that populates it.
- Write the live-listing query: SELECT environment_id, env_name, region, status FROM environment WHERE client_id = $1 AND destroyed_at IS NULL ORDER BY env_name.
Expected result: A partial unique index covering only live rows, a separate append-only history table with its own partitioned retention, and a listing query whose predicate matches the index predicate exactly so the planner can use it.
Verify it yourself
- Run retire-and-recreate three times for the same name: three tombstoned rows and one live row coexist without violating the index.
- On the NULLS NOT DISTINCT variant, retire and recreate the same name twice inside one transaction and watch it fail on duplicate destroyed_at, since now() does not advance within a transaction; the partial index has no such failure mode.
- EXPLAIN the listing query and see the partial index; widen the predicate to destroyed_at IS NULL OR destroyed_at > now() and watch the index drop out, because that predicate no longer implies the index's.
- Soft-delete a row and confirm the history trigger fired; a DELETE-only trigger records nothing, which is the common way this audit trail ends up empty.
Follow-up
- What stops the next endpoint from forgetting the destroyed_at IS NULL predicate, and does that mechanism survive a developer writing raw SQL?
- The history table stores row images as JSONB and the base table gains a column. What do old rows now mean, and how do you read them six years later?
- A residency erasure request requires a genuine hard delete for one client. What breaks, and what do you keep?
How do you ensure the precision and stability of mission analysis software?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How would you architect a system to handle large-scale trajectory data efficiently?
Medium
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Walk us through your approach to debugging a multi-threaded application in a time-sensitive environment.
Medium
Approach
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Apply records to a client system exactly once under retries
Medium · idempotency · exactly-once · ambiguous failures · transactions
A connector run claims a watermark range, reads records from a client endpoint and writes them to a client-owned target that offers no idempotency key of its own. connector_run stores (connector_id, idempotency_key, attempt_no, status, failure_class, watermark_from, watermark_to, records_read, records_applied), and status includes 'ambiguous'. A write can time out after the request bytes were sent, leaving the target's state unknown. Design the apply loop so that any number of retries leaves the target exactly as one clean run would, and say precisely what advances the watermark and when.
Approach
- Derive the dedupe key deterministically from stable source fields - the source's own identifier plus a hash over exactly the fields you write - and store the input field list and a derivation version beside the hash. The client will eventually rename or retype one of those fields, and every key computed before that day must stay reproducible; without the version, the first schema change re-applies the entire history.
- Order the local writes so a crash is recoverable: insert the ledger row (connector_id, dedupe_key, unique) as a claim, write to the target, then mark it applied and advance the watermark in one local transaction. The target write cannot join that transaction - no client grants you a distributed one - so the claim row exists precisely to tell the retry that a write may have happened.
- Give ambiguity its own state. A timeout, a 502 from an intermediary and a reset connection after the bytes went out all leave the outcome unknown, and collapsing them into 'failed' duplicates on retry while collapsing them into 'succeeded' loses data silently. On retrying an ambiguous run, resolve per record: read back by natural key if the target has one, and if it does not, negotiate one writable reference field at integration time so the read-back is possible at all. That negotiation is cheaper than every reconciliation you would otherwise run forever.
- Advance the watermark only across a range whose ledger rows are all applied or all deduped, and advance it with deliberate overlap. Many source systems stamp updated_at at transaction start, so a record committed after your read can carry a timestamp inside a range you already consumed; re-claim a bounded lag window each time and let the dedupe key absorb the duplicates, which is what it is for.
- Bound the ledger's growth against that same window. Retention must exceed the maximum overlap plus the longest plausible late arrival, because pruning earlier converts a late record into a silent re-apply. Partition the ledger by watermark date so pruning is a partition drop rather than a mass delete.
Worked solution (30 min)
- Build the stand-in target the way the prompt describes the real one: an insert-only table with no unique index, no primary key over the dedupe key and no upsert path, plus one writable reference column holding the dedupe key, indexed but not unique. A duplicate write must succeed and be visible, or the harness dedupes for you and the run proves nothing.
- Implement claim, fetch, validate, write, ledger, advance, with the ambiguous-retry path resolving each record by selecting the reference column for the range's keys before writing, and writing only the misses. One run owns a watermark range at a time - read-then-write across two concurrent runs over the same keys is a race no constraint-free target can arbitrate.
- Kill the process between each adjacent pair of steps and, for all five crash points, run the retry and count rows in the target.
- Make the target accept a write but drop the response, then retry and count again.
- Remove the read-back from the ambiguous path and repeat the dropped-response case, to measure what the reconciliation is actually buying.
- Rename one source field, bump the derivation version, and re-run a range already processed under the old version.
Expected result: Every crash point ends with the same target row count as one clean run: the retry finds its ledger row either unapplied, in which case it reconciles the range against the reference column and writes only what is missing, or applied, in which case it skips. The dropped-response case yields one row because the read-back finds the record the first attempt wrote; with the read-back removed the identical run yields two, and that pair of numbers is the whole argument, since the target rejects nothing either way. The version bump re-applies nothing, because keys for already-processed rows are still computed under the old derivation.
Verify it yourself
- The stand-in target carries no unique index, no primary key over the reference column and no upsert path, asserted against its schema, so no row can be deduped by the target rather than by the apply loop.
- records_applied plus the dedupe hit count equals records_read on every run.
- No watermark has advanced past a range containing a ledger row that is claimed but not applied.
- Replaying an entire previous day's range end to end changes zero rows in the target, and the run log attributes each skip to a read-back hit rather than to a rejected write.
Follow-up
- The target exposes no natural key and no field you are allowed to write a reference into. What is your next move, and what does it cost per run?
- A succeeded run reports records_read 10,000 and records_applied 9,997. Is that a bug? What single query settles it?
- The client restores their source database to a point two hours earlier. What does your next run do, and what should it do?
Revenue dashboard and issued invoices disagree for one region
Medium · timezones · billing · reconciliation
Finance reports that the monthly revenue dashboard runs 0.3 to 1.2 percent below the invoiced total for one region every month, and that the gap is largest when the month ends on a Friday or Saturday. The invoice generator selects approved work_record rows with work_date between period_start and period_end. The dashboard aggregates the same table with date_trunc('month', entered_at) inside a separate reporting job. Say which figure is correct, name both defects, and give the fix.
Approach
- Reconcile rows, not totals. For one engagement and one month, take the set difference between the work_record_ids the generator selects and the ids the dashboard aggregates. A percentage gap is unarguable; a list of discrepant rows names the defect in a minute.
- Read the columns on the discrepant rows. They will share one pattern: work_date inside the period, entered_at after it. That identifies the first defect as column choice. entered_at is when somebody filled in the timesheet, routinely days later by design, so end-of-month work lands in the following bucket. The weekday correlation is the confirmation, because a month ending Friday or Saturday pushes more entries into Monday.
- Fix the column and reconcile again, expecting a residual for one region. date_trunc('month', x) on a timestamptz truncates in the session TimeZone setting; the reporting job runs with TimeZone = UTC while that region's teams work at UTC+10, so entries made in their local morning on the first carry a UTC instant on the previous day. The same applies to any timestamptz cast to date.
- Demonstrate rather than assert: run the same date_trunc over the same row with SET TimeZone = 'UTC' and then with the region's zone, and show the two buckets differ. That is the discriminating observation, and it takes one query.
- Fix by removing the ambiguity, not by compensating for it. Aggregate on work_date, which is a DATE and has no zone to be wrong about, and make it the only period key for both billing and reporting. Where a timestamptz genuinely must be bucketed, such as entry latency or an SLA timer, state the zone explicitly via date_trunc's three-argument form on PostgreSQL 16 or later, or an explicit AT TIME ZONE, and never rely on the session default.
- State the answer plainly: the invoice is correct and the dashboard is wrong. Do not reconcile by adjusting invoiced rows, which are immutable once locked.
Follow-up
- Finance asks for a restatement of the last twelve months. What is safe to restate, given that invoiced work is immutable and corrections are credit notes?
- Someone proposes storing work_date as a timestamptz for consistency with the rest of the schema. Argue the case either way.
- What test fails the build if a future reporting job reintroduces session-dependent bucketing?
Suggested practice checklist
Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.
Day 1 — Fix the scope and take a cold baseline
- Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
- Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
- Set the week's rule: one thing finished every night, including the night you only have forty minutes.
Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.
Day 2 — One pattern, written three times from blank
- Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
- On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
- Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.
Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.
Day 3 — One design, only to the depth you can defend
- Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
- Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
- Write the one tradeoff you are choosing against and the observation that would make you reverse it.
Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.
Day 4 — Only the fundamentals you will have to defend
- Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
- Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
- Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.
Deliverable: Three written answers, each under two hundred words and each timed aloud.
Day 5 — Your own work, timed
- Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
- Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
- Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.
Deliverable: Two timed narratives with one defensible number in the opening line.
Day 6 — The one full rehearsal, in the weekend block
- Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
- Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
- Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.
Deliverable: Mock notes naming three failure moments with a specific fix written under each.
Day 7 — Taper
- Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
- Re-read only your own notes from this week and open no new material.
- Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.
Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.
Know where your numbers came from
Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.
Describe your experience with Java and how you manage memory in a complex GUI application.
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
How do you handle edge cases in software that operates on physical, real-world constraints?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Describe a situation where you had to reconcile conflicting requirements from different engineering teams.
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?