Interview guides · Alliance Healthcare Deutschland
Alliance Healthcare Deutschland Software Engineer Interview Questions & Guide 2026
Timed coding practice for Alliance Healthcare Deutschland software engineer candidates: full attempts under a clock, then scored against a rubric.
The 60-second brief
As a Software Engineer at Alliance Healthcare Deutschland, you are at the heart of the digital transformation within one of Europe’s leading pharmaceutical wholesalers. This role is not merely about writing code; it is about building the robust, scalable, and secure infrastructure that ensures life-saving medications reach pharmacies and patients across the country. You will work on critical systems that underpin supply chain efficiency, logistics, and internal operations, directly impacting the availability of essential healthcare products.
The shape of the workload matters more for your prep than the industry label does. Read-heavy serving, write-heavy ingestion and scheduled batch processing have different binding constraints and fail in different places, so find out which one the team lives in before picking design topics.
Focus areas
- Make HL7 and X12 ingestion idempotent under replay
- Authorise every identified read against current consent
- Model bitemporal coverage and retroactive eligibility changes
Overview
The work environment at Alliance Healthcare Deutschland is fast-paced and highly professional. You will likely collaborate with cross-functional teams, including logistics experts, pharmacists, and IT operations, to solve complex technical challenges. Whether you are optimizing cloud architecture, managing network infrastructure, or leading cloud-based initiatives, your work will directly influence the company’s ability to remain agile in a highly regulated and demanding industry.
The roles at Alliance Healthcare Deutschland often emphasize deep technical specialization, such as cloud architecture and network engineering. Focus your preparation on demonstrating how your technical expertise solves real-world business problems.
The Alliance Healthcare Deutschland interview process
Alliance Healthcare Deutschland candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Screening Call
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 Assessments
What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Meet Technical Peers
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
Meet Leadership
An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
Where candidates lose points
Assuming admission, discharge and transfer messages arrive in the order the events happened.
Interface engines route by message type across separate queues and retry independently, so a discharge can land before the admission it closes and an update can land before the registration it modifies. Ordering has to come from the sender's event timestamp plus a per-encounter sequence, and the consumer has to apply out-of-order and late-arriving events correctly rather than rejecting them, because rejection turns a recoverable ordering issue into permanent data loss that nobody notices until a report is short.
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.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
Hardcoding to the sample inputs
Solve the stated problem rather than the two examples; special-casing a literal to make a sample pass is obvious immediately and reads as either a misunderstanding or an attempt to fake progress. If you genuinely cannot generalise yet, say which part is a stub and what would replace it.
Questions & practice
Sum surviving claim versions in one pass over unordered lines
Easy · hash map · versioning · streaming aggregation
You are streamed up to 50 million claim_line records in arbitrary order: claim_id, claim_version, line_number, frequency_code (original, replacement, void), enterprise_person_id, allowed_amount_cents. Every version of a claim shares its claim_id, and a replacement arrives as a higher claim_version. Return total allowed_amount_cents per enterprise_person_id, counting each claim once at its highest version and contributing zero when that version is a void. Amounts are non-negative int64 minor units. Target O(N) time and O(C) space for C distinct claims, single pass, no sort.
Approach
- Say the two grains out loud before writing anything: the version lives at claim grain, the money lives at line grain. Every wrong answer here comes from applying a claim-level rule with a line-level filter.
- Keep one hash map from claim_id to a small record (best_version, sum_cents, person_id, is_void). Per line: if claim_version is greater than best_version, reset sum_cents to this line's amount and overwrite the person and void flag; if equal, add to it; if lower, discard the line. That reset is what makes the pass order-independent, so a replacement arriving before its original still wins.
- Resolve the void at the end, not at ingest. A void only nullifies the claim if it is the surviving version, and zeroing on sight would let an earlier replacement that arrives later resurrect the money.
- Fold the C claim records into a person-keyed map in a second phase, O(C). Do not accumulate into the person total during the stream: you cannot subtract a superseded version you have already forgotten.
- Cost is O(N) time, O(C) space. The sort-based alternative, group by (claim_id, claim_version) and keep the max, is O(N log N) and needs the set resident; prefer it only when C approaches N and the map will not fit.
- Keep cents in int64 throughout. Fifty million lines at a realistic per-line ceiling stay four orders of magnitude below 2^63, so the integer type costs nothing and removes the float drift class entirely.
Follow-up
- You shard the stream across eight workers. Sharding by hash of claim_id works; what exactly breaks if you shard by enterprise_person_id instead?
- A replacement arrives for a claim_id whose original never appears in the batch. What does your map produce, and is that the financially correct answer or a reconciliation break?
- The same claim_id is reused by two different submitting organisations. How does the key have to change, and when would you have noticed?
Reconstruct what the chart displayed at five million past instants
Hard · external sort · as-of join · io bound
observation_result holds 200 million versions: observation_id, enterprise_person_id, filler_order_id, loinc_code, value_numeric, result_status, collected_ts, issued_ts, version, supersedes_observation_id. An incident review hands you 5 million queries of (enterprise_person_id, filler_order_id, loinc_code, as_of_instant). For each, return the version that was visible at that instant, meaning the one with the greatest issued_ts at or before it. Neither side fits in memory. The per-query scan is correct. Explain precisely why it is too slow, then give a plan with its complexity.
Approach
- Name the clock before naming an algorithm. Visibility is issued_ts, the release time. collected_ts is when the specimen was drawn and can precede release by hours, so ordering on it reports a correction as visible long before anyone could have seen it. No amount of index work rescues the wrong column.
- Be exact about why the naive plan fails, because the interviewer is testing whether you can tell arithmetic cost from I/O cost. Per query the chain scan is O(V_k) and the arithmetic is trivial, but 5 million independent lookups into a 200-million-row structure that does not fit in RAM is 5 million random reads. The job is bounded by seeks per query, not by comparisons, and buying a faster comparison changes nothing.
- Convert random access into sequential access. Hash-partition both sides on the chain key (enterprise_person_id, filler_order_id, loinc_code) into P shards sized to fit memory, sort each shard's versions by (chain key, issued_ts) and its queries by (chain key, as_of_instant), and sweep the pair in lockstep. Total O((V + Q) log(V + Q)) with external sort, replacing Q seeks with two sequential passes.
- Inside a chain the sweep is linear, not logarithmic, because queries are visited in ascending as_of order and the version pointer only moves forward: O(V_k + Q_k) per chain. Binary search per query is the better shape only when Q is small relative to V and the versions are already indexed and resident.
- Return the empty answer as a distinct outcome. A query whose as_of precedes the first issued_ts means nothing was displayed, which is not the same as the earliest value, and is frequently the exact fact the review is chasing.
- Return a version later marked entered_in_error if it was live at the instant asked about. Reconstructing the past means reporting what was on the screen, including what was wrong, and quietly substituting today's truth defeats the purpose of the exercise.
Follow-up
- Read replicas lagged 40 seconds at the time. Does your answer describe what the clinician actually saw, and how would you bound the difference?
- Serve the same question online for a single chart at a p99 under 50ms. What changes?
- One partner changed its filler_order_id format mid-year, so the chain key is not stable. How does that appear in your output, and how do you detect it rather than returning empty answers?
Group transfer-chained encounters into episodes with out-of-order arrival
Medium · graph traversal · cycle detection · memoisation
You have up to 5 million encounter rows for a reporting month, in arbitrary order: encounter_id, enterprise_person_id, facility_id, encounter_class, admit_ts, discharge_ts (nullable), status, prior_encounter_id (nullable, set on transfer), source_event_ts. prior_encounter_id may reference a row that arrives later or never arrives. Group rows into episodes, one per maximal transfer chain, returning episode root, person, earliest admit_ts, latest discharge_ts (null if any member is still open) and member count. Report cycles instead of following them. Target O(N) expected time.
Approach
- Recognise the shape: every row has at most one prior_encounter_id, so this is a forest of parent pointers, not a general graph. A hash map from encounter_id to row is the whole index you need, and the work is O(N) expected rather than anything sort-shaped.
- Resolve roots by iterative pointer-following with write-back memoisation: from each node walk prior_encounter_id until you reach a node with no parent, a parent absent from the input, or a node whose root is already known, then stamp the discovered root onto every node on that path. Each node is finalised once, so total work stays O(N) even when one chain is very long.
- Do not recurse. A malformed feed can produce a chain tens of thousands deep, and a recursive resolver dies on stack depth taking the whole batch with it. An explicit loop with write-back is both faster and bounded.
- Detect cycles with three-state marking: unvisited, on the current path, finalised. On reaching a node that is on the current path, emit a data-quality record naming every encounter_id in the cycle and exclude that component from the episode output. Choosing an arbitrary root instead would produce an episode that looks plausible and will be believed.
- Keep a dangling prior_encounter_id distinct from a genuine root. It means the chain starts outside the window or has not arrived, so flag the episode left-truncated and carry that flag into the output: an episode with an unknown start cannot be used for length of stay or readmission counting without biasing both downward.
- Compute aggregates during the same pass that assigns roots: minimum admit_ts, maximum discharge_ts with null propagation so any open member leaves the episode open, and exclude cancelled and entered_in_error members from the aggregates while keeping them retrievable.
Worked solution (25 min)
- Load all rows into a hash map keyed by encounter_id, with a colour array alongside.
- For each unfinalised node, walk parents onto an explicit path stack until you hit a root, a dangling reference, a finalised node or an on-path node.
- Stamp the resolved root, or the cycle marker, back onto every node on the path.
- Fold each node into its root's aggregate: min admit_ts, max discharge_ts with null propagation, member count, left-truncated flag.
- Emit episodes and the cycle records as two separate outputs.
Expected result: Rows arriving as E3 (prior E2), E1 (prior null), E2 (prior E1) form one episode rooted at E1 with three members, admit from E1 and discharge from E3. A row E9 whose prior E8 never arrives forms a one-member episode flagged left-truncated, and its admit_ts is not the episode's true admission. A pair E5 and E6 pointing at each other yields no episode and one cycle record naming both.
Verify it yourself
- Episode member counts plus cycle members sum to N exactly.
- Every episode has exactly one member whose prior_encounter_id is null or dangling.
- Shuffling the input produces identical episodes: same roots, same member counts, same aggregates.
- No episode has its earliest admit_ts after its latest discharge_ts. When one does, comparing facility_id on those two rows separates a wrongly-built chain from genuine clock skew between facilities.
Follow-up
- Two encounters name each other as prior_encounter_id. What does your detector emit, and who needs to see it?
- One episode spans two facilities whose clocks differ by four seconds. Do you order on source_event_ts or admit_ts, and what does the choice cost?
- A chain is cut in half by the month boundary. What does this month's report say, and how do you make next month's agree with it?
Return the displayable version of each analyte with a window function
Medium · window functions · versioned rows · indexing
observation_result is append-only: observation_id, enterprise_person_id, encounter_id, accession_id, placer_order_id, filler_order_id, loinc_code, value_numeric, unit_ucum, result_status in ('registered','preliminary','final','amended','corrected','entered_in_error'), collected_ts, issued_ts, version, supersedes_observation_id. A chart view needs the currently displayable value for each (accession_id, loinc_code) for one person over the last 90 days, excluding analytes whose newest version is entered_in_error but keeping analytes whose older versions were. Write the query, give the index that serves it, and say honestly which part of the plan the index cannot remove.
Approach
- Recognise the shape: this is a top-1-per-group over a version chain, not a filter. Pick the newest version first, then apply the status predicate to the winner — the order is the whole exercise.
- Use DISTINCT ON (accession_id, loinc_code) with ORDER BY accession_id, loinc_code, version DESC, observation_id DESC on PostgreSQL; the ORDER BY must start with the DISTINCT ON expressions or it is a syntax error. Use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY version DESC, observation_id DESC) with an outer WHERE rn = 1 if you need portability, since a window function cannot be referenced in the WHERE of its own query level.
- Push only person and time into the inner WHERE. Pushing result_status <> 'entered_in_error' inside promotes a retracted result's predecessor back onto the chart, which is the bug the prompt is testing for.
- Index (enterprise_person_id, collected_ts DESC) so the 90-day slice is an index range scan rather than a scan of the person's whole history.
- Be straight about the limit: because collected_ts carries a range predicate, no b-tree index can also deliver rows pre-ordered by (accession_id, loinc_code, version DESC), so the sort stays in the plan. That is acceptable because one person's 90-day slice is hundreds to low thousands of rows, and it is a quicksort in work_mem rather than an external merge — verify that rather than assume it.
Worked solution (30 min)
- Write the DISTINCT ON form with the full sort key and only person plus time in the inner WHERE.
- Wrap it in an outer query and apply the entered_in_error filter there, so it can only eliminate winners.
- Write the ROW_NUMBER equivalent and confirm both return identical rows against a fixture containing one preliminary, one final, one corrected and one retracted chain.
- Add the index, then read EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan feeding a Sort node, with Sort Method reported as quicksort and no Disk usage.
- Construct the adversarial fixture: an analyte whose version 1 is entered_in_error and whose version 2 is final. It must appear in the output; an analyte whose version 2 is entered_in_error must not.
Expected result: SELECT * FROM ( SELECT DISTINCT ON (o.accession_id, o.loinc_code) o.accession_id, o.loinc_code, o.value_numeric, o.unit_ucum, o.result_status, o.collected_ts, o.issued_ts FROM observation_result o WHERE o.enterprise_person_id = :p AND o.collected_ts >= now() - interval '90 days' ORDER BY o.accession_id, o.loinc_code, o.version DESC, o.observation_id DESC ) latest WHERE latest.result_status <> 'entered_in_error'; with CREATE INDEX ON observation_result (enterprise_person_id, collected_ts DESC). The plan is an index scan on that index feeding a Sort feeding Unique; the sort cannot be indexed away because collected_ts carries a range predicate.
Verify it yourself
- Output row count equals SELECT count(DISTINCT (accession_id, loinc_code)) over the same window minus the number of chains whose newest version is entered_in_error.
- Every returned observation_id has no other row pointing at it via supersedes_observation_id within the window.
- All versions of one analyte share the same collected_ts; if they do not, the 90-day window can cut a version chain in half and return a stale winner.
- EXPLAIN (ANALYZE) reports Sort Method: quicksort with Memory in kilobytes, not external merge Disk — if it spills, the per-person slice is larger than assumed and the design needs revisiting.
Follow-up
- A correction arrives carrying the same filler_order_id and the same version number as the row it corrects. What breaks, and what do you add to the sort key?
- The same query for a cohort of 50,000 people instead of one person: does DISTINCT ON still hold up, and at what point do you materialise a latest-version table with a partial index?
- How would you show the clinician that the displayed value was amended, given the chart query returns only the winner?
Reproduce and fix a lost update on a deductible accumulator
Medium · isolation levels · lost update · lock contention
An accumulator row holds deductible_applied_cents bigint and plan_deductible_cents bigint, keyed by (enterprise_person_id, plan_id, benefit_year). The adjudicator opens a transaction, SELECTs the row, computes the member's share in application code, then UPDATEs the row to the absolute new total it computed, all under PostgreSQL's default READ COMMITTED. Two claim lines for one member adjudicate concurrently: both charge the member deductible, but the accumulator advances by only one of the two amounts, so the member is billed deductible again on a later line after the plan deductible has already been met. Write the exact two-session interleaving that produces it. Then give three fixes, each naming the lock or isolation level, the SQLSTATE you must handle, and the throughput cost.
Approach
- State the anomaly precisely. READ COMMITTED takes a fresh snapshot per statement, so it prevents dirty reads but permits a lost update when a transaction reads a value, computes outside the database, and writes back an absolute result. The vulnerable window is the round trip through application code between two statements, not the transaction boundary — the same logic expressed as one relative UPDATE is safe at this very isolation level.
- Write the interleaving as an ordered script both sessions can be replayed from, with the commit points marked, and make both writes absolute (SET deductible_applied_cents = :computed_total). The point is not that two writes happen — it is that the second write stores a total computed from a value that had already been superseded by the time it landed.
- Fix one, SELECT ... FOR UPDATE on the read: the second session blocks on the row lock and, under READ COMMITTED, re-reads the newest committed version when it unblocks, so its computation starts from the winner's total. No serialization error to handle. Cost is serialised throughput per member and a lock held for the transaction's whole duration, so nothing inside may call an external payer.
- Fix two, REPEATABLE READ or SERIALIZABLE with a retry loop: in PostgreSQL, REPEATABLE READ aborts the second writer of a row with SQLSTATE 40001, 'could not serialize access due to concurrent update'; SERIALIZABLE additionally aborts on read/write dependencies detected by SSI, also under 40001. The retry re-runs the whole transaction, so the claim application must be idempotent or keyed by claim_line_id, or a retry double-applies.
- Fix three, single-writer partitioning: route by hash(enterprise_person_id) to one consumer per partition, so no two transactions ever touch one accumulator. No locks, no retries; the cost is head-of-line blocking behind a slow claim, rebalancing during deploys, and losing the ability to scale a hot member.
- Name the cheapest option and its real catch. A single UPDATE ... SET deductible_applied_cents = LEAST(plan_deductible_cents, deductible_applied_cents + :amt) ... RETURNING deductible_applied_cents does not lose the update: under READ COMMITTED an UPDATE that hits a concurrently updated row blocks, then re-evaluates its expressions against the newly committed version, so both increments land. The catch is that the member's share is the delta this statement actually applied, which is the returned value minus the row's pre-image — and RETURNING does not expose the pre-image before PostgreSQL 18's OLD/NEW aliases. On earlier versions you must read that pre-image under the same row lock, which puts you back in fix one with a shorter critical section. The one-statement form is a clean fix only when the caller does not need the delta.
Follow-up
- A claim is reversed six months later under a plan design that has since changed. What amount does the reversal subtract, and where is that number stored?
- Your retry loop hits 40001 repeatedly for one member during a batch window. What is the backoff, and at what point do you stop retrying and pend the claim?
- Which of the three fixes survives a retroactive eligibility change that invalidates everything applied in the last month, and what does the rebuild look like?
How do you approach the migration of legacy on-premise infrastructure to a cloud-native environment?
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
How do you balance cost-efficiency with performance requirements in cloud infrastructure?
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Choose a partition key and say what query it makes expensive.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
How do you ensure security compliance and data integrity when architecting cloud solutions?
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- State the consistency you need, and where you are willing to be stale.
- Choose a partition key and say what query it makes expensive.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Design an error taxonomy for a real-time eligibility check
Easy · error taxonomy · timeouts · graceful degradation
The registration desk calls GET /eligibility?person_id=&plan_id=&service_date= before a visit. The service answers from locally held coverage_span rows or by an external inquiry that takes seconds and sometimes fails outright. Staff must then proceed, proceed with an unverified flag, or stop, and the screen cannot make that call on its own. Define the response and error taxonomy: which outcomes are 2xx with a payload, which are 4xx, which are 5xx, what each carries, which are safe to retry, and what the desk is instructed to do for each. State the timeout budget.
Approach
- Separate the three things a boolean conflates: covered, not covered, and could not determine. Only the first two are answers. Flattening the third into 'not covered' produces a wrong financial answer wearing the clothes of a determinate one, and the desk cannot tell the difference.
- Put every determinate and indeterminate answer on 200 with a discriminated payload: {determination: covered | not_covered | indeterminate, reason_code, as_of, computed_at, source: local | payer | cache, coverage_order, cost_share, next_action}. The transport status describes the call; the payload describes the coverage. A not_covered answer is a successful call.
- Reserve 4xx for caller faults a retry cannot fix and make them distinguishable, because the remedy differs: 404 unknown person, 422 malformed or out-of-range service_date, 403 when the access decision refuses the purpose of use. Nothing retryable belongs in 4xx.
- Treat an external timeout as an outcome, not a transport failure. Return 200 with determination=indeterminate, reason_code=payer_timeout, and the locally derived coverage_span answer as of service_date alongside it. A 504 here throws away a usable degraded answer the desk could act on. Keep 5xx for the case where even the degraded answer could not be produced.
- Fix the budget end to end and state who enforces it: a roughly 2s desk interaction budget, a hard 1500ms deadline on the external inquiry, the remainder for the local path, and a client timeout set strictly above the server's so the server decides the fallback rather than the client abandoning a call that was about to degrade correctly. Note that every outcome here is retry-safe because the call is a read whose only side effect is the access audit row.
Worked solution (20 min)
- Write the full payload for a covered answer, including as_of, computed_at and the coverage_order that drives coordination of benefits.
- Enumerate every failure the call can hit - unknown person, bad date, consent refusal, no local data, payer timeout, payer error, our own fault - and assign each a status code and a reason code.
- For each row write the one sentence the desk sees and the action offered, then check that no two causes collapse into the same action.
- Do the deadline arithmetic: total budget, external deadline, remaining local budget, and the client timeout that must exceed the server's.
- State the retry rule per class: retryable, not retryable, or retryable with backoff only.
Expected result: 200 for covered, not_covered and indeterminate, each carrying {determination, reason_code, as_of, computed_at, source, next_action}; 404 unknown person; 422 malformed or out-of-range service_date; 403 consent or purpose-of-use refusal; 502 or 504 only when not even the degraded local answer could be produced; 500 for our own faults. A payer timeout is 200 + indeterminate + payer_timeout, carrying the local coverage_span answer as of service_date. External deadline 1500ms inside a 2s desk budget, with the client timeout set above it.
Verify it yourself
- No single field doubles as both the transport result and the coverage answer.
- Every 4xx names an input the caller can change, and nothing retryable is a 4xx.
- An indeterminate answer still carries an as_of date and whatever local coverage was readable.
- The client timeout is strictly greater than the server's external deadline, so degradation is the server's decision.
Follow-up
- Two payers answer and disagree about which is primary. What does the endpoint return?
- How long may an eligibility answer be cached, and what exactly is the cache key?
- The desk proceeded on an indeterminate answer and the claim denies six weeks later. What in the response makes that decision reconstructable?
Evening eligibility denials cluster in western time zones
Easy · timezones · bitemporal data · date boundaries
Eligibility denials rose from 0.4% to 3.1% of responses. They cluster after 17:00 local time, only at facilities in UTC-5 through UTC-10, and spike on the first and last day of each month. coverage_span.effective_date and termination_date are date columns in business time; the service derives the service date from encounter.admit_ts, a timestamptz. The enrolment files for the affected members look correct on inspection. Give the ordered diagnosis and the fix.
Approach
- Pivot the denial rate two ways before reading code: by facility UTC offset and by local hour of day. A defect that tracks local hour and offset is a time-conversion bug; one that tracks a source file, payer or plan is a data bug. The month-boundary spike is a consequence rather than a second problem, because terminations cluster on month ends and a one-day skew is most visible there.
- Take one denied request and recompute it by hand: the raw admit_ts, its UTC calendar date, the facility-local calendar date under the facility's IANA zone, and the coverage row's effective_date and termination_date. If the answer flips between the UTC date and the local date, the conversion is the cause and no further hypotheses are needed.
- Find the cast. In PostgreSQL, timestamptz::date is evaluated in the session's TimeZone setting, so the identical SQL returns different dates on different pooled connections and a pool that inherits UTC yields the UTC calendar date. date columns carry no zone at all, so comparing a date column to a UTC-derived date compares two different calendars.
- Fix once, at the edge: resolve the service date as (admit_ts AT TIME ZONE facility.iana_zone)::date and pass it down as an explicit date parameter. Use the IANA zone name, never a stored numeric offset, because the offset changes twice a year under daylight saving and a stored -8 is wrong for roughly eight months.
- Forbid re-derivation below the edge and pin the boundary with a frozen-clock test: an encounter at 23:30 local at a UTC-10 facility on the last day of a month whose coverage terminates that day, plus the same encounter one minute later. A test that calls now() passes for most of the day and hides the defect.
Follow-up
- Where else does the as-of date enter the system — the eligibility cache key, claim_line.service_from_date, the accumulator's plan year? Which of those are already computed in UTC?
- An inpatient encounter spans midnight local. Which date governs eligibility — the admit date or the service line date — and at which layer is that decided?
Suggested practice checklist
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Day 1 — Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Day 2 — Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Day 3 — Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Day 4 — Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Day 5 — The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Day 6 — Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Day 7 — Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Bounding a vague ticket before writing code
When the requirements were thin, the interesting part is how you fenced the problem off: the assumption you wrote down, who you got to confirm it, the narrow version you shipped first so the rest stayed cheap to change. Guessing and being right is luck. Guessing in writing, where someone could correct you, is method.
How do you manage competing priorities when working with multiple internal stakeholders?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- 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?
Tell us about a time you had to explain a complex technical issue to a non-technical manager.
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.
- 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?
Describe a time you had to lead a technical project through a period of significant change.
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
- What would you do differently if you ran that again?
- What did you decide not to do, and why?