cswork / libraryOpen practice workspace

Interview guides · A healthcare data

A healthcare data Machine Learning Engineer Interview Questions & Guide 2026

A healthcare data Machine Learning Engineer interview guide: reported rounds, clinical NLP and ML questions, bias and metrics prep, and a 7-day plan.

The 60-second brief

Candidates describe A healthcare data's work as turning clinical information into evidence, using predictive models and natural language processing. The reports do not name a specific product, customer or team, so prepare around clinical data, evidence generation and NLP rather than guessing at a platform. In the Machine Learning Engineer role, the reported responsibilities cover the ML lifecycle from data exploration and feature engineering through deployment and monitoring, working with data engineers and product managers.

This guide is for candidates interviewing for the Machine Learning Engineer role. It covers the two reported stages (an initial screening and technical rounds), the ML, NLP, pipeline and production-design questions candidates report, the behavioral prompts about explaining concepts and integrating data, and a seven-day plan. It does not cover Data Scientist or Software Engineer loops.

Candidates report two stages, an initial screening followed by technical rounds, over roughly 2-4 weeks. The questions reported skew toward applied ML, NLP, pipelines and cross-functional behavioral prompts, with healthcare data handling running through all of them.

Focus areas

  • Choose indexes from the query's access path
  • Paginate large result sets with keyset cursors
  • Make every write idempotent under retry

Overview

The reported work is machine learning on clinical data: the requirements candidates describe list an advanced quantitative degree, 3+ years of ML experience with clinical or healthcare data, advanced Python, and hands-on NLP with real-world evidence generation. Cloud-based ML infrastructure, familiarity with healthcare regulation such as HIPAA, and open-source or research contributions in health-tech appear as nice-to-haves. Candidates are advised to review their past projects, so the first job is knowing your past work well enough to defend every choice in it.

The reported questions fall into three groups. The first is modelling: trade-offs between model architectures for specific NLP tasks, handling missing or noisy data, validating a model for clinical decision support, and choosing metrics such as F1-score or AUC-ROC for a clinical outcome. The second is engineering: optimizing an ML pipeline for large clinical datasets, and keeping models scalable and maintainable in production. The third is behavioral: explaining a technical concept to a non-technical stakeholder, prioritizing between product and clinical teams, integrating disparate data sources, and building equity and ethics into model development.

Candidates are told to expect to implement solutions from scratch and to explain the limitations of their models and how they behave on edge cases. That shapes preparation: practise writing small, correct Python pipelines without leaning on a library call you cannot explain, and for every model you discuss, have a prepared account of where it fails, which patients or note types it serves worst, and what you would monitor after deployment.

Healthcare context is the thread that ties the loop together. Data privacy, clinical data structures and bias in health-related models are all reported as things to acknowledge in technical answers. Do not bolt this on at the end of an answer. Build it into the setup: say what the label is, who is in the cohort, how the split avoids putting one patient on both sides, and which subgroups you would report separately.

The A healthcare data interview process

Candidates describe two stages over roughly 2-4 weeks. Each description below combines what is reported about the stage with how to prepare for it; confirm the exact format with your recruiter.

Initial Screening

Candidates describe the Initial Screening as a preliminary phone conversation that checks your background and foundational skills before the technical stages. The reported guidance is to treat it as your chance to tell your story clearly. Prepare a two-minute narrative that connects your degree, your years of ML work, any clinical or healthcare data you have handled, your Python depth and your NLP experience, since those are the areas the role's requirements list. Pick one project to anchor it and be ready to name the data, the model, the metric you chose and one limitation. Keep the answer concrete: a model you shipped and a measurable outcome beats a list of libraries.

What to demonstrate

  • How well your background lines up with the requirements candidates describe: a quantitative degree, ML experience on clinical data, Python and NLP
  • Your foundational skills, as far as a preliminary conversation can show them
  • How clearly you can narrate your own project history

How to prepare

  • Write a two-minute summary of your path and one anchor project, covering data source, model, metric, deployment and one limitation you would still flag
  • Map each stated requirement (clinical data, NLP, real-world evidence, Python) to a specific piece of work you can describe in two sentences, and note honestly where you have a gap
  • Refresh the foundations of the stack the role lists: the Python data libraries you use, core ML concepts, and how you would explain a model choice in plain terms

Technical Rounds

