cswork / libraryOpen practice workspace

Interview guides · iLoF

iLoF Software Engineer Interview Guide

iLoF Software Engineer interview guide: reported rounds, questions and how candidates describe the process.

The 60-second brief

As a Software Engineer at iLoF, you are at the intersection of cutting-edge biotechnology and advanced data science. Your work directly contributes to our mission of accelerating drug discovery and clinical trials by leveraging our proprietary platform to map the complex world of biology. You are not just writing code; you are building the digital infrastructure that enables us to analyze massive, high-dimensional datasets with speed and precision. The role demands a balance of technical rigor and a passion for solving high-impact, real-world health challenges. You will collaborate with multi-disciplinary teams—ranging from biologists to data scientists—to transform complex research requirements into scalable, robust software solutions.

This guide is scoped to a Software Engineer candidate at iLoF.

Focus areas

  • Technical Interviewing (Past Experience Deep-Dive)
  • Interview Communication
  • HR Interview Preparation

Where to focus your preparation

No round sequence has been reported for iLoF. Confirm the format with your recruiter.

Preparation focus

No round sequence has been reported for this company, so confirm the format with your recruiter and work the reported questions below.

What to demonstrate

  • Breadth across the topics this company reports testing
  • Whether you confirm the format before preparing for it

How to prepare

  • Ask the recruiter for the sequence, the duration of each stage and whether you will be writing code
  • Work the reported questions below and time yourself

Where candidates lose points

Show your work

When discussing past research, be prepared to talk about the specific tools and libraries you used.

Be proactive

Given the nature of the startup environment, demonstrate your ability to identify problems and propose solutions without waiting for instructions.

Ask meaningful questions

Use the time at the end of your interviews to ask about our current technical challenges or how the engineering team supports our long-term research goals.

Always have 2-3 prepared questions for your interviewers

This shows you have researched the company and are genuinely interested in the team's success.

Questions & practice

Collapse a redelivered event batch into per-aggregate high-water marks

Easy · hashing · at-least-once · aggregation

You drain a batch of up to 5,000,000 events, each (aggregate_id BIGINT, aggregate_version INT, event_type, payload). The log guarantees order within one aggregate only; the batch merges 64 partitions, and a relay failover has redelivered a range, so an older version for an aggregate can appear after a newer one. Given a map of last_applied_version per aggregate, produce the events worth applying, at most one per (aggregate_id, version), plus the count discarded. Target O(n) time. State the memory for 2,000,000 distinct aggregates and what you do when it does not fit.

Approach

  1. One pass, one hash map from aggregate_id to the highest version kept, and a discard counter. An event whose version is at or below last_applied_version for its aggregate is dropped without further work, which is the whole reason the event carries its version rather than a delta. O(n) expected time, O(d) space in distinct aggregates.
  2. Keep the maximum, never the last occurrence. The redelivered range means the final appearance of an aggregate in the batch can be an older version than one seen earlier in the same batch, so last-wins applies stale state over newer state and the projection regresses with no error anywhere.
  3. Cost the memory instead of calling it large: an 8-byte key plus a 4-byte version is 12 bytes of payload, and an open-addressed table held at a 0.7 load factor costs roughly 17 bytes per entry before per-slot metadata, so 2,000,000 aggregates is tens of megabytes in a native layout and several times that in a runtime that boxes both key and value.
  4. If the distinct set exceeds memory, partition on hash(aggregate_id) mod P and reduce each partition independently. Every event for one aggregate hashes to the same partition, so the per-partition result is exact and the merge is concatenation rather than a second reduction.

Follow-up

  • The payload is a patch rather than a snapshot, so applying only the highest version loses the intermediate changes. What changes in your reduction?
  • How do you detect that version 7 arrived while version 6 was never delivered, and what should the consumer do about the gap?

Canonicalise a request body into a stable idempotency fingerprint

Medium · parsing · canonicalisation · hashing

idempotency_key.request_fingerprint is a SHA-256 over the method, path and canonicalised body, and a retry whose fingerprint differs must be rejected with 422 rather than served the stored response. Write the canonicaliser. Bodies are JSON up to 256 KB nested at most 32 levels; clients vary key order, whitespace and unicode escaping, and some send 64-bit ids as JSON numbers. Produce a deterministic byte string such that semantically identical bodies match and any semantic difference does not. State your complexity and name two normalisations you refuse to perform.

