As a Software Engineer at the University of Toronto, you play a pivotal role in maintaining and advancing the digital infrastructure that supports one of the world’s leading academic and research institutions. This position is not merely about writing code; it is about enabling students, faculty, and researchers to achieve their goals through reliable, scalable, and intuitive software solutions. Whether you are working on internal administrative systems, academic platforms, or research-driven applications, your work directly impacts the daily operations of a massive, diverse community.
The environment at the University of Toronto is uniquely collaborative and mission-driven. You will often work alongside professors, researchers, and departmental leads, requiring you to communicate complex technical concepts to non-technical stakeholders effectively. The complexity of the projects often stems from the scale of the user base and the specific, mission-critical needs of an academic environment. If you thrive in an intellectually stimulating atmosphere where your contributions have a tangible, real-world impact on education and innovation, this role offers a rare opportunity to bridge the gap between high-level engineering and institutional service.
Initial Screening
reportedHalf 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 Interview
reportedWhat this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Behavioral Interview
reportedWhat you say here is written down by each interviewer and compared afterwards, so the unit of evaluation is a claim someone else could check, not a well-told narrative. Two things make a story checkable: detail only a participant would hold, and a clean line around which part was yours. Vague ownership is the usual failure and it is usually accidental, because engineers say we about the team's work and we about their own, so the thing they personally built disappears into the plural. Name the part you wrote, and name who did the rest.
What to demonstrate
- Whether your details are ones a participant would hold and an observer would not: the constraint that ruled out the obvious approach, the first attempt that failed, the person who objected and on what grounds
- Whether ownership survives a direct question, since a follow-up to we decided is routinely who decided, and an answer that stays plural at that point is read as the work belonging to someone else
- Whether the numbers you quote are ones you would say identically to a former colleague with the dashboard open
How to prepare
- Go through each story replacing every we with either I or a named role (the on-call engineer, the reviewer, the other team) and check the story still holds together. Wherever it stops making sense you have found a part you cannot actually speak to
- Open the artefacts for two of your stories, the pull request, the design doc, the incident notes, and read them for dates and figures you have been rounding in the retelling. Correct your version to match
- For each story write the single sentence you would least want repeated to a former teammate, then either make it accurate or take it out
Practical 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.
Final Interview
reportedCoding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.
What to demonstrate
- Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
- Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
- Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
- Whether you can say which calls you made alone and which you escalated, and why the line sat where it did
How to prepare
- Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
- Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
- Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap
Offer Discussion
reportedYou 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.
PracHub editorial advice for the preparation topics above.
Editing a content item in place and thereby rewriting the meaning of every historical score.
Authors expect to fix a typo, change a distractor, or adjust a point value, and nothing warns them that thousands of stored results reference the record they are editing. If attempts and analytics join to the mutable item rather than to a version, a regrade or a rebuilt report scores past answers against an item that did not exist when they were given. Versioning has to be the default write path, because a convention that authors must remember will not hold.
Treating the launch subject identifier as a global user id, or merging identities on email.
The subject claim is stable only within its issuer, so the same human arriving from a second platform is a different subject and must be a second external identity bound to the same person. Email is worse than useless as a merge key here: privacy settings frequently suppress it from the launch entirely, districts recycle addresses between graduating and incoming students, and younger learners often have none. A merge on email eventually joins two children's work into one account, which is both a grade bug and a privacy incident.
A queue or buffer with no bound
Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
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?
Find peak concurrent attempts to size a pre-warm
You have one district's attempt rows for a single school day: started_at, and an end instant taken as submitted_at when present and server_deadline_at otherwise. There are up to 50,000,000 rows, all inside one local calendar day, and the district's org.time_zone is known. Compute the maximum number of simultaneously in-progress attempts and the second at which it occurs, so capacity can be pre-warmed against the bell schedule. A sort-based sweep is acceptable but is not the best answer here. State your bounds and what happens on a day with a daylight-saving transition.
Approach
- Note the structural fact that decides the algorithm: the output domain is one day at one-second resolution, so there are on the order of 86,400 distinct answer positions while there are 50,000,000 inputs. That inverts the usual sweep, because the coordinate space is far smaller than the data.
- Allocate a difference array over seconds from local midnight, with one sentinel slot past the end so the decrement for an attempt ending in the final bucket has somewhere to land. For each attempt add 1 at the bucket containing
started_atand subtract 1 at the bucket after the one containing the end instant, then prefix-sum once and take the argmax. That is O(N + T) time and O(T) space, which is 86,401 int32 values, about 346 KB — small enough to stay in L2 and to run per section if you want. - Say why this beats the sweep-line answer here. Sorting 2N endpoints is O(N log N) with 100,000,000 entries to materialise; the difference array touches each input twice and never sorts. The sweep only wins when the coordinate space is large or unbounded, which is exactly the condition this problem does not have.
- State the bias the bucketing introduces rather than hiding it. Every attempt is counted in full in every second it touches at all, so the bucketed maximum is greater than or equal to the true instantaneous maximum, never less, which is the safe direction for pre-warming. The overcount is not confined to sub-second attempts: two hour-long attempts that merely share a boundary second without ever overlapping, one ending at 10:00:05.2 and the next starting at 10:00:05.8, already put 2 in that bucket against a true instantaneous maximum of 1. The bound that does hold is per bucket. The attempts touching second s split into those that span it entirely, which by definition coexist at every instant of s, and those with an endpoint inside it, so the overcount at s is at most the number of attempts that begin or end within that second. Equality with an exact sweep is guaranteed when no attempt begins or ends inside the peak second — which holds, for instance, on a fixture whose every endpoint lands exactly on a second boundary.
- Handle the calendar honestly, and fix the indexing convention before sizing anything. Index buckets by elapsed seconds from the UTC instant of local midnight and size the array from the real UTC span between successive local midnights: in a zone that shifts by an hour that span is 82,800 seconds on the spring-forward day and 90,000 on the fall-back day, not 86,400. A hardcoded 86,400 therefore overruns on the fall-back day, whose last hour indexes up to 89,999, and merely leaves 3,600 dead slots at the tail on the spring-forward day, which is harmless. The opposite convention fails the opposite way: bucketing by local wall-clock seconds-since-midnight always stays in range, but maps the fall-back day's repeated hour onto buckets that already hold the first pass through it, folding two real hours of load into one and understating the peak.
- For per-section peaks, do not allocate T buckets per section — 10,000 sections is 3.5 GB. Partition the input by
section_idand reuse one array, or keep only sections whose total attempt count clears a threshold, since the rest cannot produce a peak worth pre-warming for.
Follow-up
- Now compute the peak across three districts in different time zones on one shared cluster. What is the coordinate space and does your answer survive?
- Some attempts have neither
submitted_atnorserver_deadline_atbecause they were abandoned. What end do you pick, and how does the choice bias the number you hand to capacity planning? - You need the top ten peak minutes rather than the single peak. What changes, and what does not?
Backfill frozen lateness without converting a hundred million times
A backfill must set due_at_utc on 1,000,000 assignment rows where it is NULL, from due_at_local plus policy_time_zone, and then set is_late on the 100,000,000 attempt rows beneath them whose is_late is NULL, by comparing submitted_at to that instant. Converting one local time per attempt row is correct and far too slow. Give a plan that is both faster and reproducible when re-run months later, state the speed-up you expect, and define the result for a local due time that is ambiguous or nonexistent across a daylight-saving transition.
Approach
- Name why the naive plan is slow rather than asserting it. A local-to-UTC conversion is a binary search over that zone's transition table plus an object construction, on the order of one to three microseconds in a managed runtime; 100,000,000 of them is a few minutes of pure conversion and, once per-row fetch and allocation are included, hours. Nothing about it is wrong — it is the row count multiplied by a cost that did not have to be paid per row.
- Collapse the cardinality. The conversion depends only on
(policy_time_zone, due_at_local), and due dates cluster hard on end-of-day instants for school days, so the distinct pair count is on the order of 10,000 against 100,000,000 attempts. Convert the distinct pairs once into a small mapping, then resolve each attempt with a hash probe. That is roughly a 10,000-fold reduction in conversions, and the per-row cost drops from a conversion to a lookup. - Do it in two stages so the second one is a join, not a computation: materialise
due_at_utcon the 1,000,000 assignment rows from the mapping, then setis_lateon attempts by joining onassignment_id. The attempt stage then performs no time-zone work at all, which is also what makes it re-runnable without the tz database on the path. - Pin reproducibility explicitly. The local-to-UTC mapping is a function of the zone, the local time, and the time-zone database version, because releases correct historical transitions. Record the tzdata version on the backfill run, or a re-run months later can produce a different instant for the same row and a grade will move with no grade event behind it.
- Define the two degenerate cases instead of letting a library default decide. An ambiguous local time in a repeated hour maps to two instants; a nonexistent one in a skipped hour maps to none. Pick the rule — the later instant when ambiguous, the first instant after the gap when nonexistent — encode it as an explicit flag rather than relying on a fold default, and assert both in tests. An 11:59 PM due time is outside the affected hour for most zones, so this is rare, which is exactly why it will not be noticed when it is wrong.
- Respect the freeze. Update only rows where
is_late IS NULL; a row that already carries the flag was decided at submit and must not be recomputed, or the backfill becomes a retroactive regrade. Then chunk the update by primary-key range with a bounded transaction size, because one 100,000,000-row statement holds locks and generates dead tuples for the whole run and cannot be resumed after a failure.
Worked solution 40 min
- Count the distinct
(policy_time_zone, due_at_local)pairs over the 1,000,000 affected assignments and write the number down; the whole plan rests on it being small. - Convert those pairs once into an explicit mapping table, recording the tzdata version and the ambiguity rule alongside it.
- Update
due_at_utcfrom that mapping, then updateis_lateon attempts by joining onassignment_idwith aWHERE is_late IS NULLpredicate, chunked by primary-key range. - Seed a fixture with a due time of 01:30 local on a fall-back date, one of 02:30 local on a spring-forward date, and one ordinary 23:59 due time, each with attempts on both sides of the boundary.
- Run the whole backfill twice and diff the resulting columns row by row.
Follow-up
- A district is moved to a different school with a different time zone mid-term. Which rows, if any, change, and what does the freeze rule say?
- The tzdata version on the backfill host differs from the one in the application image. How would you have found that out before the run rather than after?
late_policyisblockfor some assignments, so a late submission should not exist. What do you write for an attempt that submitted after the due instant under that policy?
Stop the gradebook rollup from counting attempts as learners
For one section, produce per published assignment: learners assigned, learners with a graded attempt, mean best score, and late count. Tables: enrollment as above (a person may hold two roles, and may have withdrawn and re-enrolled), assignment(assignment_id, tenant_id, section_id, status, max_attempts), attempt(attempt_id, tenant_id, assignment_id, person_id, attempt_no, state, is_late), score(attempt_id PRIMARY KEY, tenant_id, score_given, score_maximum, revision). Grading policy is best attempt wins. Write one query. State the grain of every branch before you join it, and name where a naive join multiplies rows.
Approach
- Say the grains out loud first: enrollment is one row per (section, person, role) and repeats per person; attempt is one row per (assignment, person, attempt_no); score is one per attempt. Join all three flat and the row count is enrollment rows multiplied by attempts, so every aggregate above it is weighted twice over.
- Collapse attempts to the policy grain before anything else: SELECT DISTINCT ON (a.assignment_id, at.person_id) a.assignment_id, at.person_id, at.is_late, s.score_given FROM assignment a JOIN attempt at ON at.tenant_id = a.tenant_id AND at.assignment_id = a.assignment_id JOIN score s ON s.tenant_id = at.tenant_id AND s.attempt_id = at.attempt_id WHERE a.tenant_id = $1 AND a.section_id = $2 AND at.state = 'graded' ORDER BY a.assignment_id, at.person_id, s.score_given DESC, at.attempt_no DESC. That ORDER BY is where 'best attempt wins' is written down, and it is the only place it should appear.
- Collapse the roster the same way: SELECT DISTINCT person_id FROM enrollment WHERE tenant_id = $1 AND section_id = $2 AND role = 'learner' AND deleted_at IS NULL AND begin_date <= $3 AND (end_date IS NULL OR end_date >= $3). Now re-enrolment and dual roles cannot multiply anything downstream.
- Join the two collapsed sets, LEFT from assignment so an assignment nobody submitted still returns a row of zeros, and aggregate with FILTER instead of one correlated subquery per column: count(b.person_id) AS learners_graded, avg(b.score_given) AS mean_score, count(*) FILTER (WHERE b.is_late) AS late_count, with learners_assigned taken from the roster cardinality.
- Reject COUNT(DISTINCT person_id) as the fix. It repairs the counts and leaves AVG computed over the multiplied rows, so the mean quietly weights learners who attempted more often, and the number stays plausible for as long as anyone looks at it.
- Keep the cost where it belongs: with an index on attempt (tenant_id, assignment_id, person_id, attempt_no) the DISTINCT ON is an ordered index scan over a bounded set — order 10^2 assignments times order 10^1 to 10^2 learners — rather than a sort of the table. Carry tenant_id into every join condition, not just the outer WHERE.
Follow-up
- A regrade changes score_given after the fact. Does the best attempt change, and what would you order by if the policy were latest attempt instead?
- The instructor opens twelve sections at once. Does this query shape hold, or does the DISTINCT ON become the problem?
- Half the attempts are state='voided' after an integrity review. Where in the query does that belong, and what happens to mean_score?
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.
Worked solution 20 min
- Seed one section with a straight-through learner, a withdraw-and-return learner, a person holding aide and observer, and one soft-deleted row.
- Run the query for a date inside the withdrawal gap and for a date after the return.
- Attempt to insert the second live interval for the returning learner and observe the partial unique index reject it; replace it with the exclusion constraint and retry.
- EXPLAIN the query and confirm the partial index on (tenant_id, section_id, role, begin_date) is used with no filter on soft-deleted rows.
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?
What applications do you know that can trace network packets?
What applications do you know that can trace network packets?
Approach
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Explain the agile process.
Explain the agile process.
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Sign licensed media when the entitlement store is unreachable
Video is delivered as short-TTL signed HLS manifests and segment tokens, and entitlement is checked at signing time because a district's licensed content must not be reachable from another tenant's session. A district-wide video assignment creates a synchronised fan-out on one bell. The entitlement store then becomes unreachable for ten minutes, mid-bell. Design the signing path's behaviour: what is cached and for how long, what the partition does to new grants versus existing ones, the bound on how long a revoked licence stays usable, and what the edge cache key must contain. Justify fail-open or fail-closed for each case.
Approach
- Start from the invariant, because it settles half the argument. An id from one tenant must never resolve under another, which makes an unknown entitlement a denial rather than a coin flip and takes blanket fail-open off the table whatever it costs the period. The real question is what you can pre-decide so that 'unknown' is rare during a partition.
- Replace the per-request lookup with a lease. The entitlement service pushes, or the signer pulls on a schedule, a set of (tenant, content) grants carrying an expiry a few minutes out. During a partition the signer serves from unexpired leases, so a district already licensed keeps playing while a district whose licence was granted during the outage does not: continuation for existing grants, fail-closed for new ones. The staleness you accept is exactly the lease TTL, and it is the same number as your revocation bound, which is the honest way to pick it.
- Distinguish the two revocations. A licence revocation is bounded by the lease TTL, so a five-minute lease means a removed licence can still be signed for up to five more minutes. An already-issued token is bounded by its own TTL, which should be short, tens of seconds for a segment and a minute or two for a manifest, so a leaked URL dies quickly. Do the arithmetic that follows: a 40-minute video at a 60-second token TTL is about 40 re-signs per learner, and 2,000 learners on one bell is roughly 33 signing requests per second sustained across the period.
- Put the entitlement dimension in the edge cache key, not only the asset path. Segments are byte-identical across tenants, so caching on path alone is only safe if the token is checked before the cached object is served and the token itself is tenant-bound. Where the edge cannot enforce that, the key needs a tenant or entitlement-class dimension, and you should quantify the cost: a district-wide bell is one tenant, so per-tenant keys still collapse well, while per-learner keys destroy the hit rate and move the bytes back to origin.
- State what the class actually experiences and what you refuse. With leases, a ten-minute partition is invisible to a district already playing, and a district whose licence is not yet leased gets an explicit unavailable message rather than a 403 mid-video. Justify the asymmetry: a wrong allow is a licensing and privacy incident that leaves no external signal, while a wrong deny is a support ticket with an obvious cause.
Worked solution 40 min
- Write the two failure semantics side by side, unknown-means-allow and unknown-means-deny, and evaluate each against the tenant-isolation invariant and against a bell-time outage.
- Design the lease: its contents, TTL, refresh interval, and what the signer does with one that has expired.
- Compute the re-sign rate for a district-wide video at your chosen segment and manifest TTLs.
- Write the edge cache key and state the hit-rate consequence of every dimension you add to it.
- Describe what the learner sees in three cases: already playing, starting during the partition on an existing licence, and starting on a licence granted during the partition.
Follow-up
- A licence must stop immediately for cause. What is the mechanism, given that the steady-state bound is the lease TTL?
- The lease push is itself what is partitioned, so leases expire during the outage rather than before it. What happens at the moment of expiry, and does your answer change at 08:55 on a Monday?
- What is the edge hit rate for a 2,000-learner district watching one 40-minute video at your chosen token TTL, and where do the re-signing requests land?
School progress report slows tenfold after a repository refactor
A school-wide progress report went from 1.1 s to 9.5 s after a release and now trips a 10 s gateway timeout for the largest schools. Application CPU is flat. pg_stat_statements shows statements per request up from 3 to about 4,900 for a 2,400-learner school, with mean execution time unchanged at 0.2 ms and rows per statement unchanged. Measured database round trip is 1.1 ms. The release introduced a repository that loads enrollments, then fetches the person and the latest attempt per enrollment. Give an ordered diagnostic checklist, the root cause, and the fix.
Approach
- Order the checklist by what the evidence already rules out. First, split the added time between application and database: take a pg_stat_statements delta across the release boundary and compare total_exec_time to the request-time delta. Second, sort that delta by calls, not by total time. Third, only if a single statement dominates do you look at plans or indexes. Here mean_exec_time and rows per statement are unchanged and calls are up 1,600x, so plans and indexes are not the defect and you stop reading EXPLAIN output.
- Confirm the shape by checking what the call count tracks. An N+1 shows one normalised query whose calls scale with learners per school rather than with requests per second, and whose mean_exec_time is small enough that it never appears in a slow-query log. Two such queries here, at roughly 2 per enrollment, matches 4,900 statements for 2,400 learners.
- Do the arithmetic before proposing anything: the cost is serial round trips, 4,900 x (1.1 ms round trip + 0.2 ms execution) = about 6.4 s of the 8.4 s regression. The remaining 2 s is per-row object mapping. This tells you a faster database or a bigger instance cannot recover the time, because almost all of it is waiting.
- Collapse to three statements: one scan of enrollment for the school's sections, one SELECT over person keyed on the distinct person_ids, and one latest-attempt query. Write that third one as SELECT DISTINCT ON (assignment_id, person_id) ... WHERE tenant_id = $1 AND assignment_id = ANY($2) ORDER BY assignment_id DESC, person_id DESC, attempt_no DESC, and hydrate in memory. All three ORDER BY columns descend for a reason: a backward b-tree scan reverses every key column at once, so the existing UNIQUE (tenant_id, assignment_id, person_id, attempt_no) index serves that ordering with no sort node, given the equality predicate on the leading tenant_id that makes it constant. The ordering that reads more naturally, ORDER BY assignment_id, person_id, attempt_no DESC, is mixed-direction and that index cannot produce it -- it costs a full sort, or on PostgreSQL 13+ an incremental sort on the ascending (assignment_id, person_id) prefix. If you want the ascending group order in the plan itself, the price is a second index on (tenant_id, assignment_id, person_id, attempt_no DESC); the report is small enough after grouping that ordering it during hydration is cheaper than carrying that index.
- Get two domain details right in the rewrite. Latest attempt is by attempt_no, not by started_at, because a resumed attempt keeps its original started_at. And every batched statement keeps its tenant_id predicate, because batching by id alone is exactly the shape that lets a repository-level tenant guard be bypassed.
- Add a per-request statement-count budget asserted in an integration test so the next accidental lazy load fails the build rather than a term-end report.
Follow-up
- The report is read far more often than the underlying data changes. What would you materialise, and what event invalidates it?
- A district asks for the same report across 40 schools. Does your three-statement version still hold, and what is the new bound?
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 ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.
What is your experience with Python and machine learning?
What is your experience with Python and machine learning?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
- 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?
How would you support instructors in your role?
How would you support instructors in your role?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Describe a time you were in a group setting and there was a difficult …
Describe a time you were in a group setting and there was a difficult person to work with.
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
What is your experience with building a website using specific tools?
What is your experience with building a website using specific tools?
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
- 01
What is your experience with Python and machine learning?
- 02
How would you support instructors in your role?
- 03
Describe a time you were in a group setting and there was a difficult person to work with.
- 04
What is your experience with building a website using specific tools?
Is this an official University of Toronto interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at University of Toronto. Rounds and questions reflect what candidates have reported, not a process University of Toronto has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult are the technical interviews?
The difficulty varies. Some departments conduct very light technical discussions focused on your resume, while others may require a formal coding assessment. Prepare for the latter by brushing up on good programming practices, such as writing test cases and refactoring.
PracHub interview research ↗What is the best way to stand out to the interviewers?
Be prepared to discuss your projects with passion and clarity. The University of Toronto values candidates who show a genuine interest in the specific project or mission of the department they are applying to.
PracHub interview research ↗Is there a specific culture I should be aware of?
The culture is professional, academic, and highly collaborative. You will be working with experts in other fields, so the ability to communicate technical concepts to a non-technical audience is a significant differentiator.
PracHub interview research ↗How long does the process usually take?
While it can vary, many candidates report receiving follow-ups within a few days of their initial application. The entire process from screen to offer can take a few weeks, depending on the number of stakeholders involved.
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