Candidates describe the Technical Rounds as intensive assessments centred on practical application rather than theory. Reports say to expect implementation from scratch, discussion of the tools named in your application, and deep-dive questions about the limitations of your chosen models and how they behave on edge cases. A technical stage is where the ML, NLP, pipeline and production-design question types are practised, along with validating a model for clinical decision support. Approach it as problem solving out loud: define the objective, state constraints, pick an evaluation metric that fits the clinical outcome, then name where the approach breaks.

What to demonstrate

  • Working Python that you can write and explain without leaning on unexplained library calls
  • Whether your choice of model, pipeline design and metric is justified against alternatives
  • Whether you can state your model's limitations, edge cases and potential bias
  • How you handle healthcare data concerns such as privacy and clinical data structure in your answers

How to prepare

  • Implement from scratch in plain Python or numpy a split grouped by patient, a precision/recall/F1 calculation, and a simple logistic regression, then explain each line
  • Prepare a validation story for a clinical decision support model: cohort definition, patient-level split, temporal hold-out, calibration, subgroup metrics and a monitoring plan. Add your answer on why you chose precision, recall, F1-score or AUC-ROC, and how you handle missing or noisy data
  • Practise the pipeline and production questions on large clinical datasets: where time goes, what to cache or parallelise, how you version data and models, and how you detect drift
  • For every model you plan to discuss, write a limitations paragraph covering failure cases, imbalanced classes, missing data and the subgroups it may serve worse

Where candidates lose points

Reporting a single headline metric for a clinical outcome with no view of class balance or error cost

State the prevalence of the outcome and which mistake costs more, a missed case or a false alarm. Report precision, recall and F1-score at a named threshold alongside AUC-ROC, add a precision-recall view when positives are rare, and mention calibration if the output feeds a decision.

Splitting data by row, so records from the same patient land in both train and test

Split by patient, or by time when the model will be used prospectively, and say so in the first minute of any validation answer. Name the other leakage routes too: features recorded after the outcome, and note text that states the diagnosis.

Naming libraries and models without being able to say why they beat the alternatives

For each project, prepare the option you rejected and the reason: for an NLP task, say what a rules or linear baseline would have given, what a larger pretrained model adds, and what it costs in latency, maintenance and interpretability.

Treating bias and ethics as a closing sentence ('I would also check for bias')

Give a procedure: choose subgroups up front, report metrics per subgroup, check whether the label is a proxy for something other than health need, check how missingness differs across groups, and describe what you would do if a gap appeared.

Presenting a model with no limitations when asked about edge cases or failure modes

Write a limitations paragraph per model: inputs it has not seen, note styles or populations it was not trained on, thresholds that shift with prevalence, and the monitoring signal that would tell you it is degrading in production.

Questions & practice

Explain the trade-offs between different model architectures for specific NLP tasks.

Medium · machine learning fundamentals

Approach

  1. Start with the task, because it decides the architecture. Note-level classification (does this note mention a condition?), entity extraction (medications, diagnoses, doses), relation extraction and summarisation each favour a different model. Name the task and the label granularity before naming a model.
  2. Set a baseline first: TF-IDF features with logistic regression or a linear SVM. It trains in minutes, its coefficients are inspectable, and it is often competitive for document classification with enough labels. Unigram TF-IDF ignores word order; n-grams catch short negations like 'denies chest pain' but not longer-range cues such as 'no evidence of ... or ...', so add NegEx-style negation rules or move to a contextual encoder.
  3. Encoder transformers (BERT-style, ideally pretrained on biomedical or clinical text) give contextual token representations for classification and token-level NER with BIO tags. The trade-off is cost: self-attention is O(n^2) in sequence length, and BERT-base takes at most 512 tokens. Long clinical notes then need sliding-window chunking with aggregation, or a long-context encoder.
  4. Decoder LLMs can extract entities zero-shot or few-shot and can generate summaries. The costs are per-token inference cost and latency, outputs that may not match the source text and are hard to calibrate, and governance: protected health information may not be allowed to leave a controlled environment. Every extracted value should be checked against a span in the note.
  5. Choose with explicit criteria: labelled-data volume, document length, throughput and latency budget, interpretability needs and where the model must run. For NER, evaluate with entity-level F1 and say whether matching is strict (exact span) or lenient (overlap), because the two can differ a lot.

Follow-up

  • How would you handle notes far longer than the encoder's maximum input length without losing entities near chunk boundaries?
  • How would your chosen model handle negation and uncertainty cues such as 'no evidence of' or 'rule out'?
  • With a few hundred labelled notes, would you fine-tune an encoder or prompt an LLM, and how would you compare them fairly?