Approach

  1. Parse once into a tree, then re-serialise under fixed rules: object keys sorted, array order preserved, one escaping convention, no insignificant whitespace. Parsing is O(n) and sorting keys is O(k log k) per object, so O(n log n) overall with O(depth) stack, and the 32-level cap is enforced during parsing because hostile nesting is how a canonicaliser becomes a stack overflow.
  2. Sort keys by their UTF-8 bytes and say why the obvious implementation is wrong in some runtimes: a default string comparison that orders by UTF-16 code units places surrogate pairs, meaning code points from U+10000 up, below U+E000 to U+FFFF, which is not UTF-8 byte order, so two services written in different languages disagree on the same document.
  3. Do not re-encode numbers through a double. IEEE-754 binary64 represents integers exactly only up to 2^53, so normalising a 19-digit id through a float changes it, and 1 against 1.0 cannot be reconciled without deciding whether they are the same value. Preserve the literal token, and require ids as strings at the API boundary if you want them comparable.
  4. Reject duplicate keys rather than picking one. JSON permits them and parsers disagree, most keeping the last, so any choice you make ties the fingerprint to a parser detail that the code handling the request does not necessarily share.

Follow-up

  • A client sends the same logical request with an extra field your API ignores. Same key, different fingerprint, so you return 422. Is that the right answer?
  • Where does the fingerprint get computed relative to request decompression and the body-size limit?

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

Stop tag and share joins from fanning out a page

Medium · join fan-out · semi-joins · many-to-many

resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.

Approach

  1. Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
  2. Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
  3. Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
  4. Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.

Follow-up

  • The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
  • A resource can be shared with the same user twice under different permissions. Does your count change, and should it?

Can you walk us through your most significant previous research or technical project?

Medium · Technical Research and Experience

Approach

  1. Pick the project you can defend three levels deep, not the most impressive-sounding one: work where you owned design choices and can explain the alternatives you rejected, because every decision you mention invites a question.
  2. Open with a 60-second summary: the problem, why it mattered, your role, and the outcome. Then pause and let the interviewer steer; a ten-minute monologue hides whether you can adjust depth to your listener.
  3. Separate "I" from "we" explicitly. Say what the team built, then what you personally designed, wrote, or decided, e.g. "the team defined the study; I built the ingestion pipeline and chose the storage format".
  4. Spend the middle on one or two hard decisions: the constraint, the options you weighed, why you picked one, and what it cost. This turns a résumé recital into evidence of engineering judgment.
  5. Close with the outcome in numbers (runtime, accuracy, data volume, users, hours saved) and who used the result afterwards, so the listener hears the project's impact, not only that it was finished.
  6. Avoid a tour of every tool used, jargon the listener cannot follow, or a project whose outcome you cannot state. If the work sat in a specialized field, explain the domain in one plain sentence before the technical detail.

Follow-up

  • What would you do differently if you started again? Name one specific technical decision and its cost, such as a data format or architecture choice, not "more time" or "better planning".
  • How did you know the result was correct? Describe the validation (baselines, held-out data, sanity checks against known cases, independent review) and any bug that validation caught.
  • Which part would not have happened without you? Point to a component or decision you owned end to end and what would have been different if someone else had built it.

How did you approach the technical challenges you faced in your previous research?

Medium · Technical Research and Experience

Approach

  1. Treat this as a question about your problem-solving process, not a replay of the project. Pick one specific obstacle, e.g. a model that would not converge, a job that ran out of memory, or results that would not reproduce.
  2. Define the challenge precisely before solving it: what you expected, what actually happened, and why the obvious fix did not apply. A vague "it was hard" gives the interviewer nothing to evaluate.
  3. Walk through your method as hypotheses and tests: how you broke the problem down, what you measured or isolated first, and what each experiment ruled out. Include at least one dead end; a story with no wrong turns sounds rehearsed.
  4. Show how you used resources: papers, documentation, a throwaway prototype, or a colleague with the right expertise. Say when you asked for help and the precise question you brought, which signals judgment rather than dependence.
  5. End with the resolution, its measurable effect (e.g. "processing went from hours to minutes", "error dropped by a third"), and the reusable lesson, such as a check or debugging habit you now apply to every project.
  6. Stories that backfire: an interpersonal conflict dressed up as a technical one, a problem someone else actually solved, or one where tools or teammates take the blame. Keep ownership on your decisions, including the ones that failed.

Follow-up

  • What did you try that did not work? Name a specific dead end, what it ruled out, and how quickly you recognized it; this shows you learn from failed experiments instead of hiding them.
  • How did you decide when to abandon an approach? Describe a time-box or a kill criterion set in advance, such as a metric threshold, rather than sunk-cost persistence.
  • How did the challenge change the project's plan or results? Say whether scope, timeline, or conclusions shifted and how you told your collaborators.

p99 jumped on one listing filter while p50 stayed flat

