As a Software Engineer at the University of Calgary, you are a critical bridge between complex research data and actionable digital solutions. Whether working within the Cumming School of Medicine or other specialized academic departments, your work directly impacts the ability of researchers, faculty, and staff to manage, analyze, and disseminate vital information.
This role is unique because it combines standard software development practices with the specific needs of an academic environment. You will often find yourself building custom database architectures, designing research-oriented tools, or developing software that supports curriculum delivery. Success in this role requires not only technical proficiency but also the ability to translate non-technical academic requirements into robust, scalable, and user-friendly software products.
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
Interview with Hiring Manager
reportedThe design portion here is shorter and lower-stakes than a dedicated design round, which changes what it tests. There is rarely time to reach a full component diagram, so what gets read is your first two minutes: whether you pin down constraints, meaning request rate, data size, what must not be lost and how stale a read may be, before naming any technology. Opening with a stack list invites being steered back. Once the numbers are on the table, say what breaks first if they grow tenfold, and defend the plain option where the load does not justify more.
What to demonstrate
- Whether constraints come before components: peak request rate, data volume, what must survive a process dying, and the staleness the product can tolerate
- Whether you can name what saturates first when traffic grows by an order of magnitude, and whether that matches the design you just sketched
- Whether a cache is reasoned about on both paths, since a cold or recently flushed cache sends the full request rate to the origin, so capacity has to cover the miss case and not only the steady state
- Whether you distinguish what you have operated from what you have only read about, which usually shows up in the answer to why a particular component is there
How to prepare
- Take one system you worked on and write down the numbers you would need to defend it: requests per second at peak, rows in the largest table, the latency you were actually held to. Not having them is the common stall in this part.
- Practise the tenfold question on that system out loud, naming the first bottleneck you would hit, whether that is a single writer, connection limits, disk, or a queue that grows faster than it drains, and the smallest change that buys headroom.
- Prepare one decision where the plain option was correct: the cache you did not add or the queue you did not introduce, with the load figure that made that the right call. Being able to argue for less is rarer than being able to argue for more.
Technical Screening
reportedThe same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
PracHub editorial advice for the preparation topics above.
Autoscaling on average utilisation against a bell-shaped step function.
Scale-up latency (scheduling, image pull, process and JIT warm-up, connection pool and cache fill) is typically tens of seconds to minutes, while the demand step is under a minute and repeats on a published schedule. By the time the metric crosses a threshold, the period has started and the queue has already built. The workable answer is a schedule-driven pre-warm derived from the tenant calendar plus a load shedder that protects writes, not a more aggressive threshold.
The launch works locally and breaks inside the embedding page, because the session cookie is a third-party cookie.
A standards-based launch finishes with a cross-site form POST back to the tool, so the state and session cookies must carry SameSite=None; Secure or the browser will not send them at all: the common Lax default is only released on top-level GET navigations, never on a cross-site POST. Correct attribution is not sufficient either, and the two browser behaviours fail differently. An engine that blocks third-party cookies outright never stores the cookie, so the state check fails on every launch; an engine that partitions them instead keeps the launch working but scopes that session to the one embedding site, so the session established inside the frame is not the session the same user has when they open the tool directly. Local development is same-site, so all of this passes and the failure surfaces only in the embedded context as a generic 'invalid state' error. The fix is a designed fallback rather than a retry: request storage access, or break out to a first-party window for the first launch and bind the session there.
Optimising an axis nobody named
Ask which resource is actually scarce here: wall-clock latency, throughput, memory footprint, cost per request, or engineering time. Shaving a constant factor off an in-memory step is wasted effort when the same function makes a blocking remote call inside the loop.
Going silent while thinking
Narrate the candidates and why you are discarding them, even in fragments: sorting first would make this a two-pointer scan, but it destroys the original indices, which the output needs. From the other side of the table, a candidate thinking hard and a candidate stuck are indistinguishable until one of them speaks.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Stream a quoted roster CSV without loading the file
You are handed a 2 GB roster export as a byte stream. It follows RFC 4180: any field may be quoted, a quoted field may contain commas and CRLF, a literal quote inside a quoted field is written as two quotes, and the file may open with a UTF-8 byte order mark. There are up to 10,000,000 records and a single field may reach 64 KB. Write a reader that yields one record at a time without buffering the whole file and that fails loudly on a malformed file rather than emitting a short record. State your bounds.
Approach
- Refuse the shape that fails first: splitting the stream on newlines and then splitting each line on commas. A quoted field may contain a CRLF, so line splitting cuts records in half, and the damage is silent because both halves parse into plausible short records.
- Write a byte-level state machine with four states — field start, unquoted field, quoted field, and quote-seen-inside-quoted. In the last state a second quote emits one literal quote and returns to quoted; a comma or newline ends the field; anything else is a malformed file. That table is the whole parser and it is the part to get right on paper before typing.
- Strip the BOM only at offset zero, comparing the first three bytes against EF BB BF. Left in place it becomes part of the first header name, so the header lookup for that column misses and the column reads as absent for every record in the file.
- Cap field length at the stated 64 KB and record length at a sane multiple of it. Without a cap, one unbalanced quote makes the parser treat the remaining 2 GB as a single field and the process dies of memory exhaustion rather than telling you the file is broken at byte 12,004.
- Treat end of stream inside a quoted field as a hard error, not an implicit close. A truncated upload is the most common malformed input here and it is byte-for-byte indistinguishable from a complete file if you close the field silently — the missing rows then present downstream as a mass withdrawal.
- Complexity: O(n) time in bytes with one pass and no backtracking, and O(longest field + longest record) space, which is bounded by the caps rather than by the file. Read in fixed blocks and keep the partial field across block boundaries instead of reading line-wise.
Follow-up
- The file arrives gzipped and you must report progress as a percentage. What can you actually report, and what does that do to your memory bound?
- Two exporters disagree on line endings and one emits a bare LF inside a quoted field. Does your state machine care, and should it?
- How do you distinguish a truncated file from a complete one when the byte stream itself gives you no signal?
Validate an org hierarchy and resolve inherited entitlements
You hold a tenant's normalised org rows from one snapshot: org_id, parent_org_id (nullable), tenant_id, and a boolean saying whether that node carries an explicit content entitlement. There are up to 100,000 nodes and the feed is not trusted, so parent links may form a cycle, reference a node that does not exist, or point at another tenant's org. Reject the snapshot if the links do not form a forest inside one tenant, naming the offending nodes. Otherwise return, for every node, its nearest entitled ancestor including itself, or none.
Approach
- Validate the edge set before traversing anything. Each node has at most one parent, so the graph has at most V edges; reject any edge whose target is missing from the snapshot, and reject any edge whose target carries a different
tenant_id. The cross-tenant edge is the security-relevant one — it makes one district inherit another district's licensed content, which is the tenancy invariant failing through a data path rather than a query. - Detect cycles by reachability rather than by coloured recursion. Collect the roots (
parent_org_id IS NULL), build a child adjacency list in O(V), and run an iterative traversal from the roots. Any node left unvisited is on or below a cycle, because a finite functional graph where every node has one parent decomposes into trees hanging off cycles. To name a cycle, start from any unvisited node and follow parents into a visited set until a node repeats. - Make the traversal iterative with an explicit stack. A well-formed hierarchy is three or four levels deep, but a corrupt feed can produce a chain of 100,000 nodes, and recursive descent dies on it — CPython stops at a recursion limit of 1,000 by default, and a JVM thread's default stack overflows in the tens of thousands of frames. The failure arrives as a crash during ingest of a customer file, which is the worst place to learn this.
- Resolve entitlements on the way down, not on the way up. Push
(node, nearest_entitled_so_far)onto the stack; at each node the value is the node itself when it carries an explicit entitlement and the inherited value otherwise. One pass, O(V + E) time and O(V) space, no memo table needed. - State the alternative you rejected and why. Walking upward per node is O(V * depth), degrading to O(V^2) on the pathological chain the validator is there to catch; memoising the upward walk recovers O(V) amortised but needs a second map and still re-enters nodes. Top-down is strictly simpler here because you are already traversing for the cycle check.
- Report failures as a set, not the first one. Roster operators fix files in batches, so returning every missing parent, every cross-tenant edge and one representative node per cycle in a single pass saves a round trip per defect.
Follow-up
- A node's parent changes between snapshots, moving a school to a different district. What must be recomputed, and what must not change about attempts already taken under it?
- Entitlements become time-bounded rather than boolean. How does the top-down pass change, and what is the new answer type?
- The snapshot is valid but 40 percent of nodes changed parent in one run. Is that a defect, and which gate decides?
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.
Worked solution 25 min
- Write the key tuple down before any code and check it against the partial unique index in the schema, not against intuition about what a class roster is.
- Build the stored map with a
seenbit per entry, then run the incoming stream through a single probe per record, appending to three lists: adds, updates, no-ops. - Sweep the map once at the end for unseen entries to get
deletes_proposed, and compute the fraction against the live count captured before the run started. - Feed a fixture with a person holding learner and observer rows in one section, a row whose
end_datealone changed, a previously soft-deleted row that the snapshot asserts again, and one genuinely absent row. - Re-run the same fixture end to end and confirm every count except
rows_inis zero the second time.
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?
Reconstruct paused and active time from an append-only event table
attempt_event is append-only: (attempt_id, seq BIGINT monotonic per attempt, event_type IN ('start','pause','resume','autosave','submit'), occurred_at TIMESTAMPTZ, client_ts TIMESTAMPTZ). attempt carries started_at, server_deadline_at, accumulated_pause_seconds and submitted_at. Write one query returning, per attempt in a section, total paused seconds and active seconds between start and submit, and flagging attempts whose active time exceeds the assignment's time limit by more than 60 seconds. A pause may have no matching resume. State which ordering you use and which window frame, and why.
Approach
- Order by seq, not by a timestamp. seq is monotonic per attempt by construction; occurred_at is server-receive time and ties under a burst, and client_ts is attacker-controlled on a device the learner owns. Ordering on a column with ties silently reorders a pause and its resume.
- Close each pause with the next 'resume' or 'submit', not with the next event of any type, or an autosave landing during a pause ends the interval two seconds in. Either filter to the closing event types in a CTE and then LEAD over that reduced set, or keep every row and use min(occurred_at) FILTER (WHERE event_type IN ('resume','submit')) OVER (PARTITION BY attempt_id ORDER BY seq ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING). The second form works because FILTER is permitted on an aggregate used as a window function; it is not permitted on lead(), which is a window function and not an aggregate, so lead(...) FILTER (...) is a syntax error rather than a subtle bug.
- Define the unmatched pause explicitly instead of letting NULL decide: close it at submitted_at, and where there is no submit, at least(server_deadline_at, now()). An interval closed at now() makes the number change on every run, so pin it for any stored report and say so in the output.
- Aggregate with sum(EXTRACT(EPOCH FROM (closed_at - occurred_at))) FILTER (WHERE event_type = 'pause') GROUP BY attempt_id, then active = EXTRACT(EPOCH FROM (submitted_at - started_at)) - paused. Where you use a running window anywhere in this query, write the frame: with ORDER BY and no frame clause the default is RANGE UNBOUNDED PRECEDING AND CURRENT ROW, which includes every row tied on the ordering key — harmless on a unique seq, which is exactly why the tiebreak is the load-bearing decision.
- Reconcile the computed total against attempt.accumulated_pause_seconds and treat a disagreement as a finding, not as a rounding difference: it means either an event was lost, or a code path changed the counter without emitting an event. The event table is the auditable side, so it wins in the report and the counter gets repaired from it.
- Support the access with an index on attempt_event (attempt_id, seq) so each partition is an ordered range scan; then confirm no Sort node sits above it, since a sort here is O(E log E) over every event in the section rather than a walk.
Worked solution 30 min
- Seed one attempt with start, autosave, pause, autosave-during-pause, resume, submit, and a second attempt whose pause has no resume.
- Write the closing-timestamp expression both ways — a filtered CTE with LEAD, and the min(...) FILTER window with an explicit ROWS frame — and confirm they agree.
- Add the unmatched-pause fallback and re-run against the second attempt.
- Compare the computed paused seconds against attempt.accumulated_pause_seconds for every attempt in the section and list the disagreements.
Follow-up
- The same event is delivered twice with the same seq. What does your query do, and what constraint should have prevented it?
- Two tabs produce interleaved events for one attempt. How do you detect the interleaving from this table, and what does it do to the pause arithmetic?
- How would you use this output to prove a learner did not gain time by reloading?
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?
What type of data formats have you worked with in the past?
What type of data formats have you worked with in the past?
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Can you walk us through your most significant previous projects?
Can you walk us through your most significant previous projects?
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Fix an attempt-start endpoint whose timeout retry burns an attempt
POST /assignments/{id}/attempts allocates the next attempt_no for the caller, copies content_version_id onto the row, and computes server_deadline_at once from time_limit_seconds. The caller is the learner's browser on school wifi; it retries once when a request times out. With max_attempts = 2, a timed-out create followed by that retry consumes both attempts and hands the learner a fresh deadline. Redesign the endpoint so a retry is safe, a deliberate second attempt is still possible, and two tabs cannot each create one. Give the request shape, the storage, and the response to a replay.
Approach
- Say why the naive shape fails rather than patching it: POST-creates-a-child is non-idempotent by construction, and here the duplicate is worse than the original because it spends a countable resource (an attempt_no against max_attempts) and re-derives server_deadline_at, handing back time.
- Reject start-or-resume as the fix. Returning any existing in_progress attempt makes the retry safe but makes a legitimate second attempt impossible once the first is submitted, so the two cases have to be distinguished by something the client sends, not inferred from server state.
- Take a client-supplied idempotency key (an Idempotency-Key header, currently an IETF draft rather than a published RFC, or an explicit body field) scoped as (tenant_id, assignment_id, person_id, key). Store the key with a fingerprint of the semantic request and the created attempt_id in the same transaction that creates the attempt, so a unique index decides the race rather than application logic.
- Define replay precisely: same key and same fingerprint returns the original attempt_no and the original server_deadline_at, never a recomputed deadline. Same key with a different fingerprint is a 422 and creates nothing, because that is a caller bug and silently minting a new attempt hides it.
- Handle the concurrent loser explicitly. A second tab racing the first hits the unique constraint mid-flight, so the key row needs an in-flight state: the loser gets either the winner's completed result or an explicit retry-shortly response, never a 500 surfaced from a constraint violation.
- Set key retention against the client's real retry horizon plus clock skew. Expiring keys after an hour while an offline client drains its queue the next morning reintroduces the exact bug you removed.
Worked solution 30 min
- Write the DDL for attempt_idempotency(tenant_id, assignment_id, person_id, key, request_fingerprint, attempt_id, state, created_at) with a unique index on the first four columns.
- Implement create as one transaction that inserts the key row and the attempt row together; on unique violation, read the existing row and branch on its state and fingerprint.
- Run the same request twice sequentially and assert one attempt row, with identical attempt_no and identical server_deadline_at in both responses.
- Run two requests concurrently and assert one attempt row, with the loser receiving the winner's result or an explicit in-flight status rather than a 500.
- Send the same key with a changed assignment_id and assert a 422 and zero new rows.
- Submit attempt 1, send a fresh key to create attempt 2, then send a third key and assert max_attempts refuses it.
Follow-up
- The learner submits attempt 1 and legitimately starts attempt 2. Where does the new key come from, and what stops the client reusing the old one?
- A replay must not reset the deadline. Write the test that fails if someone later moves the deadline computation into the response serializer.
- What status and body do you return while the key is still in flight, and what is the caller supposed to do with it?
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?
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Practice prompt ↗Practice prompt ↗Worked solution ↗02References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Practice prompt ↗Practice prompt ↗03Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Practice prompt ↗Practice prompt ↗04Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Practice prompt ↗Practice prompt ↗06Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Practice prompt ↗Practice prompt ↗07Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
How do you adapt your technical approach when working with non-technic…
How do you adapt your technical approach when working with non-technical stakeholders?
Approach
- Give the blast radius: what could have broken, and what you measured.
- 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.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Can you describe a time you had to explain a complex technical concept…
Can you describe a time you had to explain a complex technical concept to a non-technical colleague?
Approach
- Close with what you would do differently, concretely.
- 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
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
How do you handle the transition from project requirements to implemen…
How do you handle the transition from project requirements to implementation?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
How do you interact with team members who have different levels of exp…
How do you interact with team members who have different levels of experience or communication styles?
Approach
- Give the blast radius: what could have broken, and what you measured.
- State the situation in two sentences and spend the rest on the reasoning.
- 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?
- 01
How do you adapt your technical approach when working with non-technical stakeholders?
- 02
Can you describe a time you had to explain a complex technical concept to a non-technical colleague?
- 03
How do you handle the transition from project requirements to implementation?
- 04
How do you interact with team members who have different levels of experience or communication styles?
Is this an official University of Calgary interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at University of Calgary. Rounds and questions reflect what candidates have reported, not a process University of Calgary has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the interview process take?
The process is generally efficient, often spanning a few weeks from the initial screening to a final decision.
PracHub interview research ↗What is the most important thing to prepare?
Be prepared to talk about your past projects in great detail, specifically focusing on the technical choices you made and why you made them.
PracHub interview research ↗Is there a coding test?
While formal whiteboard coding challenges are less common, you should be ready to discuss your coding philosophy and demonstrate your understanding of fundamental programming concepts during your conversation.
PracHub interview research ↗What is the culture like?
The culture is typically collaborative and intellectually driven, focusing on supporting the university's research and educational objectives.
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