How do you incorporate ethical considerations and equity into your model development process?

Medium · machine learning fundamentals

Approach

  1. Start at framing, where most harm enters: check whether the label is a proxy that encodes unequal access. A published study of a care-management algorithm found that using healthcare cost as the target understated the needs of patients who used less care for the same level of illness. Say which outcome you would predict instead and why.
  2. Audit the data by subgroup (age, sex, race and ethnicity where recorded, payer, site): representation, label quality and missingness. In clinical data, missingness is often informative, because tests are ordered unevenly across groups, so a single global imputation can hide a disparity.
  3. Report metrics disaggregated by subgroup instead of one pooled AUC: sensitivity, PPV and calibration per group, with confidence intervals since some groups are small. When base rates differ, calibration within groups and equal false-positive and false-negative rates cannot all hold at once (outside degenerate cases), so pick the criterion from the harm of the decision.
  4. Removing the sensitive attribute is not a fix, because proxies such as zip code or insurance type carry the same signal. Real levers are reweighting or collecting more data for underrepresented groups, changing the label, and reviewing thresholds per group with clinical and legal input.
  5. Close with privacy and governance: de-identify or minimise protected health information under HIPAA rules, write a model card covering intended use, the groups evaluated and known limitations, keep a human in the loop for clinical decisions, and monitor subgroup performance after deployment.

Follow-up

  • How would you assess equity if the dataset has no race or ethnicity field, or it is mostly missing?
  • If equalising sensitivity across groups makes one group's risk scores poorly calibrated, which do you prioritise for this use case, and who decides?
  • Which fairness criterion would you choose for a model that ranks patients for limited care-management slots, and why?

How would you optimize a machine learning pipeline for large-scale clinical datasets?

Medium · machine learning fundamentals

Approach

  1. Profile before changing anything: measure wall time, peak memory and data volume per stage (extraction, joins, feature computation, training, inference) and fix the stage that dominates. Rewriting the model is wasted effort if a row-by-row pandas apply in feature building takes most of the run.
  2. Data layer: store intermediate tables in a columnar format such as Parquet, partitioned by date or site, so reads get column pruning and predicate pushdown. Replace Python loops with vectorised operations and cast to smaller dtypes (float32, categoricals). Move to Spark, Dask or Polars only when the data no longer fits on one machine, and process new encounters incrementally.
  3. Keep it correct while making it fast. Compute features point-in-time, using only data recorded before the prediction time. Split train and test by patient, not by encounter, so one patient's visits never sit on both sides. Validate on a later time window to mimic deployment. A faster pipeline that leaks is worse than a slow one.
  4. Training: histogram-based gradient boosting (LightGBM, or XGBoost with tree_method='hist') bins features, so split finding scales with the number of bins rather than distinct values. Use early stopping on a validation set and successive halving or Hyperband for tuning instead of a full grid. Cache artefacts keyed by data version and code hash so unchanged stages are skipped.
  5. Operations: run stages under an orchestrator with idempotent, retryable tasks. Add data-validation checks at stage boundaries (schema, value ranges, null rates, row counts per site). De-identify early so most of the pipeline runs on data with less protected health information.

Follow-up

  • How would you show that an optimised pipeline produces the same features and predictions as the old one?
  • If the dataset grows tenfold, which stage breaks first, and what would you change?
  • A patient has hundreds of encounters. How do you build training examples without leakage or letting heavy users dominate the loss?

Merge partitioned event streams into one ordered feed with bounded lateness

Hard · k-way merge · watermarks · out-of-order streams

The read-model service consumes 64 log partitions carrying about 4,000 events per second in total. Each partition is ordered within itself, but partitions drift by up to 30 seconds, and the activity feed must present a tenant's events in occurred_at order. Produce the merge. State its complexity, the buffer it requires in events and in bytes, what happens when one partition is idle, and what you do with an event that arrives after you have already emitted its position. Payloads average 1 KB.

