cswork / libraryOpen practice workspace

Interview guides · Ensemble Health Partners

Ensemble Health Partners Software Engineer Interview Questions & Guide 2026

Testing practice for Ensemble Health Partners software engineer candidates: the boundaries worth covering, and why flakiness is a real defect.

The 60-second brief

As a Software Engineer at Ensemble Health Partners, you play a vital role in building and maintaining the technological infrastructure that powers healthcare revenue cycle management. Your work directly impacts how healthcare providers streamline operations, analyze complex medical data, and deliver financial clarity to patient care systems. Whether you are designing analytics engines, building application integrations, or developing developer platforms, your engineering contributions directly support the business objectives of major healthcare networks.

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

  • Design person merges that remain reversible afterwards
  • Hold accumulator limits under concurrent claim adjudication
  • Version clinical results instead of updating rows in place

Overview

This role requires a balance of technical rigor, domain curiosity, and collaborative problem-solving. You will tackle challenges ranging from standard software development life cycles to specialized technical domains like data engineering, analytics, and healthcare-specific coding definitions. The engineering environment at Ensemble Health Partners values practical, reliable solutions that scale smoothly across distributed teams and high-stakes healthcare environments.

You can expect an environment where your code directly influences operational efficiency. Teams often operate with a degree of informal collaboration, but they maintain high standards for technical accuracy and domain knowledge. Preparing for this role means demonstrating not only your core coding abilities but also your adaptability when facing domain-specific conceptual questions.

The Ensemble Health Partners interview process

Ensemble Health Partners candidates report 2 rounds · ≈ 2-4 weeks. The stages below are what candidates describe, not a published process.

Phone Screening

Before anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.

What to demonstrate

  • Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
  • Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
  • Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled

How to prepare

  • Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
  • Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
  • Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market

Technical Interviews

Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.

What to demonstrate

  • Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
  • Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
  • Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
  • Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply

How to prepare

  • For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
  • For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
  • Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last

Where candidates lose points

Treating a medical record number or a member ID as a globally unique key and joining on it directly.

These identifiers are unique only within the authority that issued them. Two facilities in one network routinely have the same medical record number for different people, and member IDs get reissued when someone changes plans. Joining on the bare value merges two patients' records, which is the most damaging failure available in this domain, and it passes every test written against a single-facility fixture because the collision only appears once a second source is connected.

Upserting on the order or result identifier, so a correction overwrites the original row.

It makes the question 'what did the clinician see at 14:02' unanswerable, which is exactly what an incident review or a legal hold asks. It also leaves downstream consumers that already acted on the preliminary value with no correction event to react to, because the state transition was collapsed into a single mutated row and never emitted.

A queue or buffer with no bound

Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.

Finishing a solution without stating its complexity

Give time and space in the same breath as the code, and define n explicitly when there are two sizes, since n nodes and m edges are not interchangeable. Space is the half that gets skipped: count the auxiliary structures you allocate and the recursion stack at its deepest, not only the answer you hand back.

Questions & practice

Can you write a clean, efficient script to solve a standard data manipulation challenge?

Medium · data structures and algorithms

Approach

  1. Walk one small example through your approach before writing the whole thing.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

How would you explain a conceptual problem in artificial intelligence or data modeling?

Medium · data structures and algorithms

Approach

  1. Choose the data structure from the access pattern, not from familiarity.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

What are the definitions of specific coding modifiers and medical or technical attributes?

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Name the brute-force solution and its complexity before improving on it.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • What is the worst case, and how likely is it on real data?

Flag a requester reading too many distinct charts per window

Medium · sliding window · two pointers · distinct count

You consume access-audit records (event_ts, requester_id, enterprise_person_id, purpose_of_use, break_glass) at tens of thousands per second, non-decreasing in event_ts. For each requester, emit an alert the first time any sliding 600-second window contains reads of more than D distinct enterprise_person_ids. Break-glass reads count toward the window and are also reported separately. Return (requester_id, window_start_ts, distinct_count). Target O(1) amortised per event and memory proportional to the events resident in the window, not to the day.

