Interview guides · Applied Training Solutions
Applied Training Solutions Software Engineer Interview Questions & Guide 2026
Applied Training Solutions software engineer prep on partial failure: a timed-out request has an unknown outcome, not a failed one.
The 60-second brief
A Software Engineer at Applied Training Solutions plays a pivotal role in bridging the gap between complex training requirements and high-performance technical execution. You will be responsible for designing and implementing software solutions that support mission-critical training environments, often working on projects that require precision, reliability, and scalability. Whether you are developing specialized GIS applications or contributing to Naval Undersea Warfare Center (NUWC) projects, your work directly impacts the readiness and proficiency of the end-users who rely on these systems.
For a design discussion, a catalogue of architectures is worth less than the ability to turn a vague requirement into a data model and an API contract. A box diagram with no schema under it collapses at the first follow-up question.
Focus areas
- Bound credential lifetime by the engagement end date
- Evolve schemas expand-contract across un-upgradable client deployments
- Scope every query by tenant below application code
Overview
The role is both intellectually demanding and strategically significant. You will often operate at the intersection of domain-specific constraints and modern software engineering practices. This position is ideal for engineers who enjoy solving non-trivial problems, thrive in environments where the output has a clear, tangible purpose, and are comfortable collaborating across multidisciplinary teams to deliver robust, high-quality code.
Where to focus your preparation
Treat the sections below as preparation areas and confirm the format with your recruiter.
Preparation focus
No round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.
What to demonstrate
- Breadth across SQL, experimentation and product reasoning
- Ability to state assumptions before choosing a method
How to prepare
- Drill the practice exercises below and time yourself
- Prepare three quantified stories about decisions you drove
Where candidates lose points
Tenant context leaking across a connection pool
Setting the tenant on a connection and relying on it for the rest of the request looks correct in every test that runs one request at a time. Under transaction pooling the connection goes back to the pool with that session state still attached, and the next checkout, serving a different client, inherits it; row-level security then enforces the previous tenant's policy flawlessly and returns the wrong rows. It reproduces only under concurrency, which is the load profile your integration tests do not have. Use a transaction-scoped setting, and assert that the setting matches the request's tenant immediately before the first query rather than trusting that it was set correctly upstream.
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.
Assuming the bug is in the framework
Suspect your own code first: read the stack trace top to bottom, check which versions are actually installed rather than which ones you believe are, and reproduce in isolation before blaming a library that thousands of people run daily. When the fault really is upstream, you need that minimal reproduction to say so credibly anyway.
Going silent while thinking
Narrate the candidates and why you are discarding them, even in fragments: sorting first would make this a two-pointer scan, but it destroys the original indices, which the output needs. From the other side of the table, a candidate thinking hard and a candidate stuck are indistinguishable until one of them speaks.
Questions & practice
Measure credential exposure past engagement close without double counting
Medium · interval merge · sweep line · credential ttl
For one engagement you have credential_grant rows: (grant_id, principal_id, target_system_id, issued_at, not_after, revoked_at which may be NULL). The cutoff instant T is the engagement's ends_on plus access_grace_days. A grant is live over the half-open interval [issued_at, min(not_after, revoked_at)). There are up to 200,000 grants across target systems. For each target_system_id, return the total wall-clock time after T during which at least one grant was live, plus the disjoint segments that make it up. Grants that overlap must be counted once.
Approach
- Clip each grant to [max(issued_at, T), min(not_after, coalesce(revoked_at, +inf))) and discard any interval whose start is not strictly before its end. O(n), and it removes every grant that already expired inside the engagement window.
- Bucket the survivors by target_system_id and sort each bucket by start. O(n log n) overall, and sorting dominates the whole algorithm.
- Sweep each bucket once, holding one open segment [s,e): if the next start is greater than e, emit [s,e) and open a new segment, otherwise set e = max(e, next_end). O(n) after the sort, O(1) working state, O(number of emitted segments) output.
- Total exposure is the sum of emitted segment lengths, which is the measure of the union. Summing per-grant durations instead answers a different question and is unbounded above by the elapsed wall clock.
- Produce a second, conservative figure that ignores revoked_at and uses not_after alone. revoked_at records that you asked for revocation; whether the client's identity provider honoured it is not something this table knows, and a self-contained token validated offline stays valid to its own expiry regardless.
- If a bucket does not fit in memory, sort externally and sweep the stream, or push end timestamps into a min-heap keyed by end and pop those below the current start. Same O(n log n), memory proportional to the maximum number of concurrently live grants.
Follow-up
- Three grants overlap and one of them belongs to an offboarded contractor. What must the sweep emit so exposure can be attributed per principal?
- Which of your two numbers is the real bound on access if the client system validates tokens offline, and what does that imply about issuing TTLs in the first place?
- Run this across 20,000 engagements as a nightly job with a fixed memory budget. What changes in the shape of the computation?
Assign connector bindings to rollout waves and name the cycle
Medium · topological sort · cycle detection · dependency graph
Connector bindings declare ordering dependencies: an edge u -> v means v must not run in a wave earlier than one after u has succeeded. You have up to 50,000 bindings and 200,000 edges, and a misconfigured dependency may have created a cycle. Assign each binding the earliest wave number such that every predecessor sits in a strictly earlier wave, return the total wave count, and if a cycle exists return the bindings on one concrete cycle rather than a boolean. The implementation must be iterative: a dependency chain can be 50,000 deep.
Approach
- Build an adjacency list and an indegree array, then run Kahn's algorithm from every indegree-zero node. Bindings with no predecessors get wave 0. O(V+E) time, O(V+E) space.
- Relax waves as you decrement: when popping u, set wave[v] = max(wave[v], wave[u]+1) for each successor before enqueueing v at indegree zero. Because v is only enqueued after its last predecessor is popped, wave[v] is 1 + max over predecessors, which is the earliest legal wave. Total wave count is max(wave)+1, the longest path in nodes.
- If Kahn emits fewer than V nodes, the residual subgraph is exactly the nodes lying on or downstream of a cycle. Every residual node still has an in-edge inside the residual, otherwise it would have been popped, so walking backwards along in-edges cannot dead-end and must revisit a node within |residual| steps; the segment between the two visits is a concrete cycle. O(V) with a visited-position map.
- Use an explicit stack or a queue rather than recursion. Recursive DFS on a 50,000-node chain exceeds the default stack in most runtimes, and the failure looks like a crash rather than a cycle.
- At this size, recompute the whole plan whenever an edge changes. Incremental topological order maintenance costs far more code than the O(V+E) rebuild it saves.
Follow-up
- Two bindings land in the same wave but hit the same client endpoint, whose rate budget is shared. How does the wave plan have to change, and what problem class does that turn into?
- One dependency holds only for overlapping watermark ranges rather than globally. Is the dependency graph still a DAG over bindings, and what is the right vertex if not?
- Report the critical path so an operator can see which chain sets the wave count, not just that it is five.
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.
Worked solution (40 min)
- Span [00:00, 06:00). Runs: K1 [00:00, 02:00) succeeded; K2 [02:00, 03:00) attempt 1 ambiguous and attempt 2 failed; K3 [03:00, 04:00) succeeded; K4 [04:00, 05:00) abandoned. Nothing claims [05:00, 06:00). Chunk size is one hour.
- Merge succeeded ranges: [00:00, 02:00) and [03:00, 04:00). They do not touch, so the union has two segments.
- Complement inside the span: gaps are [02:00, 03:00) and [04:00, 06:00).
- Match claims to gaps: K2 intersects the first gap and is emitted at [02:00, 03:00); K4 intersects the second and is emitted at [04:00, 05:00). Both keep their original ranges and therefore their original keys.
- Subtract the emitted claims from the gaps: [05:00, 06:00) remains, unclaimed by any attempt, and becomes a single new claim at the one-hour chunk size.
- Compare against the merge shortcut: emitting the two gaps as [02:00, 03:00) and [04:00, 06:00) would give the second range a key that no prior attempt ever used, discarding K4's ledger history.
Expected result: A three-entry plan: retry K2 at [02:00, 03:00) with its existing key, retry K4 at [04:00, 05:00) with its existing key, and one new claim at [05:00, 06:00). Two of the three reuse a reproducible key; only the never-claimed hour gets a new one.
Verify it yourself
- The union of the plan's ranges together with the succeeded union covers [00:00, 06:00) with no hole.
- Every entry tagged as a retry matches an existing run's watermark_from and watermark_to exactly, so recomputing its idempotency_key yields the same string.
- No emitted range is empty, and no emitted range extends outside the span.
- Running the planner a second time against unchanged input produces the identical three entries, so the plan itself is idempotent.
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?
Explain why the failed-run backlog query ignores three indexes
Medium · indexing · query planning · partial indexes
connector_run holds roughly six million rows over three years. Its indexes are (connector_id, started_at DESC); (started_at) WHERE status = 'failed'; and ((finished_at AT TIME ZONE 'UTC')::date). The operator backlog query is SELECT run_id, connector_id, failure_class, started_at FROM connector_run WHERE environment_id = $1 AND status = ANY($2) AND started_at >= now() - interval '7 days' ORDER BY started_at DESC LIMIT 50, with $2 bound to {failed, ambiguous}. It sequentially scans. Explain index by index why each is unusable, then give the one index you would add.
Approach
- Index one leads on connector_id, which this query never constrains. A B-tree can only be probed from a prefix of its key columns, so with no equality on connector_id it degenerates into a full index scan plus heap fetches, which the planner will rightly cost above a sequential scan. PostgreSQL 18 added B-tree skip scan for an unconstrained prefix, so state your server version before assuming this is fatal; it helps only when that prefix has few distinct values, and hundreds of connectors is already too many to rely on it.
- Index two is partial on status = 'failed', and the rows the query wants are partly not in it at all. Predicate implication runs one way: the query's predicate must imply the index's. status IN ('failed','ambiguous') is weaker than status = 'failed' and implies nothing, so no amount of inlining the parameter helps. It would serve only the failed branch of a UNION ALL.
- Index three is an expression index on a function of finished_at, while the query filters started_at as a plain range. Even on the right column, a range predicate on the bare column cannot use an index on a function of it. Note also why the AT TIME ZONE 'UTC' wrapper is present: finished_at::date on a timestamptz is STABLE rather than IMMUTABLE because it depends on the session TimeZone, so PostgreSQL refuses to index it without a fixed zone.
- Add (environment_id, started_at DESC) WHERE status IN ('failed','ambiguous'): equality on the scoping column first so the scan starts in the right place, the ordering column second so ORDER BY started_at DESC LIMIT 50 stops after fifty index entries instead of sorting the week, and partial on the status set so the index covers the small failing minority rather than six million rows.
- Confirm the partial predicate is provable for the query's parameterised form. Under a custom plan the array parameter is substituted as a constant and the prover handles the ScalarArrayOpExpr; under a generic plan it stays a Param and the proof fails, so check plan_cache_mode or the sixth execution of a prepared statement before declaring victory.
- Say what you deliberately did not do. Adding INCLUDE (run_id, connector_id, failure_class) to chase an index-only scan pays only while the visibility map is current, which ties the query's latency to whether autovacuum is keeping up with the write rate — a dependency worth accepting consciously, not by accident.
Follow-up
- The same query is also run without the environment filter, for an estate-wide backlog. Does your index still help, and what would you add instead?
- How does row-level security on this table interact with your index, given that policy quals are applied before non-leakproof user quals?
- Six months from now the partial index covers 30 percent of the table. What signal tells you to change it?
Denormalise the entitlement snapshot without freezing the clock
Medium · denormalisation · read patterns · caching
Authorisation currently joins engagement (effective-dated, with status, ends_on, access_grace_days, effective_from, effective_to), entitlement and principal_assignment on every request, at the platform's full read rate, against tables taking a few hundred writes a day. You are asked to publish a denormalised snapshot that callers cache with a few seconds of staleness and invalidate on change events. Specify the snapshot's key and shape, say which columns you denormalise, name the one thing that must not be precomputed, and say what a cache miss does.
Approach
- Justify the shape from the ratio rather than from taste: a few hundred writes a day against the whole estate's read rate is the case for a published read model. The normalised tables stay the system of record; the snapshot is derived, versioned and disposable, and can be rebuilt from source at any time.
- Key it on (client_id, principal_id) with a monotonic snapshot_version and a generated_at. Consumers cache by version, refresh on a change event, and reject anything older than the staleness bound, which turns the bound into an enforced property instead of an assumption about how fast events usually arrive.
- Denormalise only what changes on a write: the open engagement version's engagement_id, status, isolation_tier, data_region, ends_on, access_grace_days, and the resolved scope set. Resolving the effective-dated row once at publish time — selecting the version WHERE effective_to IS NULL — is the entire saving, because that resolution is what the per-request join was paying for.
- Do not precompute is_active or any boolean that folds now() into the stored value. A write event invalidates the snapshot; the passage of time does not, so an engagement that ended an hour ago keeps authorising until something unrelated triggers a republish. Ship ends_on and access_grace_days as data and compare them to the current time at read, so time-based expiry needs no invalidation at all.
- Fail closed on a miss or on over-staleness: deny, and do not fall through to the source tables. A fallback read path means the first mass cache miss converts an availability incident into either an authorisation bypass or a stampede onto the one service whose availability target already exceeds everyone else's.
- State the cost you accepted: a revoked entitlement is still honoured for up to the staleness bound. That is defensible for widening access and not for narrowing it, so pair it with an out-of-band deny signal for revocation and closure, and write the residual window into the runbook instead of leaving it for an auditor to discover.
Worked solution (25 min)
- Write the snapshot record's field list and key, marking each field as write-invalidated or time-evaluated.
- Write the publisher query that resolves the open engagement version and aggregates scopes per (client_id, principal_id).
- Write the consumer's decision function, taking the snapshot plus a current timestamp, and show that it denies on missing, on stale, and on ends_on plus grace elapsed.
- Write the reconciliation query that recomputes a sample of keys from source and diffs them against the published snapshot.
Expected result: A (client_id, principal_id)-keyed record carrying resolved scopes plus engagement_id, status, isolation_tier, data_region, ends_on, access_grace_days, snapshot_version and generated_at; every time-dependent decision evaluated at read; missing or over-stale entries denied rather than backfilled from source.
Verify it yourself
- Freeze the publisher and advance the clock past ends_on plus grace: authorisation must start failing with no new event and no republish.
- Delete a snapshot entry and confirm the caller denies rather than querying the source tables.
- Reconciliation over a sample of keys reports zero differences outside the staleness bound; any difference inside it is explainable by a specific event.
Follow-up
- How do you detect the snapshot quietly diverging from the source tables, given that a wrong snapshot is silently permissive?
- An amendment creates a new engagement version mid-request. Which version does the in-flight request use, and can you defend that?
- The snapshot carries data_region. What stops the snapshot itself from being replicated somewhere its own residency rule forbids?
How do you ensure your code is maintainable and scalable?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Describe a challenging technical problem you solved recently and your approach to it.
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Fan out desired state and collect convergence evidence per tier
Medium · fan-out · reconciliation · partial observability · rollout
The control plane is the only writer of desired state for a few hundred environments (environment_id, isolation_tier, region, desired_release_id, reported_release_id, reported_config_fingerprint, status, last_heartbeat_at). Shared-pool and dedicated environments sit in networks you can reach; client-managed installs do not and can only report outward on their own schedule, sometimes not for weeks. You have to roll out a release and then, later, decide it is safe to remove a deprecated API field. Design the distribution path and the reporting path, and define exactly what converged means.
Approach
- Split delivery by reachability instead of pretending one mechanism covers both. Reachable tiers get a push-and-reconcile loop you drive; client-managed installs pull desired state and post a report, because nothing you build reaches into a network you have no route to. Both write the same reported_ columns, so the question of what is live stays one query rather than two subsystems with different truths.
- Define converged as a positive statement from the environment: reported_release_id equals desired_release_id, reported_config_fingerprint equals the fingerprint of the desired config, and last_heartbeat_at falls inside that tier's freshness window. A NULL reported_release_id means unknown, and unknown must never be folded into converged - that fold is precisely how a client's install ends up on a version nobody knew was running.
- Recognise that a few hundred targets is not a throughput problem, so spend the design on evidence rather than on delivery concurrency. Carry reported_at per environment, expose its age, and make any 'N of M converged' number carry the age distribution of the missing rows; without that, the dashboard's reassuring figure is the thing that causes the incident.
- Gate the contract step on observed convergence, never on elapsed time. The removal condition is that no fresh report names a release below the target and no environment is stale or unknown, which means one silent install blocks the removal - the correct and unpopular answer. Then make the block actionable by reporting it per client with an age, so it becomes a specific conversation rather than an indefinite wait.
- Bound the version spread rather than discovering it later: refuse to provision or upgrade onto a release outside the supported window, and track max(desired) minus min(reported) as a first-class number. An unmeasured spread widens until some schema change becomes impossible, and by then the cost lands on whoever proposes it.
Worked solution (25 min)
- Load 300 environment rows across the three tiers with a dozen distinct release ids and heartbeat ages from seconds to four months.
- Write convergence as a three-part condition and removal-safety as its negation, then run both and read the outputs.
- Set reported_release_id to NULL on 20 client-managed rows and re-run removal-safety.
- Introduce a config fingerprint mismatch on an environment whose release id matches, and see which condition catches it.
Expected result: Removal-safety returns blocked whenever any environment is stale or unknown, and names them with their client and report age rather than returning a bare boolean. The NULL rows block removal instead of counting as converged. The fingerprint mismatch is caught by the config term while the release term passes, which is why convergence is three conditions rather than one.
Verify it yourself
- No query on the rollout path reads reported_release_id without also testing heartbeat freshness.
- Converged, diverged, stale and unknown are exhaustive and mutually exclusive, and their counts sum to the environment total.
- Provisioning an environment onto a release outside the supported window fails rather than widening the spread.
Follow-up
- A client-managed install has not reported for four months. It may be decommissioned or it may be serving. What do you do before removing the field?
- One environment reports a release id you never published. What are the candidate explanations, and what do you do with the row meanwhile?
- How would a rollout stop itself automatically, and what signal could you possibly have for a tier you cannot instrument?
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
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
Scope is read from what you decided
Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.
Can you describe a time you had to adapt to changing project requirements?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you approach working with team members who have different technical backgrounds?
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.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
What interested you in the company?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
- 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 did you decide not to do, and why?
How do you handle feedback on your code or design choices?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?