Approach

  1. Merge with a min-heap over the 64 partition heads keyed on (occurred_at, event_id): O(log P) per event and O(n log P) overall. The tie-break on event_id is what makes the output deterministic when two partitions carry the same millisecond, which matters because the feed is paginated and a non-deterministic order reorders pages under the reader.
  2. Emitting the heap head is only correct once every partition has produced everything up to that timestamp, so the emit condition is a watermark: the minimum across partitions of the highest occurred_at seen, less the allowed lateness. Events are held until the watermark passes them, which is what turns individually ordered streams into a jointly ordered one.
  3. Size the buffer from the lateness rather than guessing: 4,000 events per second times 30 seconds is 120,000 buffered events, and at 1 KB each about 120 MB of heap. That number is the real price of the ordering guarantee and belongs in front of whoever asked for it.
  4. Handle the idle partition explicitly, because it fails the feed rather than corrupting it: a partition with no traffic never advances its own maximum, so the watermark freezes and output stops entirely. Either every partition emits a periodic idle marker carrying the broker's current time, or the watermark falls back to wall clock for a partition silent beyond a threshold.
  5. Choose the late-event policy from what the projection is keyed on. The projection upserts on (aggregate_id, aggregate_version) and discards a version it has already applied, so a late event is safe to apply out of order and correctness never depended on the merge at all. Apply it, recompute the affected feed page, and count lateness so the 30-second budget can be re-derived from data rather than folklore.
  6. Say what the merge does not buy: ordering is guaranteed within one aggregate by the log's partitioning, and no watermark makes the cross-aggregate order authoritative. Two events from different aggregates in the same millisecond have no true order, so the feed's order is a presentation choice that must be stable rather than correct.

Worked solution (35 min)

  1. Write the heap comparator on (occurred_at, event_id) and the per-partition head refill.
  2. Write the watermark computation and the emit-loop condition, then list which buffered events are held at a chosen instant.
  3. Compute the buffer at 4,000 events per second, 30 seconds and 1 KB per event, and state what fraction of a worker's heap that represents.
  4. Add the idle-partition marker and trace the watermark with one silent partition, both with and without the marker.
  5. Write the late-event path and name the key that makes applying it safe.

Expected result: A 64-way min-heap merge at O(n log P) with a deterministic (occurred_at, event_id) comparator, a watermark of the per-partition minimum less 30 seconds gating emission, a stated buffer of 120,000 events and roughly 120 MB, idle markers so a silent partition cannot freeze the watermark, and a late-event policy justified by the projection's idempotency on (aggregate_id, aggregate_version).

Verify it yourself

  • Feeding the partitions in an adversarial interleaving produces the same output sequence as feeding them in timestamp order.
  • Silencing one partition stalls output without the idle marker and does not stall it with the marker.
  • Applying a late event twice leaves the projection byte-identical, which is what permits the out-of-order path to exist.

Follow-up

  • The lateness budget is raised to five minutes. What is the new buffer, and what besides memory changes?
  • The consumer restarts. Where does it resume from, and what does the feed look like for the first 30 seconds?
  • One partition is ten minutes behind because its producer is slow. Do you stall the feed or emit without it?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.
  5. Interpret rather than report: no gaps plus a normal p95 of published_at - created_at points at the consumer; gaps or a fat lag tail point at the relay; rows still 'pending' with attempts > 0 point at neither, because they never left the database.
  6. Be explicit that the partial index on (created_at, event_id) WHERE status = 'pending' does not serve any of these — they read published rows. Name the index a recurring monitor would need, and say why a query run twice a year may not deserve one.

Worked solution (30 min)

  1. Write the three queries against seven days of data and confirm each returns without error.
  2. In a scratch copy, delete one middle event for a single aggregate and confirm the gap query names that aggregate and the versions either side.
  3. Run a running total over ungrouped rows ordered by date_trunc('second', created_at), once with the default frame and once with ROWS, and record where the two series diverge.
  4. Compare the DISTINCT ON and row_number() plans on the same data and record rows-read for each.

Expected result: The gap query returns one row per hole with the version on each side; the daily query returns seven rows whose running total ends at the seven-day count; the newest-per-aggregate query returns exactly one row per aggregate_id present in the window. The RANGE running total is higher than the ROWS one at every tied second.

Verify it yourself

  • The per-day counts sum to COUNT(*) over the same predicate.
  • Re-running the gap query after reinserting the deleted event returns zero rows.
  • count(*) of the newest-per-aggregate result equals count(DISTINCT aggregate_id) over the same filter.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?
  • The consumer claims it never received event 4,812,006. What do you look at, in what order?

Write the update path that detects a concurrent edit

Medium · optimistic concurrency · transactional outbox · audit history

resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.

