As a Software Engineer at the University of British Columbia (UBC), you are at the intersection of academic innovation and large-scale technical infrastructure. You will work on systems that support world-class research, student success, and institutional policy, often operating in complex, multi-stakeholder environments. Whether you are building tools for Health Research & Policy or developing cutting-edge xR (Extended Reality) applications, your contributions directly impact the university's ability to disseminate knowledge and conduct vital research.
The work here is characterized by its scale and its public-service mission. You will not only be writing code but also collaborating with researchers, administrators, and faculty to translate complex requirements into stable, performant software. This role offers the unique opportunity to solve engineering problems that have a tangible, positive impact on the community, all while working within a highly collaborative and intellectually stimulating campus environment.
Initial Screening
reportedBefore 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 Discussions
reportedInput 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
Cultural Fit Assessment
reportedMost 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.
Team Meetings
reportedWhen a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.
What to demonstrate
- Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
- Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
- Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
- Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened
How to prepare
- Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
- Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
- Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure
Final Selection
reportedWhere the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.
What to demonstrate
- Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
- Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
- Whether you establish what decision is on the table before proposing anything
- Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip
How to prepare
- Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
- Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
- Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it
PracHub editorial advice for the preparation topics above.
Applying a bulk roster snapshot as authoritative, including its absences.
A truncated file, a partially completed export, or a change in the upstream identifier scheme all present as a large set of rows that vanished, and nothing in the file distinguishes that from a real mass withdrawal. Applied naively it deprovisions enrollments in a single run, and the recovery is not just an undo because in-progress work and derived caches have already moved. Every pipeline that survives has a proportional gate, a recorded diff, and a restartable apply keyed on the snapshot digest.
Running learner submissions without hard, enforced resource ceilings.
Submitted code is untrusted input written by people who are actively learning and sometimes actively probing. Without a wall-clock limit alongside the CPU limit, a sleeping or blocked process pins a worker; without a process/thread cap, a three-line fork bomb takes the host; without an output cap, a stray print loop fills the disk and the log pipeline; without network isolation, a submission can reach internal services or exfiltrate the hidden tests. Each ceiling has to be enforced by the kernel or the runtime, since a limit checked by the harness after the fact is not a limit.
Assuming the bug is in the framework
Suspect your own code first: read the stack trace top to bottom, check which versions are actually installed rather than which ones you believe are, and reproduce in isolation before blaming a library that thousands of people run daily. When the fault really is upstream, you need that minimal reproduction to say so credibly anyway.
Treating a network call as though it were a local function call
A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Detect two-device autosave interleaving in an event stream
Autosave events arrive on a stream as (attempt_id, client_instance_id, seq, received_at) at roughly 4,000 events per second, out of event-time order by at most 120 seconds. For each attempt, report the earliest 600-second window in which that attempt's autosaves alternate between client instances — A, then B, then A again — which indicates a second tab or device rather than a reload. A plain handoff, where one instance stops and another starts, must not be reported. Memory must be bounded by concurrency, not by total events. State your bounds.
Approach
- Partition by
attempt_idso every event for one attempt lands on one worker, then buffer per attempt until the watermark — the maximumreceived_atseen minus the 120-second lag bound — passes an event's timestamp. Only then is that event's event-time position final. Anything arriving after its watermark goes to a side output and is counted, not silently dropped, because a rising late rate is the signal that the lag bound is wrong. - Inside one attempt, slide two pointers over the event-time-ordered buffer, advancing the right edge one event at a time and evicting from the left while
right.t - left.t > 600. Each event enters and leaves exactly once, so the window maintenance is O(1) amortised per event. - Represent the window as a run-length-compressed deque of
(client_instance_id, count)rather than a raw list. Pushing on the right either increments the tail run or appends a new one; evicting on the left decrements the head run and pops it at zero. Runs never merge, because adjacent runs have different ids by construction, so both operations stay O(1). - Read the alternation criterion straight off that structure: three or more runs in the window means A, B, A or three distinct instances, which is interleaving. Exactly two runs is a clean handoff — a reload where one instance stops and the next begins — and must not fire. Counting distinct ids instead of runs is what produces a false positive on every browser refresh.
- Bound the memory arithmetically. At 4,000 events per second with one autosave per learner every 25 seconds, about 100,000 attempts are active, and a 600-second window holds roughly 24 events each. That is 2.4 million live events; packed at 48 bytes it is about 115 MB, and several times that with boxed objects. The run-compressed deque is smaller still, since a single-instance window collapses to one entry.
- Separate this from the write-path rule it resembles. The attempt rejects an autosave whose
seqis belowlast_autosave_seq, which protects the stored state; it does not detect anything, because two tabs sharing a monotonic counter produce strictly increasing sequences and never trip it. Detection and rejection answer different questions and both are needed.
Worked solution 35 min
- Implement the per-attempt reorder buffer keyed on event time, releasing events only once the watermark has passed them, and count anything later as a side output.
- Implement the run-compressed window with explicit push-right and evict-left operations, asserting after each that no two adjacent runs share an id.
- Emit the window's start instant the first time the run count reaches three for an attempt, and stop tracking that attempt for reporting.
- Feed three fixtures: a clean handoff A...A then B...B; an interleave A, B, A inside 600 seconds; and an A, B, A where the two A events are 700 seconds apart.
- Replay each fixture with the events shuffled within the 120-second lag bound and confirm the verdicts are unchanged.
Follow-up
- A learner's connection drops for ten minutes and the client replays its buffered autosaves on reconnect. Does your detector fire, and should it?
- You must now also report how many seconds of overlap the two instances had. What changes in the window structure?
- The lag bound is violated and events arrive 15 minutes late for one tenant. What does your side output let you do that a dropped-event counter would not?
Dispatch a bell-driven autograde burst under per-tenant caps
Thirty thousand autograde jobs enqueue over 90 seconds as a district's periods end. Each carries tenant_id, enqueued_at, a language-dependent wall_limit_seconds between 2 and 15 with a mean near 3, and a deadline equal to its section's period end. You have 400 one-shot workers, jobs cannot be preempted once started, and no tenant may hold more than 20 percent of the workers at once. Choose the dispatch order that maximises jobs returning feedback before their deadline, give the data structure and its per-dispatch cost, and say plainly whether that order is optimal.
Approach
- Do the capacity arithmetic first, because it reframes the question. Four hundred workers at a 3-second mean serve about 133 jobs per second; 30,000 arrivals over 90 seconds is 333 per second. Twelve thousand are served during the burst, leaving 18,000 queued, which drains in about 135 more seconds. The system is oversubscribed by roughly two and a half times, so the scheduler is not deciding whether anyone misses a deadline — it is deciding who.
- Choose earliest-deadline-first and then state its standing precisely. On a single machine with preemption, EDF meets every deadline whenever any schedule can. This is neither: 400 machines and non-preemptive jobs of unequal length. Minimising the number of late jobs on parallel identical machines is NP-hard — even minimising makespan on two machines reduces from PARTITION — so EDF here is a defensible heuristic, not an optimum. On one machine, Moore-Hodgson would give the exact optimum in O(n log n); that exactness does not survive the move to 400.
- Enforce the cap with a heap of queues rather than by scanning. Keep one min-heap per tenant ordered by deadline, and a global min-heap holding at most one entry per eligible tenant keyed on that tenant's head deadline. Dispatch pops the global head, pops that tenant's head job, increments its in-flight count, and re-pushes the tenant only if it is still under 80 workers. On completion, decrement and re-insert the tenant if it was parked with work waiting. Each dispatch is O(log T + log J_t) instead of O(J), and the cap costs a counter comparison rather than a rescan.
- Add admission control, which is where most of the win actually is. A job whose
now + wall_limit_secondsalready exceeds its deadline cannot return feedback in time no matter what, so running it burns a worker slot that a still-feasible job could have used. Route it to a graded-after-the-bell path immediately. This is the Moore-Hodgson instinct — discard the jobs you cannot save — applied as a filter rather than as an exact algorithm. - Break ties on
enqueued_atascending so the order is deterministic and a job cannot be starved by a stream of equal-deadline arrivals. Without a deterministic tiebreak the same burst replays differently, and an operator comparing two incidents cannot tell a scheduling change from noise. - Give the honest ceiling. No dispatch order makes a 2.5x oversubscription feasible; the levers are worker count, wall limits per language, and whether feedback is required before the bell at all. A candidate who presents EDF as the fix, rather than as the allocation of an unavoidable shortfall, has answered a different question.
Follow-up
- One tenant submits 20,000 of the 30,000 jobs. Show what your cap does to the other tenants' deadline-miss rate versus pure EDF.
- Workers become preemptible with a checkpoint. Does EDF's optimality argument come back, and under exactly what conditions?
- A job exceeds its wall limit and is killed. Does it re-enter the queue, and what does your answer do to the deadline guarantees of everything behind it?
Stream a quoted roster CSV without loading the file
You are handed a 2 GB roster export as a byte stream. It follows RFC 4180: any field may be quoted, a quoted field may contain commas and CRLF, a literal quote inside a quoted field is written as two quotes, and the file may open with a UTF-8 byte order mark. There are up to 10,000,000 records and a single field may reach 64 KB. Write a reader that yields one record at a time without buffering the whole file and that fails loudly on a malformed file rather than emitting a short record. State your bounds.
Approach
- Refuse the shape that fails first: splitting the stream on newlines and then splitting each line on commas. A quoted field may contain a CRLF, so line splitting cuts records in half, and the damage is silent because both halves parse into plausible short records.
- Write a byte-level state machine with four states — field start, unquoted field, quoted field, and quote-seen-inside-quoted. In the last state a second quote emits one literal quote and returns to quoted; a comma or newline ends the field; anything else is a malformed file. That table is the whole parser and it is the part to get right on paper before typing.
- Strip the BOM only at offset zero, comparing the first three bytes against EF BB BF. Left in place it becomes part of the first header name, so the header lookup for that column misses and the column reads as absent for every record in the file.
- Cap field length at the stated 64 KB and record length at a sane multiple of it. Without a cap, one unbalanced quote makes the parser treat the remaining 2 GB as a single field and the process dies of memory exhaustion rather than telling you the file is broken at byte 12,004.
- Treat end of stream inside a quoted field as a hard error, not an implicit close. A truncated upload is the most common malformed input here and it is byte-for-byte indistinguishable from a complete file if you close the field silently — the missing rows then present downstream as a mass withdrawal.
- Complexity: O(n) time in bytes with one pass and no backtracking, and O(longest field + longest record) space, which is bounded by the caps rather than by the file. Read in fixed blocks and keep the partial field across block boundaries instead of reading line-wise.
Follow-up
- The file arrives gzipped and you must report progress as a percentage. What can you actually report, and what does that do to your memory bound?
- Two exporters disagree on line endings and one emits a bare LF inside a quoted field. Does your state machine care, and should it?
- How do you distinguish a truncated file from a complete one when the byte stream itself gives you no signal?
Answer a point-in-time roster question against soft-deleted enrollments
enrollment(enrollment_id, tenant_id, section_id, person_id, role CHECK IN ('learner','instructor','aide','observer'), source, begin_date, end_date NULL, status, created_at, updated_at, deleted_at NULL) carries partial UNIQUE (tenant_id, section_id, person_id, role) WHERE deleted_at IS NULL. A learner withdraws in week 3 and re-enrols in week 6; one person is both aide and observer in the same section. Write the query returning learners enrolled in one section on a given date, name the index that serves it, and say what the deleted rows can and cannot tell you about a past roster.
Approach
- Separate the two time axes before writing anything. begin_date and end_date are valid time — when the enrollment was true in the world. deleted_at is transaction time — when the system stopped asserting the row. An 'as of date D' roster is a valid-time question, so the date predicate does the work and deleted_at only filters what you currently believe.
- Write it as: SELECT p.person_id, p.display_name FROM enrollment e JOIN person p ON p.person_id = e.person_id AND p.tenant_id = e.tenant_id WHERE e.tenant_id = $1 AND e.section_id = $2 AND e.role = 'learner' AND e.deleted_at IS NULL AND e.begin_date <= $3 AND (e.end_date IS NULL OR e.end_date >= $3) ORDER BY p.display_name. The role predicate, not DISTINCT, is what stops the dual-role person appearing twice.
- Notice what the partial unique index forbids: two live rows for the same (tenant, section, person, role) even when their date ranges are disjoint, so a genuine withdraw-and-return cannot be modelled as two live intervals. Either the first row is soft-deleted, which destroys the interval, or the uniqueness moves to EXCLUDE USING gist (tenant_id WITH =, section_id WITH =, person_id WITH =, role WITH =, daterange(begin_date, end_date, '[]') WITH &&) WHERE (deleted_at IS NULL), which needs the btree_gist extension and permits disjoint intervals while still rejecting overlaps.
- Index for the equality prefix: (tenant_id, section_id, role, begin_date) WHERE deleted_at IS NULL. Only the leading equality columns plus the first inequality are scan keys, so end_date belongs in the index for the visibility of the filter, not as a second range key.
- State the limit of a soft delete: it stores one bit and one timestamp, so it answers 'is this row still asserted', not 'what did we believe on D'. Reconstructing a past belief needs an append-only history row per change, or the recorded diff from the ingestion run; if support has to answer why a learner vanished, the run record is the artefact, not deleted_at.
Follow-up
- The feed sends two overlapping intervals for the same learner in the same section. Which one wins, and what does the exclusion constraint do to that apply?
- A learner withdrew but has a graded attempt in week 2. What does the gradebook show, and which predicate in your query decides it?
- How do you answer 'which learners did we deprovision last Tuesday' with this schema?
Explain why the grading queue index stopped serving its ORDER BY
attempt holds 2 billion rows and 99.7% are state='graded'. The grading queue runs SELECT attempt_id FROM attempt WHERE tenant_id = $1 AND state IN ('submitted','grading') ORDER BY submitted_at LIMIT 100 against index ix_attempt_queue (tenant_id, state, submitted_at). At the bell the query takes 30 seconds and EXPLAIN shows a Sort above a bitmap heap scan, even though the queue is only a few thousand rows. Explain why that index cannot serve the ordering, give the replacement definition, and say how the replacement behaves as rows leave the queue.
Approach
- Start from what the index actually orders. Its tuples are sorted by (tenant_id, state, submitted_at), so with two state values the scan yields all 'grading' rows in submitted_at order, then all 'submitted' rows in submitted_at order. The query wants one submitted_at order across both, which that physical order does not provide, so the planner must sort the whole matching set and the LIMIT cannot stop the scan early. With a single state constant the same index satisfies the ordering perfectly — the plan changed because of the IN list, not because of the data.
- Replace it with a partial index that moves the predicate out of the key: CREATE INDEX CONCURRENTLY ix_attempt_queue ON attempt (tenant_id, submitted_at) WHERE state IN ('submitted','grading'). Ordering is now satisfied directly, the LIMIT stops after 100 index tuples, and the index covers roughly 0.3% of rows so it stays resident in cache instead of competing with the table.
- State the precondition, because it is the part people skip: the planner uses a partial index only when it can prove the query's predicate implies the index predicate. A literal IN list matching the definition proves; state = ANY($1) with the list bound as a parameter does not, so the queue's state set has to be literal in the SQL, not passed in.
- Fix the estimate separately from the access path. With five states the planner already has each one in its most-common-values list, so single-column selectivity is fine; what it gets wrong is the dependence between tenant_id and state, because a bell means one tenant owns nearly the whole queue while the planner multiplies the two selectivities as if independent. CREATE STATISTICS ON tenant_id, state FROM attempt corrects it, and you ANALYZE explicitly after a bulk transition rather than waiting for autovacuum, whose analyze threshold of 50 + 0.1 x reltuples needs 200 million changed rows on this table.
- Account for the lifecycle. A row leaves the queue by UPDATE state='graded', so its new version is not in the partial index at all while the old index entry remains until vacuum reclaims it. The index therefore churns in proportion to throughput rather than to depth: size it and vacuum it against how many attempts pass through per day, not against how many sit in the queue at once.
- Keep the alternative in your pocket for when the partial index is not acceptable: two explicit scans merged, (SELECT ... WHERE state='submitted' ORDER BY submitted_at LIMIT 100) UNION ALL (SELECT ... WHERE state='grading' ORDER BY submitted_at LIMIT 100) ORDER BY submitted_at LIMIT 100, which keeps the existing index and pushes the LIMIT into both branches. Confirm whichever you choose with EXPLAIN on your major version rather than from memory, since array-key handling in b-tree scans has changed between releases.
Worked solution 25 min
- Reproduce on a scaled copy: 20 million attempts, 0.3% in the queue, and confirm EXPLAIN (ANALYZE, BUFFERS) shows a Sort node above the scan.
- Re-run the identical query with state = 'submitted' alone and observe the Sort disappear, which isolates the IN list as the cause.
- Build the partial index CONCURRENTLY, re-run, and compare buffers touched and actual rows.
- Change the literal list to a bound parameter and confirm the partial index is no longer chosen.
Follow-up
- The queue drains and refills eight times a day. What is the vacuum strategy for this index, and what do you monitor to know it is losing?
- A second consumer claims rows with FOR UPDATE SKIP LOCKED. Does the plan survive, and what does skipping do to the ordering guarantee?
- Support wants oldest-first fairness across tenants rather than within one. Does the same index serve that query?
If a project requirement changes mid-development due to new research f…
If a project requirement changes mid-development due to new research findings, how do you adjust your strategy?
Approach
- Choose a partition key and say what query it makes expensive.
- 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.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Walk me through the architecture of a recent project you led from conc…
Walk me through the architecture of a recent project you led from conception to deployment.
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 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 troubleshoot a critical failure in a production environment…
How do you troubleshoot a critical failure in a production environment?
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?
- What breaks first when traffic grows ten times?
What is your approach to ensuring accessibility and compliance in publ…
What is your approach to ensuring accessibility and compliance in public-facing software?
Approach
- 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.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Accept autosave writes that arrive twice and out of order
attempt holds attempt_id, tenant_id, assignment_id, person_id, attempt_no, state, started_at, server_deadline_at, last_autosave_seq BIGINT NOT NULL DEFAULT 0 and submitted_at. A learner's browser autosaves every 20 to 30 seconds carrying a per-attempt sequence number; the same learner has the assessment open in two tabs, and a flaky network retries a save whose response was lost. Design the write path: the exact conditional statement, what happens to a save whose sequence is below the stored one, and what the endpoint returns so a retry is always safe. State the isolation level you assume.
Approach
- State the two requirements separately: a retry of the same sequence must be a no-op that still reports success, and a lower sequence must be rejected rather than merged. Merging is the tempting move and it is wrong, because the two tab bodies are different answers to the same question and a merge produces a third answer no learner wrote.
- Express it as one statement: UPDATE attempt SET body = :body, last_autosave_seq = :seq, updated_at = now() WHERE attempt_id = :id AND tenant_id = :tenant AND state = 'in_progress' AND last_autosave_seq < :seq. A row count of 1 means applied; 0 means either stale or no longer in progress, and those two need different answers to the client.
- Justify the concurrency claim instead of asserting it. Under PostgreSQL READ COMMITTED, a second UPDATE of the same row blocks on the row lock and then re-evaluates its WHERE clause against the newly committed version, so the lower sequence loses deterministically and no SELECT-then-UPDATE or explicit lock is needed. Under REPEATABLE READ the same pair raises a serialization failure the client must retry, which is worse ergonomics for the identical outcome.
- Define the response contract. Applied and stale both return success carrying the stored sequence, because a client that receives an error will retry harder and a client that receives the stored sequence can resynchronise after a crash instead of restarting at 0 and being rejected forever. Only 'this attempt is already submitted' is a distinct terminal answer.
- Name what the sequence does not solve. It orders one attempt's writes; it does not decide which tab owns the session. If the policy is one active session, that needs a session token on the attempt, and without it a learner can silently overwrite good work from an abandoned tab that happens to hold a higher counter.
Worked solution 20 min
- Write four interleavings (a retry of the same sequence, 7 then 6, two tabs alternating, a save after submit) with the required end state for each.
- Write the single conditional UPDATE and mark which predicate handles which interleaving.
- State the isolation level and explain the re-check that makes the lower sequence lose without an explicit lock.
- Define the three responses and the field that lets a restarted client resynchronise its counter.
- Compute the sustained write rate and say where the body belongs if it is large enough to hurt the row.
Follow-up
- The client restarts with no memory of its sequence number. Where does it obtain the next one, and what stops two tabs from choosing the same value?
- Autosave bodies are 50 KB arriving every 25 seconds for 40,000 concurrent learners. Does the attempt row remain the right place for the body, and what does that update rate do to table bloat and write-ahead log volume?
- A save arrives after submitted_at is set. What does the endpoint return, and is that learner's work lost or recoverable?
Autosave sequence goes backwards and loses learner work
About one attempt in 3,000 loses roughly 40 seconds of learner work. The audit log shows attempt.last_autosave_seq decreasing from 57 to 56 at 10:14:03.118. It only happens for learners with two tabs open or a second device, and it never reproduces in staging. The autosave handler reads last_autosave_seq, compares it to the incoming sequence in application code, then updates the payload and the sequence by primary key. The database runs READ COMMITTED. Give an ordered diagnostic checklist, the root cause, a deterministic reproduction, and the fix.
Approach
- Order the checklist so the decisive observation comes first. A monotonic counter that decreases is not a client problem and not a timing coincidence; it means two writers both decided they were the newest. So: first, confirm the two writes are close in time and from different sessions on the same attempt. Second, read the handler for a read-then-write across two statements. Third, check the isolation level and the WHERE clause of the write. Only after those would you look at the client's sequence generation, and here you will find it correct.
- State the interleaving exactly. Requests carrying sequences 56 and 57 arrive 8 ms apart from two tabs. Both SELECT and read 55. Both pass the application-level guard. The write for 57 commits first; the write for 56 then acquires the row lock, and because its WHERE clause names only the primary key it has nothing left to check, so it overwrites both payload and sequence. Final state is 56 with the older payload. The guard was evaluated against a snapshot that the write did not re-validate.
- Make it deterministic before fixing it, because a fix you cannot fail is a fix you cannot trust. Add a test hook that sleeps between the SELECT and the UPDATE, drive two concurrent autosaves from separate connections with a barrier, and the 1-in-3,000 becomes 1-in-1. The rarity was only the width of the window.
- Fix by moving the guard into the predicate of the write: UPDATE attempt SET payload = $2, last_autosave_seq = $3 WHERE attempt_id = $1 AND tenant_id = $4 AND last_autosave_seq < $3, and treat zero rows affected as a rejected stale write rather than an error. This is a single statement, so there is no window.
- Say what the engine does with that statement, because this is where candidates hand-wave. Under READ COMMITTED in Postgres, an UPDATE that finds a row a concurrent transaction has just committed re-evaluates its WHERE clause against the new row version and skips the row if it no longer matches, so the seq guard holds without explicit locking. Under REPEATABLE READ or SERIALIZABLE the same statement raises a serialization failure instead and the client retries, after which it sees the newer row and correctly rejects. Both outcomes are correct; the engine-independent principle is that the comparison must be in the write, not between two statements.
- Reject rather than merge, per the attempt's contract: a lower sequence is stale by definition and merging two autosave payloads invents state the learner never had. Return a response that tells the losing tab it is behind so it can re-read rather than keep sending. The same shape covers the neighbouring race: two concurrent submits both computing attempt_no + 1 are one bug, and the UNIQUE (tenant_id, assignment_id, person_id, attempt_no) constraint is one remedy, with the unique violation read as 'the other request won' rather than as a failure.
Follow-up
- The losing tab keeps autosaving with sequences the server rejects. What should it do, and what should the server return so it can tell 'stale' from 'failed'?
- Make submit idempotent on a client-supplied key. What is the outcome when a second submit arrives after grading has started?
- This attempt is timed. How do you keep the server deadline monotonic when the same learner resumes on a second device?
For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Numbers before diagrams
- Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
- Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
- Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.
Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.
Practice prompt ↗Practice prompt ↗Worked solution ↗02One system, from requirements to schema
- Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
- Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
- Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.
Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.
Practice prompt ↗Practice prompt ↗03The consistency you are actually buying
- Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
- Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
- Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.
Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.
Practice prompt ↗Practice prompt ↗04Failure is the design
- For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
- Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
- Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
- Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.
Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Scaling the hot path
- Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
- Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
- Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
- Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.
Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.
Practice prompt ↗Practice prompt ↗06Keep the coding hand in, at the bar that applies to you
- Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
- Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
- Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.
Deliverable: One problem hardened to review standard, with its test list and one written review comment.
Practice prompt ↗Practice prompt ↗07Defend it while being interrupted
- Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
- Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
- Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.
Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.
Describe your experience with version control systems in a collaborati…
Describe your experience with version control systems in a collaborative, multi-developer environment.
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you handle technical debt when working within a long-term acade…
How do you handle technical debt when working within a long-term academic project lifecycle?
Approach
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
- 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?
What motivates you to contribute to an academic or research-focused en…
What motivates you to contribute to an academic or research-focused environment?
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
- 01
Describe your experience with version control systems in a collaborative, multi-developer environment.
- 02
How do you handle technical debt when working within a long-term academic project lifecycle?
- 03
What motivates you to contribute to an academic or research-focused environment?
Is this an official University of British Columbia interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at University of British Columbia. Rounds and questions reflect what candidates have reported, not a process University of British Columbia has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the interview process typically take?
The timeline can vary depending on the department, but you should expect a few weeks from initial screening to a final decision. Candidates report that the process prioritizes finding the right fit for the university's long-term projects.
PracHub interview research ↗Is there a specific focus on algorithms?
While technical interviews will cover coding, the focus is often on practical application and the ability to build sustainable software rather than purely theoretical algorithmic puzzles.
PracHub interview research ↗What is the culture like for engineers at UBC?
It is a collaborative and intellectually focused environment. You are encouraged to ask questions, share knowledge, and contribute to the broader mission of the university.
PracHub interview research ↗Should I be prepared for a technical assessment?
Yes, depending on the role, you may be asked to complete a coding challenge or a system design exercise to demonstrate your technical capabilities.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-22