Interview guides · Apex Dental Marketing
Apex Dental Marketing Software Engineer Interview Questions & Guide 2026
Software engineer interview prep for Apex Dental Marketing: original coding, system design and debugging practice, each with a worked solution.
The 60-second brief
As a Software Engineer at Apex Dental Marketing, you are at the intersection of high-growth digital strategy and specialized healthcare technology. This role is critical to the company’s mission of empowering dental practices to reach more patients and scale their operations through sophisticated marketing automation and web solutions. You will be responsible for building, maintaining, and optimizing the platforms that drive client success, ensuring that every line of code directly contributes to improved practice visibility and patient engagement.
Most rounds test how you handle an under-specified problem more than what you recall. A wrong first approach you correct out loud is recoverable; a constraint you assumed silently and were never given is the expensive mistake.
Focus areas
- Model bitemporal coverage and retroactive eligibility changes
- Version clinical results instead of updating rows in place
- Hold accumulator limits under concurrent claim adjudication
Overview
You will likely work in a fast-paced environment that demands versatility. Because Apex Dental Marketing operates within a dynamic sector, you will be expected to "wear many hats," moving between full-stack development, system integrations, and performance optimization. This position offers the unique opportunity to see the immediate impact of your work on real-world business outcomes, making it a high-visibility role for engineers who thrive on tangible results and strategic influence.
The Apex Dental Marketing interview process
Apex Dental Marketing candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screening
Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.
What to demonstrate
- Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
- Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
- Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not
How to prepare
- Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
- Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
- If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window
Technical Assessments
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Project-Specific Questioning
You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Where candidates lose points
Treating a medical record number or a member ID as a globally unique key and joining on it directly.
These identifiers are unique only within the authority that issued them. Two facilities in one network routinely have the same medical record number for different people, and member IDs get reissued when someone changes plans. Joining on the bare value merges two patients' records, which is the most damaging failure available in this domain, and it passes every test written against a single-facility fixture because the collision only appears once a second source is connected.
Upserting on the order or result identifier, so a correction overwrites the original row.
It makes the question 'what did the clinician see at 14:02' unanswerable, which is exactly what an incident review or a legal hold asks. It also leaves downstream consumers that already acted on the preliminary value with no correction event to react to, because the state transition was collapsed into a single mutated row and never emitted.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
Questions & practice
Flag a requester reading too many distinct charts per window
Medium · sliding window · two pointers · distinct count
You consume access-audit records (event_ts, requester_id, enterprise_person_id, purpose_of_use, break_glass) at tens of thousands per second, non-decreasing in event_ts. For each requester, emit an alert the first time any sliding 600-second window contains reads of more than D distinct enterprise_person_ids. Break-glass reads count toward the window and are also reported separately. Return (requester_id, window_start_ts, distinct_count). Target O(1) amortised per event and memory proportional to the events resident in the window, not to the day.
Approach
- Per requester, hold a deque of (event_ts, person_id) and a hash map from person_id to its occurrence count inside the window, plus a running distinct counter. Push on the right; while the front is older than event_ts minus 600 seconds, pop it, decrement its count and erase the key when the count reaches zero, decrementing the distinct counter. Each event is pushed once and popped once, so the amortised cost is O(1) and the memory is O(W) for window occupancy W.
- Test the threshold immediately after each push and nowhere else. Between two consecutive events the window can only lose members as its left edge advances, so the maximum distinct count over all window positions is attained at a position whose right edge is an event. Checking at pushes is therefore exhaustive rather than a sampling approximation.
- Latch the alert per requester and re-arm only when the distinct count falls back below D, otherwise one busy stretch emits thousands of near-identical rows and the real signal is buried by its own volume.
- Bound memory both per requester and globally. A requester whose window legitimately holds tens of thousands of reads must not hold the process hostage, so cap the deque and degrade above the cap to an approximate distinct counter such as HyperLogLog, stating the error you accept in exchange.
- Count break-glass toward the window but carry it in its own output field. Break-glass has to succeed during an emergency, which is exactly why it must be the most visible path in the audit, and excluding it from the count would make the abuse route the quiet one.
- State the precondition: this is correct only while input is non-decreasing in event_ts. Out-of-order arrival needs a bounded-lateness buffer and a watermark, and dropping late events without a counter is the failure that hides itself.
Follow-up
- One region's events arrive up to 90 seconds late. What buffer do you add, and what does that do to alert latency?
- One person uses two requester accounts. What has to change in the key, and what new false positive does that introduce?
- How do you restore window state after a process restart without replaying the whole day?
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.
Worked solution (20 min)
- Define the per-claim record and the three-way comparison on claim_version: greater resets, equal accumulates, lower discards.
- Run the stream once, touching only the claim map.
- Walk the claim map, skipping entries whose surviving version is a void, and add each remaining sum into a person map.
- Return the person map sorted descending by total.
Expected result: Claim A: version 1 has lines of 12000 and 8000 cents, version 2 (replacement) has lines of 9000, 8000 and 500. Claim B: version 1 has a single line of 5000, version 2 is a void carrying one zero-amount line. Both claims belong to one person. Your answer is 17500 cents, from claim A's version 2 only. The naive sum over all seven lines is 42500.
Verify it yourself
- The number of claims contributing equals the number of distinct claim_ids whose highest version is not a void.
- For a multi-line claim, the addends in its sum equal that surviving version's line count, not one.
- Shuffling the input and re-running produces an identical result, which is the property the reset-on-higher-version rule buys you.
- The all-rows sum is greater than or equal to your total, equal only when no claim has a version above 1 and none is a void.
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?
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.
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?
Diagnose why an eligibility lookup ignores its index
Medium · indexing · query planning · partial indexes
A 40-million-row coverage_span serves eligibility lookups with: SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = $1 AND valid_to = 'infinity' AND effective_date <= $2 AND (termination_date IS NULL OR termination_date >= $2). An index exists on (effective_date). EXPLAIN (ANALYZE, BUFFERS) shows a sequential scan reading 1.8 million buffers at 2.4 seconds. Explain why that index cannot help, propose the index plus any schema change that makes the predicate index-friendly, and state what you expect the new plan and buffer count to be.
Approach
- Read the predicate for selectivity before touching the index. effective_date <= $2 matches most of a 40-million-row history, so a leading range scan on that column returns a large fraction of the table and the planner correctly prefers a sequential scan over millions of random heap fetches. The index is not being ignored; it is being rejected on cost.
- Put the equality column first. enterprise_person_id = $1 selects a handful of rows out of 40 million, so it must lead the composite index; a b-tree can only use columns after the first range predicate as filters, not as search bounds.
- Turn the open-ended termination into something indexable. The OR ... IS NULL branch forces either a BitmapOr or a post-index filter. Add coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED and rewrite the predicate to coverage_end >= $2; both COALESCE over a column and a date literal are immutable, so the generated column is legal.
- Make the index partial on WHERE valid_to = 'infinity'. Most rows in a bitemporal table are superseded beliefs, so the partial index is a fraction of the full one, stays in cache, and encodes the predicate for free rather than re-checking it per row.
- Consider the range alternative honestly: a daterange(effective_date, termination_date + 1, '[)') column with a GiST index and the && operator handles overlap queries and supports an EXCLUDE constraint against overlapping active coverage, but GiST lookups are slower than b-tree for this point-in-time pattern. Pick b-tree for the read path and keep GiST only if you also need the overlap constraint.
- Verify rather than assert: re-run EXPLAIN (ANALYZE, BUFFERS) and read Rows Removed by Filter and the shared hit/read split, not just the total time.
Follow-up
- After the change the plan is still a sequential scan for one particular member. What single query tells you whether that is stale statistics, a genuinely huge row count for that person, or parameter-dependent plan caching?
- Your generated column needs backfilling on a live table. Does adding a STORED generated column rewrite it, and what does that mean for your maintenance window?
- How would you serve the same lookup when the answer must be as of a past system-time instant rather than the current belief, given your index is partial on valid_to = 'infinity'?
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.
Worked solution (35 min)
- Open two psql sessions against a scratch database and seed one accumulator at deductible_applied_cents = 40000, plan_deductible_cents = 100000.
- Run the interleaving by hand with both writes absolute, and confirm the row ends at 70000 while the two lines between them charged the member 55000 of deductible — the serial answer is 95000, so 25000 of applied deductible has been lost.
- Run the control: repeat the identical interleaving with both UPDATEs written as SET deductible_applied_cents = deductible_applied_cents + :amt, and confirm the row ends at 95000. That form does not lose the update at READ COMMITTED, which is how you know the defect is the read-modify-write in application code rather than the isolation level by itself.
- Re-run the absolute version with FOR UPDATE on both SELECTs and confirm session two blocks, re-reads 65000, and writes 95000.
- Re-run both sessions at REPEATABLE READ and capture the exact error text and SQLSTATE from the losing session.
- Write the retry wrapper and show what makes the retry safe: a uniqueness key on (accumulator_key, claim_line_id) in an application ledger, so a re-run of an already-applied line is a no-op rather than a second application.
- Write the one-statement LEAST form with RETURNING and say where the member's share now comes from, given RETURNING carries no pre-image before PostgreSQL 18.
Expected result: Seed: deductible_applied_cents = 40000, plan_deductible_cents = 100000. Interleaving: T1 BEGIN; SELECT deductible_applied_cents -> 40000; application computes line A's deductible share as 25000 and the new total as 65000. T2 BEGIN; SELECT -> 40000, because T1 has not committed and READ COMMITTED shows only committed rows; application computes line B's share as 30000 and the new total as 70000. T1 UPDATE accumulator SET deductible_applied_cents = 65000 WHERE key...; COMMIT. T2 UPDATE accumulator SET deductible_applied_cents = 70000 WHERE key...; it blocks on T1's row lock, and on unblocking writes its absolute 70000 over the committed 65000; COMMIT. The row reads 70000 while the two lines charged the member 55000, so the serial answer is 95000 and T1's 25000 is lost; the accumulator understates by 25000 and the member is billed deductible again after the 100000 is already met. The write must be absolute for this to happen — SET deductible_applied_cents = deductible_applied_cents + :amt blocks, re-reads the committed row and applies both increments. Fixes: (1) SELECT ... FOR UPDATE on the read, no SQLSTATE to handle, serialises per member for the transaction's whole duration and therefore forbids external calls inside it; (2) REPEATABLE READ or SERIALIZABLE plus retry on SQLSTATE 40001 ('could not serialize access due to concurrent update' under REPEATABLE READ; SERIALIZABLE raises the same SQLSTATE for read/write dependencies), which needs an idempotency key because the whole transaction re-runs; (3) hash-partition claims by enterprise_person_id to one writer per partition, zero contention at the cost of head-of-line blocking and rebalancing.
Verify it yourself
- The unfixed script reproduces 70000 on a real database rather than on paper, and replaying the same two lines serially reproduces 95000.
- The control run with in-SQL relative increments, under the identical interleaving, ends at 95000 — that isolates the defect to the application-side read-modify-write and not to READ COMMITTED itself.
- With FOR UPDATE, session two's SELECT visibly blocks until session one commits, and the final value is exactly 95000.
- At REPEATABLE READ the losing session reports SQLSTATE 40001 with 'could not serialize access due to concurrent update', and its transaction is left in an aborted state requiring ROLLBACK.
- Applying the same claim_line_id twice through the retry path leaves the accumulator unchanged on the second attempt.
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?
If we have a banking-style application processing 5 million transactions daily, what multithreading design pattern would you choose and why?
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- 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 approach writing test cases for a standard login page?
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.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
Walk me through how you would troubleshoot a performance bottleneck in a production environment.
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- 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
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
How proficient are you in JavaScript and modern frameworks?
Medium
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Describe your recent project and your specific contributions to its architecture.
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Model an order placement so a lost response stays recoverable
Hard · idempotency · partial failure · reconciliation
An ordering system POSTs a medication or lab order to /orders. It assigns a placer identifier; the performing system later assigns a filler identifier, and neither side controls both namespaces. The caller times out after 3s and cannot distinguish an order that never arrived, one that was accepted with the acknowledgement lost, and one still in flight. Duplicating an order is a patient-safety event and silently abandoning one is worse. Design the contract so the caller can always determine the true outcome without guessing, and specify its behaviour at the timeout, on retry, and after repeated failure.
Approach
- Remove the ambiguity at its source by letting the caller name the resource before it exists, so the identity of the attempt never depends on our response arriving. The placer identifier is already caller-assigned and unique within the caller's namespace, so the server key is (placer_namespace, placer_order_id) under a unique constraint. A timeout stops being an unknown and becomes a question with a stable key.
- Prefer the shape that makes the retry trivially safe. PUT /orders/{placer_namespace}/{placer_order_id} is idempotent by HTTP semantics and needs no key header; POST /orders needs an Idempotency-Key to reach the same place. Under either, the write is one INSERT ... ON CONFLICT DO NOTHING, because a check-then-insert lets two retries through under READ COMMITTED. A repeat with an identical canonical body returns the existing resource; a repeat with a different body is 409 rather than a silent replace, since an order is not a document to overwrite.
- Give the caller a read that resolves a timeout without writing: GET on the same key returns accepted, routed, filled with its filler_order_id, or rejected with a reason, and 404 means we genuinely never saw it. Without that read, the caller's only instrument is another write, which is precisely the behaviour being designed out.
- Be honest about what acceptance means. Accept is durably persisted and queued for the performing system, not performed, so return 202 with the order resource and a status the caller polls or subscribes to. Returning 201 for an order that is not yet routed makes the caller believe a stronger fact than is true, and the filler identifier arrives later on the performing system's own timeline.
- Write the caller's behaviour explicitly, because the contract is only half the design: at timeout, GET the key; on 404, retry the write with the same key; on 5xx, exponential backoff with full jitter up to a bounded attempt count; after five failures, stop and raise a human-visible alert carrying the placer identifier. A queued-for-human state is a better outcome than either a duplicate order or a silent drop, and this domain cannot absorb the drop.
Worked solution (40 min)
- Draw the three timelines a 3s timeout can hide - request lost, work done and response lost, work still in flight - and mark what the caller can distinguish in each, with and without a caller-assigned key.
- Write the resource path, the unique constraint, and the single atomic statement that performs the write.
- Write the state machine the GET exposes, state what 404 means, and name the one state that must never be inferred from a timeout.
- Write the caller's pseudocode for timeout, retry, backoff and give-up, with the attempt count and the alert payload.
- Run two concurrent retries of the same placer identifier against a real database and confirm one order exists and both callers observe the same state.
Expected result: PUT /orders/{placer_namespace}/{placer_order_id} under UNIQUE (placer_namespace, placer_order_id), written by one INSERT ... ON CONFLICT DO NOTHING; a repeat with the same canonical body returns the existing resource, a repeat with a different body returns 409. GET on the same key returns {status in accepted | routed | filled | rejected, filler_order_id when known, reason when rejected}, with 404 meaning never seen. Acceptance is 202 - durably queued, not performed. Caller: on timeout GET the key; on 404 retry the same key; on 5xx back off with full jitter for five attempts, then stop and alert with the placer identifier rather than resubmitting.
Verify it yourself
- No outcome requires the caller to guess; every timeout resolves into a read.
- Two concurrent retries of one placer identifier leave exactly one order.
- A 202 is never returned for an order that is not durably persisted.
- The give-up path terminates in a human-visible state rather than a dropped order.
Follow-up
- The performing system returns a filler identifier for an order we have no record of. What do you do with it?
- The caller reissues an old placer identifier for a genuinely different order after a counter rollover. What breaks, and how would you detect it?
- Where does the record of the failed attempts live, and what must it carry for an incident review?
One facility stops updating while aggregate ingestion lag looks fine
Medium · poison message · head-of-line blocking · dead-letter queue
Encounters from one sending facility stopped updating at 02:10. Aggregate HL7 ingestion throughput looks normal and the consumer group's total lag is up only 4%. One consumer instance is pinned at 100% CPU. The error log shows the same message control ID repeating about 200 times a second with a parse error on a timestamp field. The stream is partitioned by (sending facility, encounter) to preserve per-encounter ordering. Give the ordered diagnosis, the fix, and what you must not do to the message.
Approach
- Look at lag per partition, never the aggregate. With 64 partitions, one fully stuck partition raises total lag by about 1.6% while that facility's encounters stop entirely, so the aggregate metric is structurally blind to head-of-line blocking. The alert that would have caught this is per-partition lag or a committed offset that has not advanced.
- Confirm the loop by grouping errors by offset or control ID. The same identifier repeating at the retry rate, with the partition's committed offset frozen, is a poison message. A growing set of distinct identifiers instead means a systematic parse regression, which points at a dialect change on the partner's side and has a different fix.
- Classify before acting: parse-time or apply-time failure. A parse failure has written nothing and can be parked safely. An apply failure may have committed part of its work, so you must confirm the write path is idempotent against a unique constraint on the idempotency key before you replay or skip anything.
- Fix the mechanism rather than the one message: bounded retries with exponential backoff and a maximum attempt count, then park the raw bytes, partition key and offset to a dead-letter store and advance the offset. Alert on dead-letter depth, because lag will not catch the next one either.
- Handle the ordering consequence explicitly. Silently skipping a message in a per-encounter ordered stream permanently drops a clinical event — a transfer, a discharge, a result release. Mark the encounter key as blocked and reconcile it, rather than letting later messages apply over a gap nobody recorded.
- Then repair the parser. Partial-precision HL7v2 timestamps are legal: 2026, 202609, 20260922 and 20260922143000-0700 are all valid, so the parser must carry a precision rather than assume fourteen digits plus an offset, and must reject with a typed error the dead-letter path can classify.
Follow-up
- What maximum attempt count do you choose, and what does it imply about a transient failure like a downstream store being briefly unavailable, which looks identical to a poison message for the first few attempts?
- After the parser is fixed, how does an operator replay parked messages for that encounter without re-applying the messages that already succeeded behind it?
Suggested practice checklist
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
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.
What do you look for in a team environment to be most productive?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
If you were assigned an unachievable delivery deadline, how would you handle it?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Tell me about a time you faced significant difficulties on a project; how did you overcome them?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?