Approach

  1. One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
  2. Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
  3. State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
  4. Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
  5. Refuse to auto-retry the whole PUT. A retry re-reads the winner's state and reapplies an intent formed against data that no longer exists — the silent overwrite the version token was added to detect. Return the conflict; merge field-wise only if the patches are provably disjoint.
  6. Note that now() is the transaction timestamp in PostgreSQL, so resource.updated_at, the revision's created_at and the outbox row share one instant, which is what later makes reconciliation between the three tables unambiguous.

Follow-up

  • A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
  • Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
  • Every write now touches a second hot table. How do you keep the outbox insert and its partial index from becoming the write bottleneck at 1.2k writes/second?

How do you ensure your models are scalable and maintainable in a production environment?

Medium · system design

Approach

  1. Choose the serving mode from the use case. Batch scoring (for example, nightly risk scores written to a table) scales by partitioning the job and is easy to audit. Online inference sits behind an API with a latency budget and scales horizontally with stateless model servers behind a load balancer. Say which one the clinical workflow actually needs.
  2. Prevent training-serving skew: compute each feature from one shared definition used both offline and online, through a feature store or a shared library. Pin library versions and package the model with its preprocessing in one container so the transform at inference matches training exactly.
  3. Make every model reproducible and reversible. A model registry records the data snapshot, code commit, hyperparameters and evaluation metrics. A new version goes out in shadow mode or as a canary before full traffic, and rollback means repointing to the previous registered version.
  4. Monitor on three layers: inputs (null rates, schema changes, drift per feature through PSI or a KS test), outputs (the distribution of predicted scores), and performance once delayed outcome labels arrive, broken down by subgroup. Alert on thresholds and tie retraining to those signals, not only to a calendar.
  5. Maintainability comes from tests and records. Unit-test the feature code, run data-validation checks in CI, and keep a fixed golden set whose predictions must not change unexpectedly. Log each prediction with its model version and inputs so any clinical decision can be traced back to the exact model that made it.

Follow-up

  • Outcome labels arrive months after prediction. How do you detect performance decay before then?
  • A new model beats the current one offline. What is your rollout plan, and what result would make you roll back?
  • An upstream source changes a coding system or a unit of measure. Where in your design is that caught, and what happens to the predictions in the meantime?

Make the resource write endpoint safe for retrying clients

Easy · idempotency · unique constraints · retries

The edge API accepts POST /v1/resources at 1.2k writes/second peak under a 400 ms p99 budget, and clients retry on timeout. idempotency_key has PRIMARY KEY (tenant_id, idempotency_key) plus request_fingerprint CHAR(64), state in ('in_flight','succeeded','failed'), response_status, response_body, resource_id, locked_until and expires_at. Specify the exact sequence the handler runs: what the second request does when it arrives while the first is still executing, what a retry carrying a different body receives, what happens when a process dies mid-request, and how the table is kept from growing without bound.

Approach

  1. Start from the race rather than the happy path. Insert the key row first, in its own short transaction, with state 'in_flight' and locked_until set to now() plus the request deadline. The unique constraint on (tenant_id, idempotency_key) is the arbitration mechanism: exactly one caller commits the insert, and every other caller takes a unique violation (SQLSTATE 23505) and becomes a follower. Reading the table and then inserting is a check-then-act race that both callers pass, and they pass it most often under the load that generates the retries.
  2. Give the follower a branch for every state it can observe, with no fall-through to doing the work: fingerprint mismatch means the key is being reused for a different request, so reject with 422 and never replay the stored response; 'succeeded' replays response_status and response_body verbatim; 'failed' allows a fresh attempt; 'in_flight' with locked_until in the future is answered with 409 and a retry hint, or a short bounded wait, because the only alternative is executing the effect twice.
  3. Commit the effect and the record of the effect together. The resource insert, its resource_revision row, its outbox_event row and the transition of the key row to 'succeeded' all happen in one transaction, so no crash can leave the work done and the key still 'in_flight'. The earlier insert is deliberately a separate transaction: it has to be visible to a concurrent caller before the work starts, which an uncommitted row is not.
  4. Close the wedged-key path. A process that dies after the insert leaves a row that would otherwise block that key forever, which is what locked_until exists for. Reclaim with a single conditional statement, UPDATE ... SET state='in_flight', locked_until=now()+interval WHERE ... AND state='in_flight' AND locked_until < now(), and treat a zero rowcount as losing the reclaim. Two statements that read and then update reintroduce the original race one layer down.
  5. Size the retention before it becomes an incident. At the 1.2k/second peak a 24-hour window is on the order of 100 million rows, and the table grows with traffic rather than with data, so the sweep is load-bearing. Batched deletes driven by an index on expires_at keep it bounded. Range-partitioning by day and dropping whole partitions is cheaper, but PostgreSQL requires every partition-key column to appear in the primary key, so the key becomes (tenant_id, idempotency_key, created_at) and a retry that straddles the boundary no longer collides with its original. State that cost rather than meeting it later.