Medium · query planning · composite index · percentiles

After a release that added an owner_user_id filter to the resource listing, p99 rose from 90 ms to 1.9 s while p50 stayed at 40 ms. Traffic and row counts are unchanged. resource carries the index (tenant_id, status, updated_at DESC, resource_id DESC). The new query filters tenant_id and owner_user_id, orders by updated_at DESC, resource_id DESC, and takes 20 rows. On PostgreSQL, explain the shape of the regression, prove it from a query plan, and give the index you would add.

Approach

  1. Start from the shape. A flat p50 with a moved p99 means a subset of requests changed cost, not all of them, so the first job is naming the subset. Bucket the endpoint's latency by the tenant's row count; the natural hypothesis is that large tenants are a small share of requests and all of the tail.
  2. Get the plan for the new query on a large tenant with EXPLAIN (ANALYZE, BUFFERS). Expect an index scan over the tenant's range, a filter discarding most of it, then a Sort feeding the Limit, possibly reporting Sort Method: external merge Disk. Read actual rows on the scan node, not estimated.
  3. Explain why the existing index cannot serve it. A composite B-tree is seekable only as a left prefix, and with no equality predicate on status the scan cannot treat updated_at as an ordering, because rows in the tenant's range are ordered by status first. Everything matching must be read and sorted before LIMIT 20 can apply, so a tenant with 400,000 rows pays 400,000 rows to return 20.
  4. Add (tenant_id, owner_user_id, updated_at DESC, resource_id DESC). Equality on the first two columns leaves the index ordered by updated_at within that pair, so the plan becomes an index scan that stops after 20 rows with no Sort node. PostgreSQL can scan a B-tree backwards, so the DESC markers matter only if the two sort columns ever disagree in direction; keeping them explicit documents the order the keyset cursor depends on.

Follow-up

  • The endpoint paginates with OFFSET. What does page 500 cost with your index, and what does the keyset version cost?
  • How would you have caught this before release, given that a 10,000-row seed database produces the same plan shape at an unnoticeable cost?

Suggested practice checklist

Built from the topics and questions iLoF candidates report; no round sequence has been reported.

Day 1 — Establish the iLoF format

  • No round sequence has been reported, so ask your recruiter for the sequence, the duration of each stage and whether you will write code.

Deliverable: A written reply from your recruiter confirming the format.

Day 2 — Work Technical Interviewing (Past Experience Deep-Dive)

  • Spend the session on Technical Interviewing (Past Experience Deep-Dive), which iLoF candidates report being tested on.
  • Write one worked example in Technical Interviewing (Past Experience Deep-Dive) and time yourself on it.

Deliverable: One timed worked example in Technical Interviewing (Past Experience Deep-Dive).

Day 3 — Work Interview Communication

  • Spend the session on Interview Communication, which iLoF candidates report being tested on.
  • Write one worked example in Interview Communication and time yourself on it.

Deliverable: One timed worked example in Interview Communication.

Day 4 — Work HR Interview Preparation

  • Spend the session on HR Interview Preparation, which iLoF candidates report being tested on.
  • Write one worked example in HR Interview Preparation and time yourself on it.

Deliverable: One timed worked example in HR Interview Preparation.

Day 5 — Answer out loud: Technical Research and Experience

  • Answer aloud, timed: Can you walk us through your most significant previous research or technical project?
  • Answer aloud, timed: How did you approach the technical challenges you faced in your previous research?

Deliverable: Spoken answers to 2 reported Technical Research and Experience question(s), under time.

Day 6 — Answer out loud: Behavioral and Cultural Alignment

  • Answer aloud, timed: Why do you want to work for iLoF specifically?

Deliverable: Spoken answers to 1 reported Behavioral and Cultural Alignment question(s), under time.

Day 7 — Dry run for iLoF

  • Run one full mock under time, then write down the two questions you most want to ask your interviewers.

Deliverable: A completed timed mock and two questions to ask.

Behavioural preparation

Behavioural rounds judge the decision you made and what it cost.

What specific technologies did you utilize to solve [X] problem in your past work?

Medium · Technical Research and Experience