Approach

  1. Per requester, hold a deque of (event_ts, person_id) and a hash map from person_id to its occurrence count inside the window, plus a running distinct counter. Push on the right; while the front is older than event_ts minus 600 seconds, pop it, decrement its count and erase the key when the count reaches zero, decrementing the distinct counter. Each event is pushed once and popped once, so the amortised cost is O(1) and the memory is O(W) for window occupancy W.
  2. Test the threshold immediately after each push and nowhere else. Between two consecutive events the window can only lose members as its left edge advances, so the maximum distinct count over all window positions is attained at a position whose right edge is an event. Checking at pushes is therefore exhaustive rather than a sampling approximation.
  3. Latch the alert per requester and re-arm only when the distinct count falls back below D, otherwise one busy stretch emits thousands of near-identical rows and the real signal is buried by its own volume.
  4. Bound memory both per requester and globally. A requester whose window legitimately holds tens of thousands of reads must not hold the process hostage, so cap the deque and degrade above the cap to an approximate distinct counter such as HyperLogLog, stating the error you accept in exchange.
  5. Count break-glass toward the window but carry it in its own output field. Break-glass has to succeed during an emergency, which is exactly why it must be the most visible path in the audit, and excluding it from the count would make the abuse route the quiet one.
  6. State the precondition: this is correct only while input is non-decreasing in event_ts. Out-of-order arrival needs a bounded-lateness buffer and a watermark, and dropping late events without a counter is the failure that hides itself.

Worked solution (25 min)

  1. Implement push, then the eviction loop, then the distinct counter update, in that order, and assert the counter against the map size after each event.
  2. Set D to 25 and feed 15 distinct persons between 09:06:00 and 09:08:59.
  3. Feed 15 more distinct persons from 09:11:00, one every eight seconds.
  4. Record the event at which the alert fires and the window_start it reports.
  5. Re-run the same events through fixed ten-minute tumbling buckets and compare.

Expected result: The 26th distinct read lands at 09:12:20, its window covers 09:02:20 to 09:12:20 and holds all 15 from the first group plus 11 from the second, so the alert is (requester, 09:02:20, 26). Tumbling buckets of 09:00 to 09:10 and 09:10 to 09:20 see 15 and 15, and never fire.

Verify it yourself

  • The distinct counter equals the hash map's size after every event. A missed key erase makes it drift upward and manufacture alerts out of nothing.
  • Shifting every timestamp by a constant produces the same alert with a correspondingly shifted window_start.
  • Processing the stream in two halves sequentially yields the same alerts as one pass, provided the deque survives the split.
  • With D above the day's total distinct persons for that requester, nothing fires.

Follow-up

  • One region's events arrive up to 90 seconds late. What buffer do you add, and what does that do to alert latency?
  • One person uses two requester accounts. What has to change in the key, and what new false positive does that introduce?
  • How do you restore window state after a process restart without replaying the whole day?

Diagnose why an eligibility lookup ignores its index

Medium · indexing · query planning · partial indexes

A 40-million-row coverage_span serves eligibility lookups with: SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = $1 AND valid_to = 'infinity' AND effective_date <= $2 AND (termination_date IS NULL OR termination_date >= $2). An index exists on (effective_date). EXPLAIN (ANALYZE, BUFFERS) shows a sequential scan reading 1.8 million buffers at 2.4 seconds. Explain why that index cannot help, propose the index plus any schema change that makes the predicate index-friendly, and state what you expect the new plan and buffer count to be.

Approach

  1. Read the predicate for selectivity before touching the index. effective_date <= $2 matches most of a 40-million-row history, so a leading range scan on that column returns a large fraction of the table and the planner correctly prefers a sequential scan over millions of random heap fetches. The index is not being ignored; it is being rejected on cost.
  2. Put the equality column first. enterprise_person_id = $1 selects a handful of rows out of 40 million, so it must lead the composite index; a b-tree can only use columns after the first range predicate as filters, not as search bounds.
  3. Turn the open-ended termination into something indexable. The OR ... IS NULL branch forces either a BitmapOr or a post-index filter. Add coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED and rewrite the predicate to coverage_end >= $2; both COALESCE over a column and a date literal are immutable, so the generated column is legal.
  4. Make the index partial on WHERE valid_to = 'infinity'. Most rows in a bitemporal table are superseded beliefs, so the partial index is a fraction of the full one, stays in cache, and encodes the predicate for free rather than re-checking it per row.
  5. Consider the range alternative honestly: a daterange(effective_date, termination_date + 1, '[)') column with a GiST index and the && operator handles overlap queries and supports an EXCLUDE constraint against overlapping active coverage, but GiST lookups are slower than b-tree for this point-in-time pattern. Pick b-tree for the read path and keep GiST only if you also need the overlap constraint.
  6. Verify rather than assert: re-run EXPLAIN (ANALYZE, BUFFERS) and read Rows Removed by Filter and the shared hit/read split, not just the total time.