Worked solution (20 min)

  1. Draw a timeline with the retry arriving 150 ms into a 900 ms first attempt, and mark the instant each row becomes visible to the other transaction.
  2. Write the insert-first statement, then a five-row branch table for the follower: fingerprint mismatch, 'succeeded', 'failed', live 'in_flight', expired 'in_flight' - one action each.
  3. Write the single conditional UPDATE that reclaims an expired in_flight row, and argue why two concurrent reclaimers cannot both see a non-zero rowcount.
  4. Compute rows per day at peak, name the index the sweep uses, and check the sweep rate against the insert rate.

Expected result: The key row is inserted at 'in_flight' in its own transaction before any work; the unique violation identifies the follower. The follower replays the stored response on 'succeeded', returns 422 on a fingerprint mismatch, and waits or returns 409 on a live 'in_flight', never performing the work itself. The effect, its revision row, its outbox row and the transition to 'succeeded' commit in one transaction. Expired in_flight rows are reclaimed by one conditional UPDATE, and expired keys are swept in batches.

Verify it yourself

  • The retry creates no second resource row, whether it arrives during the first attempt or after it.
  • A retry with the same key and a changed body is rejected rather than served the first attempt's response.
  • Killing the process between the key insert and the commit leaves a key that is reclaimable within locked_until rather than one that is stuck.
  • For create operations, every key row in state 'succeeded' carries a resource_id that exists.

Follow-up

  • The stored response body averages 200 KB and this table is now the largest in the database. What do you store instead, and what does a replay return once the body has been pruned?
  • A client library generates a fresh idempotency key on every attempt. What breaks, which layer should have caught it, and does the server have any defence?
  • The first attempt succeeded but its response was lost, and the client retries 30 hours later, after the key expired. What does the second attempt do, and is that acceptable?

Listing latency scales with page size, not with filters

Easy · n+1 · latency · query batching

The tenant listing endpoint reads resource filtered by tenant_id and status, ordered by updated_at DESC, and returns each row plus the owner's display name from app_user and the actor of that resource's latest resource_revision. p99 is 55 ms at 10 rows per page and 1.4 s at 200. Database telemetry shows 401 statements per request, each under 1 ms, and nothing in the slow-query log. Diagnose the cause and give the fix, stating the statement count per request and the p99 you expect afterwards.

Approach

  1. Read the counters before forming a theory. 401 statements for 200 rows is one driver query plus two per row, and sub-millisecond execution with an empty slow-query log rules out a bad plan. The time is round trips, which is why it is invisible in every per-query metric and scales with rows returned rather than with filter selectivity.
  2. Name the two per-row statements from their normalised text: a single-row app_user lookup by user_id, and a resource_revision lookup by resource_id ordered by version DESC LIMIT 1. Confirm by dropping those two response fields and watching the statement count fall to one. That locates the calls in the serialisation layer, not the repository.
  3. Check that the arithmetic accounts for the whole gap. Measure one round trip to the replica in isolation; 400 trips at roughly 3 ms of network plus 0.2 ms of execution is about 1.3 s on top of a 55 ms baseline, which matches. If the multiplication had fallen short, the N+1 would only be part of the story and you would keep looking.
  4. Batch both lookups. Collect owner_user_ids and resource_ids from the driver query, then issue WHERE tenant_id = $1 AND user_id = ANY($2) for the users, and PostgreSQL's SELECT DISTINCT ON (resource_id) ... WHERE resource_id = ANY($2) ORDER BY resource_id, version DESC for the latest revision, which the UNIQUE (resource_id, version) index serves directly. On an engine without DISTINCT ON, use a lateral join or a row_number window. Three statements per request at any page size.
  5. Keep the tenant predicate in the batched query. The per-row version was implicitly scoped because its ids came from tenant-scoped rows; a batched user_id = ANY(...) with no tenant_id is an unscoped read that behaves correctly only as long as the id list is trustworthy.
  6. Re-measure at 10, 50 and 200 rows and confirm the statement count is constant. Latency should now track bytes returned.