Approach

  1. [X] stands for a problem from your own past work, so prepare an answer for every project on your résumé. For each, write the stack by layer (language, libraries, storage, compute, tooling) and the job each piece did.
  2. Answer with reasons, not a list. For each major choice give the constraint that drove it and the alternative you rejected, e.g. "Parquet over CSV because typed columns and column-selective reads cut load time".
  3. Be honest about inherited choices. "The team already ran PostgreSQL, so I used it and learned its indexing" is a fine answer; claiming you chose something you did not will unravel on the next question.
  4. Name one real limitation you hit and how you worked around it: memory limits, a library bug, slow I/O, a dependency conflict. Knowing where a tool breaks is the strongest signal that you used it in depth.
  5. Only name tools you can discuss beyond the functions you called. If you name a library, expect questions on its internals or tradeoffs; if you only touched it briefly, say so rather than let the interviewer assume expertise.
  6. Tie the stack back to the outcome: what the technology made possible and by how much, e.g. throughput, turnaround time, or a result someone else could reproduce. The tool matters only through the problem it solved.

Follow-up

  • Why not the obvious alternative? Give the concrete constraint that ruled it out, such as data size, latency, or team familiarity, and the conditions under which the alternative would have won.
  • Would you pick the same stack today? Say yes or no and why; tool maturity, scale, or team changes are good reasons, and admitting a choice aged badly reads as maturity, not weakness.
  • How did you manage versions and environments? Pin every transitive dependency in a lockfile with hashes, and keep the built container image, referenced by digest rather than a mutable tag, so others can recreate the exact environment later.

How do you handle technical debt when working on research-heavy codebases?

Medium · Technical Research and Experience

Approach

  1. Frame your answer as a calibrated policy for keeping research code trustworthy without slowing experiments, not "I never allow debt" or "I rewrite everything", and back it with one concrete example of your own and its measured effect.
  2. Separate code by lifespan. Exploratory notebooks and one-off scripts can stay rough; once a result will be shared, published, or fed into another pipeline, promote the code into a module with tests, explicit inputs, and no hard-coded paths.
  3. Pay down the debt that threatens correctness and reproducibility first: unpinned dependencies, missing random seeds, unversioned input data, and copy-pasted analysis variants. In research code a silent numeric error costs far more than messy style.
  4. Add cheap safety nets before refactoring: a lockfile for the environment (poetry.lock, uv.lock, or conda-lock.yml from conda-lock), a golden-output regression test on a small fixture dataset compared within a tolerance, and CI running lint plus those tests.
  5. Make repayment routine and visible: log each debt item with an owner and the cost it imposes, reserve a fixed slice of each cycle, and clean up code you are already touching. Rank items by how often the code changes and how many results depend on it.
  6. Show the collaboration side: researchers adopt practices that save them time, so offer templates, shared utility libraries, and fast CI instead of process mandates, then name one practice you introduced that researchers kept using unprompted.

Follow-up

  • How do you get researchers to adopt tests or code review? Share a case where a test caught a bug before it reached a result, and what changed in how the team worked afterwards.
  • When would you rewrite instead of refactor? Only when the code's core assumptions no longer hold and golden outputs exist to verify parity; otherwise migrate it piece by piece behind tests.
  • How do you explain debt to a non-engineering stakeholder? Frame it as risk and time, e.g. "this could silently change results" or "each new analysis takes two extra days", not as code quality.

Why do you want to work for iLoF specifically?

Easy · Behavioral and Cultural Alignment

Approach

  1. A strong answer shows three things: that you researched the company, that your reasons are about this work rather than any job, and that your background fits the role. An answer you could give to any employer shows none of them.
  2. Build the specific part from what iLoF itself has published, starting with its website and the job description. Cite one or two concrete things you read and why each matters to you; never guess at details you have not read.
  3. Connect it to your history with evidence: a project, skill, or interest that maps to what the posting asks for, e.g. "the posting emphasizes X, and that is what my last project was about". State the link outright instead of leaving it to be inferred.
  4. Add what you want to grow into and why this role offers it, citing a line from the posting or something an interviewer told you. If you cannot point to a source for a trait, ask about it instead of offering it as a reason.
  5. Avoid answers centered on pay, location, title, or brand, and avoid overclaiming domain expertise you lack. If the field is new to you, say so and describe how you have ramped up on an unfamiliar area before.
  6. Keep it to about a minute in three beats: the mission or problem, the role fit, the growth. Then ask something your research could not answer, which shows the interest is real rather than rehearsed.

Follow-up

  • What do you know about what we do? Summarize the company's public materials in your own words, and ask about the part you are unsure of rather than bluffing.
  • Where else are you interviewing? Answer honestly at a high level, name the common thread across those roles, and say why this one stands out on that thread.
  • What would you want to accomplish in your first six months? Tie each goal to a responsibility the job description names, and say which one you could deliver first and why.

Argue against a design, lose, and commit anyway

Medium · disagreement · service boundaries · decision records

Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.

Approach

  1. State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
  2. Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
  3. Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
  4. Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.

Follow-up

  • What threshold on that alert would have proved you right, and did anyone ever look at it?
  • If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?