The role of a Software Engineer at the University of Oxford is a unique intersection of academic rigor and practical technical innovation. Unlike traditional corporate environments, this position requires you to contribute to complex, mission-critical systems that support world-class research, administrative operations, or educational infrastructure. Your work directly impacts the efficiency and capability of one of the world’s most prestigious academic institutions.
You will likely operate in an environment that values intellectual curiosity, precise problem-solving, and collaborative development. Whether you are building internal tools, managing infrastructure, or supporting data-driven research projects, your contributions are expected to be robust, scalable, and maintainable. This role is ideal for those who enjoy tackling non-trivial technical challenges while working within a highly collaborative, intellectually stimulating community.
The University of Oxford often values deep fundamental understanding over superficial knowledge of specific frameworks, as the work frequently requires long-term reliability and academic-standard documentation.
Application Review
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 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.
Structured Interview
reportedBecause the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Presentation to Panel
reportedA day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
Technical Discussion
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
PracHub editorial advice for the preparation topics above.
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.
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 fixed-width integer arithmetic cannot overflow
In languages with fixed-width integers, including C, C++, Java, Go and Rust, computing a midpoint as (lo + hi) / 2 overflows once the sum passes the type's maximum, so write lo + (hi - lo) / 2 instead. Say which language you are in: arbitrary-precision integers, as in Python or Ruby, remove this specific hazard and none of the others.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
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?
Rank the longest-stuck grade publications in bounded memory
You are streaming score_publication rows to build an operator view: tenant_id, attempt_id, target, state, attempt_count, created_at, next_attempt_at, last_error_code. Up to 20,000,000 rows are in states pending and failed_retryable, spread across up to 5,000 tenants, and they arrive as an unsorted export you may not re-read. Return the 50 longest-stuck publications overall, the 5 longest-stuck per tenant, and a stuck count per tenant. Memory must be bounded independently of row count. State your bounds and the field you rank on.
Approach
- Pick the ranking field before the data structure, because this is where the answer is usually lost. Stuck duration is
now - created_at, not anything derived fromnext_attempt_at. Exponential backoff pushesnext_attempt_atfurther out with every failure, so ordering ascending by it surfaces the publications that just failed once and buries the row that has been retrying for six days — the ranking inverts exactly on the rows the operator opened the page for. - For the global top 50 — the 50 smallest
created_at— keep a max-heap of size 50 keyed oncreated_at. Push each row; when the heap exceeds 50, pop the maximum. The heap head is the youngest survivor, so a row older than the head displaces it and everything else is discarded in O(1) after one comparison. That is O(N log K) time and O(K) space, with the log K term only paid on the shrinking fraction of rows that beat the head. - For per-tenant results keep a map from
tenant_idto a size-5 max-heap plus an integer count. Memory is O(T * K') = 5,000 * 5 entries plus 5,000 counters, a few megabytes, and it is bounded by tenant cardinality rather than row count. Say that out loud: if tenants were unbounded this structure is not, and you would need a sketch or a two-pass job. - Do not sort. A full sort is O(N log N) over 20,000,000 rows and must materialise all of them; the heap never holds more than K. Note that a database
ORDER BY ... LIMIT 50reaches for the same bounded heap internally, so the interesting case is precisely the one stated — a stream you cannot re-read and cannot fit. - Carry the diagnostic fields through the heap entry rather than re-joining afterward. An operator needs
attempt_countandlast_error_codenext to the age to tell a receiver that is rate limiting from one that is rejecting the payload, and a second pass to fetch them defeats the single-pass constraint you just paid for. - Exclude
deadrows from these structures and count them separately. They are terminal by definition, so mixing them in lets a permanently dead publication occupy a slot in the top 50 forever while genuinely stuck rows rotate beneath it.
Follow-up
- Two publications share the same
created_atto the microsecond. What is your tiebreak, and why does a deterministic one matter for an operator page that refreshes? - The operator wants to replay the top 50 safely. What must be true of the idempotency key for that to be a no-op when the original attempt actually succeeded?
- Tenant cardinality rises to 5,000,000. Which part of your structure breaks first and what replaces it?
Diff a roster snapshot against live enrollments in one pass
A tenant's bulk roster snapshot has been normalised into N incoming enrollment records carrying section_id, person_id, role (learner, instructor, aide, observer), source, begin_date and end_date. You also hold M stored enrollment rows for that tenant where deleted_at IS NULL. N and M are each up to 1,000,000. In one pass over the incoming records, classify each as an add, an update or a no-op, and produce the set of stored rows the snapshot never asserts, which becomes deletes_proposed on roster_sync_run. State your time and space bounds in bytes.
Approach
- Fix the key first, because the schema already decided it: the partial unique index is
(tenant_id, section_id, person_id, role) WHERE deleted_at IS NULL, soroleis part of the identity. One human legitimately holds two live rows in a section (instructor and observer), and keying on(section_id, person_id)alone collapses them into one, emitting a spurious update and a spurious delete on every run. - Load the M stored rows into an open-addressed hash map from that 3-tuple to
(enrollment_id, comparable_fields, seen_flag). Packed, the key is 24 bytes and the payload about 8, so at a 0.7 load factor that is roughly 45-50 MB for a million rows; boxed objects in a JVM or CPython map cost 5-10x that, which is the number that decides whether this fits in a worker. - Stream the incoming records and probe once each: miss is an add, hit with equal comparable fields is a no-op, hit with any difference is an update. Set the seen flag on hit. After the stream, the stored entries still unseen are
deletes_proposed; count them and divide by M to get the fraction the gate is evaluated on. - Handle revival explicitly: an incoming record that misses the live map may still match a soft-deleted row. Look that up and clear
deleted_atin place rather than inserting a newenrollment_id, because the partial index constrains only live rows and will not catch the duplicate — the history simply fragments into a new id at every term boundary. - Complexity is O(N + M) expected time and O(M) space. State the alternative and its crossover: sorting both sides and merging is O(N log N + M log M) with O(1) working memory beyond the sort, and it wins as soon as M stops fitting in RAM, since an external sort degrades gracefully where a hash map does not.
- Guard the semantics before any of this runs. Absence means deletion only for a bulk feed; on a delta feed absence is silence and only an explicit
tobedeletedstatus removes anything. A tenant that switches feed modes mid-term will otherwise have one delta file read as a snapshot, which proposes deleting almost everything.
Follow-up
- The upstream system changes its
external_sourced_idscheme, so every incoming row looks new and every stored row looks absent. How do you reconcile without creating duplicate persons? - The apply aborts halfway through. What makes the next run finish rather than double-apply, and what does
snapshot_digestdo for you? - M grows to 50 million for one tenant. Which of your two algorithms do you run, and what changes about the delete detection?
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?
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.
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?
How do you approach modifying a solution when the parameters become mo…
How do you approach modifying a solution when the parameters become more difficult?
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
How would you approach kinematics or circuit-related problems?
How would you approach kinematics or circuit-related problems?
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Verify, deduplicate and order inbound roster-change webhooks
An upstream provisioning system pushes enrollment-change events to your endpoint. It signs each request, retries any non-2xx with backoff, and disables the endpoint after repeated failure, so your status code is a control signal rather than logging. Deliver the verification procedure, the dedupe key and its retention, the rule that decides whether an event is older than the state you already hold, and what you return for a valid event you cannot process yet. Assume events arrive out of order, duplicated, and occasionally replayed hours late.
Approach
- Verify before parsing. Compute the HMAC over the exact raw request bytes, with the signed payload including the sender's timestamp, compare in constant time, and reject outside a tolerance of a few minutes so a captured request cannot be replayed indefinitely. Accept several active keys addressed by a key id in the header, otherwise rotation is an outage.
- Make the signature the admission gate for everything downstream, including logs: these bodies carry student identifiers, and an unverified body that reaches your log pipeline is a body an attacker chose to put there.
- Dedupe on the sender's event id with a unique constraint, retained at least as long as the sender's maximum retry horizon. A duplicate returns the same 2xx as the first delivery, cheaply, without re-running any side effect.
- Separate delivery order from event order. Each event must carry a per-subject version or source timestamp and the apply is conditional, UPDATE ... WHERE stored_version < incoming_version, so a late delivery of an older enrollment state changes zero rows instead of resurrecting a removed learner. If the sender provides no version, you cannot order the events and must re-read the subject from the source API rather than trust the payload.
- Answer fast and process asynchronously: verify, dedupe, durably enqueue, return inside the sender's timeout. Anything slower converts their retry into a second copy of work already in flight. Reserve 5xx strictly for 'I could not durably record it', because that is the only case where a resend helps.
- Push back honestly when saturated: 503 or 429 with Retry-After keeps the sender's backoff working, where accepting events you will later drop looks like success to both sides.
Worked solution 25 min
- Write the verifier in order: read raw bytes, extract key id and timestamp, reject outside tolerance, HMAC, constant-time compare, then parse.
- Create the dedupe table with a unique index on (tenant_id, source, event_id) and make a duplicate insert return the original response instead of raising.
- Implement the conditional apply and assert that delivering version 3 then version 2 leaves the subject at version 3 with zero rows updated on the second call.
- Replay a captured request an hour later and confirm it is rejected on the timestamp tolerance, not merely caught by the dedupe table.
- Table the response codes: verified and enqueued 202, duplicate 200, bad signature 401, malformed 400, saturated 503 with Retry-After.
Follow-up
- The sender rotates signing keys with no overlap window. What does your endpoint do, and what do you ask them for?
- An event arrives for a section you have never seen. Is that 2xx or 4xx, and why does the answer depend on whether they retry 4xx?
- Six months later, how do you prove which events you received and which you rejected?
Attempts marked late an hour early after the clock change
On the Monday after the autumn clock change, instructors in one zone report attempts submitted at 23:10 local flagged late against a 23:59 due time. attempt.is_late is true and frozen. assignment stores due_at_local, policy_time_zone ('America/Chicago'), and a materialised due_at_utc. The affected assignments were published in October and are due in November. Zones without a transition are unaffected. Produce an ordered diagnostic checklist, the root cause, the query that identifies every affected assignment, and the repair, including what learners and instructors see.
Approach
- Order the checklist to separate a read-time bug from stored bad data, because the repairs are completely different. First, confirm is_late is frozen on the attempt rather than recomputed in the report, which the schema already tells you. Second, compare each affected assignment's stored due_at_utc against the value the wall-clock policy implies. Third, check whether the divergence is exactly one hour and confined to one zone. Fourth, check the publish timestamp relative to the transition date. That ordering reaches the answer without reading the submit path at all.
- Name the defect precisely: due_at_utc was materialised at publish by applying the UTC offset in effect at publish time, not the offset in effect at the due instant. An assignment published in October under UTC-5 and due 23:59 on a November date under UTC-6 materialises as 04:59Z instead of 05:59Z, so everything submitted between 23:00 and 23:59 local is one hour past a deadline that is one hour early.
- Write the detection as a comparison against the correct conversion rather than as a guess about which rows are bad: SELECT assignment_id FROM assignment WHERE due_at_local IS NOT NULL AND due_at_utc IS DISTINCT FROM (due_at_local AT TIME ZONE policy_time_zone). In Postgres that expression interprets a naive timestamp in the named zone using the rules in effect at that local instant, which is exactly the computation that was skipped.
- Repair in two passes with different risk. Pass one recomputes due_at_utc for assignments still open, which changes only future decisions. Pass two revisits frozen is_late only for attempts whose assignment's due_at_utc moved and whose submitted_at falls inside the moved window, and emits a grade-change event for each so instructors see a reason rather than a number that changed on its own. Unfreezing every attempt would recompute history that was decided correctly.
- Close the hole permanently rather than fixing the arithmetic once. Keep both the wall-clock policy and the materialised instant, treat the materialised column as a cache with an invariant, and run a nightly assertion that due_at_utc equals the AT TIME ZONE expression for every open assignment. That same assertion catches the other source of drift: a tz database update that changes a zone's rules after you materialised.
- State the two edge cases the policy must handle before they become tickets. A due time inside a spring-forward gap has no instant, and a due time inside a fall-back repeated hour has two; resolution differs across databases and libraries, so reject or normalise such policy times at publish rather than depending on whichever rule the stack happens to implement.
Follow-up
- A section is moved from a school in one zone to a school in another mid-term. Which assignments recompute, and which must not?
- A government changes its zone's transition dates and the tz database ships an update. What does your nightly assertion report the next morning, and what is the approval path for the recompute?
- How would you present a corrected is_late to a learner who already saw a late penalty?
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Practice prompt ↗Practice prompt ↗03Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Practice prompt ↗Practice prompt ↗04Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Practice prompt ↗Practice prompt ↗Worked solution ↗05The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Practice prompt ↗Practice prompt ↗06Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Practice prompt ↗Practice prompt ↗07Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.
How much experience do you have with specific software development lif…
How much experience do you have with specific software development lifecycles?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Tell me about a significant problem-solving experience you have had.
Tell me about a significant problem-solving experience you have had.
Approach
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
What are your general professional passions and strengths?
What are your general professional passions and strengths?
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- 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 did you decide not to do, and why?
How do you keep yourself organized in a fast-paced environment?
How do you keep yourself organized in a fast-paced environment?
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.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
- 01
How much experience do you have with specific software development lifecycles?
- 02
Tell me about a significant problem-solving experience you have had.
- 03
What are your general professional passions and strengths?
- 04
How do you keep yourself organized in a fast-paced environment?
Is this an official University of Oxford interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at University of Oxford. Rounds and questions reflect what candidates have reported, not a process University of Oxford has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long should I prepare for the interview?
Dedicate at least two weeks to reviewing fundamental computer science concepts and preparing your presentation. Focus on being able to explain your past work in detail.
PracHub interview research ↗What differentiates successful candidates?
Successful candidates demonstrate a balance of deep technical knowledge and the ability to communicate their reasoning process clearly. They treat the interview as an intellectual discussion.
PracHub interview research ↗Is the atmosphere formal?
The environment is professional and academic. Expect a respectful, high-level dialogue rather than a fast-paced, high-pressure corporate environment.
PracHub interview research ↗Will I have to do live coding?
You may be asked to review code or solve technical problems on the spot. Practice articulating your thought process as you work through these challenges.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-22