Follow-up

  • The page size is capped at 200 today. What breaks first if it is raised to 2,000, and is it still this bug?
  • How do you stop the next N+1 from reaching production, given that no individual query is slow and the endpoint's tests pass?
  • The latest-revision actor is only used to render an avatar. Make the case for denormalising it onto resource, and name the write anomaly that introduces.

Suggested practice checklist

The week follows the loop: a screening narrative first, then the Python, NLP, validation and pipeline material the technical rounds are reported to cover, then the behavioral prompts, then a full mock. Each day produces something you can show or say out loud.

Day 1 — Project inventory and screening narrative

  • List your three strongest ML projects and, for each, write the data source, label, model, metric, deployment path and one limitation you would still flag
  • Map each stated requirement (clinical or healthcare data, NLP, real-world evidence, Python) to one of those projects, and note any gaps you will address honestly
  • Write and time a two-minute career narrative that ends on your anchor project

Deliverable: A one-page project inventory covering three projects plus a two-minute screening narrative you can say aloud without notes.

Day 2 — Python from scratch, libraries and pipeline performance

  • Write from scratch, without a helper library, a patient-grouped train/validation split, then confirm no patient appears on both sides
  • Write precision, recall and F1-score by hand from a confusion matrix, then verify against a library implementation
  • For pandas, scikit-learn and one deep-learning or NLP library you have used, write one concrete use, one alternative you considered and one pitfall you hit, such as chained assignment in pandas or fitting a scaler before the split
  • Take a data-prep step over a large table and measure it: profile where time goes, then try chunked reading, vectorising a loop, and caching an expensive step, recording each speedup

Deliverable: A short Python module with the grouped split and metric functions, a use/alternative/pitfall note for each library, and a before-and-after timing table for one pipeline step.

Day 3 — Model trade-offs for clinical NLP

  • Choose one clinical-text task, such as extracting a condition mention, and write a comparison of a rules or keyword baseline, a TF-IDF with linear model, and a pretrained transformer, covering data needed, latency, interpretability and maintenance
  • Write the failure cases specific to clinical notes: negated findings, abbreviations, copied-forward text and section structure, and say how each would show up in an error analysis
  • Build the baseline and score it on a small labelled sample you create, so you have a real number to compare against

Deliverable: A one-page trade-off table for one NLP task, with a baseline score and a list of five error categories.

Day 4 — Missing data, metrics and clinical validation

  • Write your answer to handling missing or noisy data in a clinical setting: name the three mechanisms (MCAR, MAR, MNAR), then note that in clinical data whether a test was ordered often carries signal about the patient, which is the case for a missingness indicator alongside imputation whichever mechanism applies
  • Take an imbalanced synthetic dataset, compare F1-score, AUC-ROC and a precision-recall curve, and write which one you would present for a rare outcome and why
  • Outline how you would validate a model for clinical decision support: cohort definition, patient-level and temporal splits, calibration, subgroup results and a monitoring plan

Deliverable: A written validation plan for a clinical decision support model plus a notebook comparing three metrics on imbalanced data.

Day 5 — Production ML, privacy and equity

  • Sketch the production lifecycle for one of your models: data validation at ingestion, versioned training data and models, deployment, drift and performance monitoring, and a rollback path. Map each stage to the cloud services you have actually used for training jobs, the model registry, serving and monitoring, and note where PHI is stored and who can access it
  • Prepare an answer on keeping models scalable and maintainable: what you test, how you retrain, what you log, and how a data engineer or product manager would run it without you
  • Write a per-model equity checklist: subgroup metrics, label proxy check, missingness by group, and what you would do if a gap appeared
  • Prepare the privacy points to raise unprompted: the two HIPAA de-identification routes (Safe Harbor removal of the 18 identifier types, or Expert Determination), the minimum-necessary principle, why dates and free-text notes still carry re-identification risk, and logging of who accessed which data

Deliverable: A production lifecycle diagram with monitoring signals and cloud services mapped, a one-page equity checklist you can apply to any model, and a short privacy talking-points sheet.

Day 6 — Behavioral stories for cross-functional work

  • Write STAR outlines for the stakeholder-explanation story and the data-integration story, each with a quantified result and a stated regret
  • Write a story for prioritizing between product and clinical demands, and one for improving a model that was performing poorly in production
  • Practise the estimation exercise in the practice library to rehearse defending a range with explicit assumptions, then say each outline aloud and cut anything that is not a decision, a number or a lesson