Follow-up

  • After the change the plan is still a sequential scan for one particular member. What single query tells you whether that is stale statistics, a genuinely huge row count for that person, or parameter-dependent plan caching?
  • Your generated column needs backfilling on a live table. Does adding a STORED generated column rewrite it, and what does that mean for your maintenance window?
  • How would you serve the same lookup when the answer must be as of a past system-time instant rather than the current belief, given your index is partial on valid_to = 'infinity'?

Bitemporal coverage table with retroactive termination queries

Easy · bitemporal · schema design · retroactive correction

You maintain coverage_span: coverage_id, enterprise_person_id, payer_id, plan_id, effective_date, termination_date (nullable, meaning open-ended, not unknown), status, valid_from timestamptz, valid_to timestamptz DEFAULT 'infinity'. An enrolment file received on 2026-03-05 terminates a member effective 2026-03-01, after a claim for service date 2026-03-03 was already adjudicated against the answer then in force. State your termination_date convention, write the two SELECTs — (a) is the person covered on 2026-03-03 as known now, (b) what the system believed on 2026-03-04 — and say precisely what the loader writes when the file lands.

Approach

  1. Separate the two time axes explicitly before writing anything: effective_date/termination_date are business time (when benefit applied), valid_from/valid_to are system time (when this row was the system's belief). Every query names both, or it is ambiguous.
  2. Pin the inclusivity convention, because the predicate depends on it. Enrolment files conventionally carry termination_date as the last covered day, so the business-time predicate is effective_date <= :svc AND (termination_date IS NULL OR termination_date >= :svc). If your feed uses an exclusive end, it becomes > :svc, and a half-open daterange column removes the ambiguity permanently.
  3. Write the as-known-now query by pinning system time to the open version (valid_to = 'infinity') and letting business time float to the service date.
  4. Write the as-of-belief query by pinning system time to an instant: valid_from <= :asof AND valid_to > :asof. Half-open on both ends is what makes 'exactly one row per key per instant' provable.
  5. Describe the write as one guarded statement rather than two independent ones: a data-modifying CTE whose UPDATE stamps the open row's valid_to to now() (the only UPDATE the design permits — it stamps system time, it never edits business facts) and returns that row's keys, with the INSERT of the new version selecting FROM that CTE. Feeding the INSERT from the UPDATE's RETURNING is what makes a replayed file a no-op; an unconditional INSERT leaves the partial unique index to raise 23505 instead, which is a backstop rather than idempotency. The pre-correction row stays readable either way, which is what makes the already-adjudicated claim defensible.

Worked solution (25 min)

  1. Write the DDL with both ranges half-open in system time: valid_from timestamptz NOT NULL, valid_to timestamptz NOT NULL DEFAULT 'infinity', and a partial unique index on (enterprise_person_id, plan_id, effective_date) WHERE valid_to = 'infinity' so only one belief is open per business period.
  2. Write query (a) against the open system-time version, with the business-time predicate on the service date.
  3. Write query (b) by replacing valid_to = 'infinity' with the half-open instant predicate, keeping the identical business-time predicate so the only thing that changed is which belief you read.
  4. Write the loader as one statement: WITH closed AS (UPDATE ... RETURNING ...) feeding INSERT ... SELECT FROM closed. Guard the UPDATE with valid_to = 'infinity' so a concurrent loader cannot close the same row twice, and with a termination_date IS DISTINCT FROM :new_term test so a replay of the same file matches no row and therefore inserts none. Under READ COMMITTED a second loader that blocks on the row lock re-evaluates the guard against the newly committed version, fails it, and writes nothing.
  5. Trace the 2026-03-05 file by hand: before it, one row (effective 2026-01-01, termination NULL, valid_to infinity); after it, two rows, and confirm (a) returns not-covered while (b) returns covered.

Expected result: (a) SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = :p AND valid_to = 'infinity' AND status = 'active' AND effective_date <= DATE '2026-03-03' AND (termination_date IS NULL OR termination_date >= DATE '2026-03-03'); returns no row. (b) the same query with valid_to = 'infinity' replaced by valid_from <= TIMESTAMPTZ '2026-03-04 00:00+00' AND valid_to > TIMESTAMPTZ '2026-03-04 00:00+00'; returns the pre-correction row with termination_date NULL. The loader runs as one statement: WITH closed AS ( UPDATE coverage_span SET valid_to = now() WHERE enterprise_person_id = :p AND plan_id = :pl AND effective_date = DATE '2026-01-01' AND valid_to = 'infinity' AND (termination_date IS DISTINCT FROM DATE '2026-03-01' OR status IS DISTINCT FROM 'terminated') RETURNING enterprise_person_id, payer_id, plan_id, effective_date ) INSERT INTO coverage_span (enterprise_person_id, payer_id, plan_id, effective_date, termination_date, status, valid_from, valid_to, source_file_id) SELECT enterprise_person_id, payer_id, plan_id, effective_date, DATE '2026-03-01', 'terminated', now(), 'infinity', :f FROM closed;

Verify it yourself

  • For any fixed instant, the system-time predicate selects exactly one row per (person, plan, business period): SELECT enterprise_person_id, plan_id, effective_date, count() FROM coverage_span WHERE valid_from <= :t AND valid_to > :t GROUP BY 1,2,3 HAVING count() > 1 returns zero rows.
  • Query (b) with :asof = now() returns exactly what query (a) returns — if it does not, the close-and-insert is not atomic or the half-open boundaries overlap.
  • No row's valid_from equals its valid_to, and no two versions of one coverage_id have overlapping [valid_from, valid_to).
  • Replaying the same enrolment file inserts nothing and raises nothing: the guarded UPDATE matches no row because the open version already carries termination_date 2026-03-01 and status 'terminated', and the INSERT selects FROM that UPDATE's RETURNING, so it has zero input rows. Assert the person's row count is unchanged. A loader that instead runs the INSERT unconditionally fails the replay with SQLSTATE 23505 from the partial unique index — that stops the duplicate, but it is an error the caller must handle, not an idempotent replay.

Follow-up

  • The claim adjudicated on 2026-03-03 is now retroactively uncovered. What does reprocessing have to read — the belief at adjudication time or the current truth — and what does the remittance reversal reference?
  • Add the constraint that stops two active coverage versions from overlapping in business time for the same person and plan, without blocking the superseded history rows.
  • A later file reinstates coverage with the same effective_date. How many rows exist for that period afterwards, and which one does query (a) return?

How do you approach intermediate software development problems under tight deadlines?

Medium

Approach

  1. Say what you would check first and why it is the highest-information step.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

Follow-up

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

Resolving surviving claim versions with late-arriving originals

Medium · versioning · late-arrival · reconciliation · materialization

claim_line carries (claim_id, claim_version, line_number, frequency_code in original, replacement or void; billed, allowed, paid and patient-responsibility amounts as bigint minor units; adjudication_status). Batch feeds do not guarantee the original arrives before the replacement that supersedes it, and a void can reference a claim you have never seen. Financial totals must derive from exactly one surviving version per claim. Design the resolution as either query-time or materialised, the handling of a late original, and how nightly remittance reconciliation surfaces an error before finance does.

Approach

  1. Resolve, never delete. Define the surviving version as a function over the chain — the highest claim_version for a claim_id, with a void making the chain contribute zero — and keep every superseded row. Deleting them makes the replay requirement, reprocessing any claim from the archive to the same result, impossible to satisfy.
  2. Choose materialised over query-time for the totals path and say why. DISTINCT ON (claim_id) ... ORDER BY claim_id, claim_version DESC is fine for one claim and unacceptable as the basis of every financial aggregate over millions of lines. Maintain a claim_current table with one row per claim_id naming the surviving version, written in the same transaction that loads a version, and build aggregates from that join.
  3. Treat the late original as ordinary traffic. A replacement for an unknown claim_id inserts and marks the chain incomplete; when the original lands, resolution recomputes and the affected aggregates are invalidated by claim_id. Rejecting the orphan loses a financial fact permanently, because nothing is going to resend it.
  4. Keep money in integer minor units from the wire to the report. Binary floating point cannot represent 0.10 exactly, so summing millions of lines accumulates representation error, and reconciliation is exact to the cent — the symptom is a balance off by a few cents with no attributable cause and days spent hunting a logic bug in code that is correct apart from its numeric type.
  5. Reconcile by construction, not by eyeball: nightly, sum paid amounts over surviving versions for the remittance's date range and compare to the remittance total; on mismatch, bisect by billing provider and then by claim so the alert names specific claim_ids. An alert reading 'off by 412.19' is not actionable; one naming four claims is.

Worked solution (30 min)

  1. Write the surviving-version function as SQL, then write the claim_current maintenance statement that runs on each version load.
  2. Construct a fixture where a replacement precedes its original and a void precedes both, and assert the totals after each arrival.
  3. Write the nightly reconciliation query and the bisect that turns a difference into a list of claim identifiers.
  4. Audit the path end to end for any float or double type touching a monetary value.

Expected result: A surviving-version rule kept as a function over retained rows, a maintained claim_current table backing aggregates instead of a per-query DISTINCT ON, late originals accepted and triggering recomputation with targeted invalidation, integer minor units throughout, and reconciliation that outputs offending claim identifiers rather than a bare difference.

Verify it yourself

  • A replaced claim contributes its replacement's amounts exactly once to every aggregate.
  • Loading original, replacement and void in all six orders yields the same final totals.
  • A voided claim contributes zero, and the superseded rows remain readable for replay.
  • No monetary value anywhere in the pipeline is held in a floating-point type.

Follow-up

  • A void arrives for a claim already paid and already reconciled in a closed period. What changes in the totals, and what does the next remittance say?
  • Two replacements for one claim arrive in the same batch carrying the same claim_version. How do you break the tie, and what becomes of the loser?

Duplicate patients created quietly by a matcher timeout fallback

Hard · partial outage · fallback policy · identity resolution

Over two days the manual duplicate-review queue grew eight-fold, and a clinician reports that a patient's prior results are missing from the chart. No error-rate alert fired: registration calls the identity resolution service with a 250ms timeout and, on timeout, creates a new enterprise person and returns 200. person_identity_link records match_score, link_status, decided_by and decided_at per link version. The identity service is operated by another team. Give the ordered investigation, what you can prove alone, and what you must ask them for.

Approach

  1. Work backwards from the damage rather than forwards from a dashboard. Take one duplicate pair, read both person_identity_link rows, and examine decided_by and match_score: a matcher version string, a user id, or the fallback's marker tells you which code path asserted the identity, and a NULL match_score means a deterministic link rather than a probabilistic one. If the duplicates were created by the fallback path, one query has given you the mechanism.
  2. Correlate decided_at for fallback-created links against your own client-side timeout counter, minute by minute. A creation rate that tracks the timeout rate with no lag is causal; one that does not sends you back to the first step rather than letting you build on a coincidence.
  3. Determine the failure shape. Is the timeout rate uniform across identity-service instances or concentrated on a few? Uniform means capacity or a global regression; concentrated means a partial outage — one degraded shard or replica — and the remedies differ. You can often infer this alone if responses carry an instance identifier; if they do not, that is the first thing to request.
  4. Name the real defect. The latency is the trigger; the incident is a fallback that converts unknown into new person. A 200 carrying a fabricated identity is worse than a 503, because it silently splits a chart, and the missing prior results are the direct consequence of that split rather than a separate problem.
  5. Stop the bleeding without blocking registration: on timeout, register under a provisional identity with link_status = potential_duplicate and route it to review. Clinical safety requires that registration proceeds; nothing requires that the system assert a new person on the strength of a timeout.
  6. Repair, then state the ask precisely. Because links are versioned and merges are reversible, remediation means re-running the matcher offline over the window's registrations and generating merge candidates — never rewriting downstream foreign keys. From the other team, request per-instance p99 and timeout counts for the window plus their deploy and index-maintenance log, and say which of your conclusions depends on that answer.

Follow-up

  • The missing results are attached to the losing identity. Describe the read path that surfaces them before the merge is approved, and what a clinician sees while the pair is still pending.
  • Your timeout is 250ms against a 200ms budget. What is the right value, and what happens to duplicate creation and to registration latency if you double it?

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.

The failure that was yours

Keep one story where the bad call was yours rather than a dependency's or a manager's. Name the check that would have caught it, whether you added that check afterwards, and whether it has fired since. Answers that route blame outward end the conversation early; answers that end in a guardrail someone still relies on tend to open it up.

Tell me about a time you worked with an unprepared or disorganized team on a project.

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. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

Walk me through your experience building software solutions listed on your resume.

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

Describe a situation where you had to adapt quickly to a changing project scope.

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  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

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