Deliverable: Four STAR outlines, each fitting under two minutes spoken, with the result and the lesson stated explicitly, plus one estimate with its range and stated assumptions.

Day 7 — Full mock loop

  • Run a spoken mock of the screening: your narrative, then three follow-up questions about the anchor project
  • Answer the four technical questions aloud: NLP architecture trade-offs, equity in model development, pipeline optimization on large clinical datasets, and production maintainability
  • For each answer, finish with the limitations and edge cases of the approach you chose, since candidates are told to expect that deep dive
  • Record the session, mark every place you made a claim without a reason, and rewrite those answers

Deliverable: A recorded mock with a marked-up list of unsupported claims and rewritten answers for each.

Cross-functional stories for a clinical ML role

The reported behavioral questions are about working across disciplines on clinical data: explaining technical ideas to non-technical colleagues, prioritizing between product and clinical requests, combining sources that do not line up, and building equity into modelling. Choose stories where you made the calls, include a number or a decision in each, and be ready to state what you would change.

  1. How do you prioritize projects when you have competing demands from product and clinical teams?
  2. How do you incorporate ethical considerations and equity into your model development process?
  3. Describe a time you optimized a model that was performing poorly in production.
  4. Describe a time you had to tell a stakeholder that a model's limitations made it unsuitable for the use they wanted.

Tell me about a time you had to explain a complex technical concept to a non-technical stakeholder.

Medium · behavioural and collaboration

Approach

  1. Choose a story in which the stakeholder had to make a decision that depended on your explanation, such as whether to trust a model's output, accept a trade-off between missed cases and false alarms, or fund more data work. A story where you simply gave a talk has no decision to anchor the result.
  2. Name the audience and what they already knew. In a clinical or product setting, translate model terms into their frame: instead of AUC-ROC, say how many true cases the model would catch and how many false alerts it would raise per hundred patients at the chosen threshold.
  3. Show the translation technique: one concrete example, one picture or table, and one analogy at most. Then say how you checked understanding, for instance by asking them to restate the trade-off or by seeing whether they could predict what happens if the threshold moves.
  4. Say what you deliberately left out and why that omission did not change the decision, and keep the numbers honest: state uncertainty and limitations rather than rounding them away to be convincing.
  5. Close with the result in STAR form: the decision made, what changed afterwards, and one thing you would explain differently. The mistake that sinks this answer is a story where the listener was another engineer, or one that ends with 'they understood' and no outcome.

Follow-up

  • How did you know they had understood, rather than just nodded?
  • What did you simplify, and what would have been dangerous to simplify further?
  • What did you do when they pushed back or asked for a guarantee the model could not give?

Describe a challenging project where you had to integrate disparate data sources.

Medium · behavioural and collaboration

Approach

  1. Open with the situation in two sentences: which sources, why they had to be combined, and what the combined data was for. In a health setting good examples include structured records alongside free-text notes, or data from different systems with different identifiers and coding schemes.
  2. Describe the specific obstacles you found rather than the tools: mismatched patient or record identifiers, different units or code systems, timestamps in different time zones or granularity, duplicate records and missingness that differed by source. Say how you resolved each, for instance a deterministic key first and fuzzy matching only for the remainder, with the match rules written down.
  3. Show how you validated the integration: row counts before and after each join, match rates, spot checks against source records, and a check that unmatched records were not systematically different from matched ones. Mention leakage control, such as making sure no field recorded after the outcome entered the feature set.
  4. Cover privacy and access: what identifying fields you removed or restricted and who could see the joined data. Quantify the result, such as coverage gained or the downstream metric change, and name the person or team you coordinated with, for example data engineers who owned a source.
  5. End with what you would do differently, concretely, such as adding schema contracts or automated reconciliation checks earlier. The mistake that sinks it is a tour of tools with no account of how you knew the merge was correct.

Follow-up

  • How did you verify that the records you linked really belonged to the same person or event?
  • What happened to records that did not match, and could dropping them have biased the dataset?
  • How would you automate this so it still works when a source changes its schema?

Estimate work you have never done and defend the range

Hard · estimation · backfills · expand-contract

You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.

Approach

  1. Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
  2. Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
  3. Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
  4. Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
  5. Commit to a checkpoint rather than a completion date: the day you report a measured number from that first batch. That is a promise you can keep under uncertainty, and it is what the asker actually needs in order to plan.

Follow-up

  • How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
  • Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?
  • Your first batch comes back ten times slower than assumed. What do you tell the person waiting on the estimate, and when?