cswork / libraryOpen practice workspace

Interview guides · The College Board

The College Board Software Engineer Interview Guide

Caching practice for The College Board software engineer prep: settle the staleness budget before choosing an invalidation strategy.

The 60-second brief

A Software Engineer at The College Board plays a critical role in driving the technological infrastructure behind some of the nation's most impactful educational programs, including the SAT, Advanced Placement (AP) courses, and BigFuture. The software systems built here enable millions of students, educators, and administrators to register for exams, access scores, and navigate their higher education journeys. Working in this environment requires a unique blend of high-scale systems engineering, rigorous data security, and a deep commitment to user-centric design.

Browser-facing seats are not covered by algorithm practice. Scope in state ownership, what the page does on a slow or failed request, and how you would diagnose something that renders correctly but feels slow.

Focus areas

  • Sandbox untrusted learner code with hard limits
  • Validate a signed launch without trusting the client
  • Make outbound grade publication idempotent and ordered

Overview

In this role, you will contribute to core applications that must remain highly available, secure, and performant, particularly during high-traffic score-release periods. The engineering organization manages complex data pipelines, cloud-native services, and interactive web frontends. As a Software Engineer, your work directly impacts educational equity and access, making your technical contributions both highly challenging and deeply meaningful.

The technical environment is diverse, ranging from robust backend services built on and to modern frontend frameworks like. Engineers collaborate across multidisciplinary product teams to solve complex integration challenges, migrate legacy systems to the cloud, and build intuitive interfaces. Succeeding here means embracing mission-driven engineering while maintaining high technical standards for code quality, scalability, and system reliability.

The The College Board interview process

The College Board candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Application Review

The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.

What to demonstrate

  • Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
  • Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
  • Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year

How to prepare

  • Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
  • Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
  • Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter

Phone Screen

The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.

What to demonstrate

  • Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
  • Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
  • Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural

How to prepare

  • Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
  • Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
  • Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one

Technical Evaluation

Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.

What to demonstrate

  • Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
  • Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
  • Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
  • Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply

How to prepare

  • For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
  • For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
  • Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last

Panel Interview

Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.

What to demonstrate

  • Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
  • Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
  • Whether you establish what decision is on the table before proposing anything
  • Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip

How to prepare

  • Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
  • Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
  • Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it

Where candidates lose points

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.

Editing a content item in place and thereby rewriting the meaning of every historical score.

Authors expect to fix a typo, change a distractor, or adjust a point value, and nothing warns them that thousands of stored results reference the record they are editing. If attempts and analytics join to the mutable item rather than to a version, a regrade or a rebuilt report scores past answers against an item that did not exist when they were given. Versioning has to be the default write path, because a convention that authors must remember will not hold.

Never running a concrete value through the code

Trace one small input and one edge input by hand, index by index, out loud. Re-reading your own code catches design mistakes; walking a real value through it catches the off-by-one, the uninitialised accumulator and the loop that never advances.

Quoting amortised or average cost as if it were a worst-case guarantee

Appending to a dynamic array is amortised O(1), but the append that triggers a resize copies every element, and hash lookup is constant only while the hash spreads the actual keys. Say which guarantee you are offering when the caller cares about the latency of one call rather than the total over many.

Questions & practice

What is the difference between a stack and a queue, and in what scenarios would you choose one over the other?

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • What is the worst case, and how likely is it on real data?

Given an array of intervals representing activities, how would you solve the Activity Selection problem to maximize the number of non-overlapping activities?

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. Walk one small example through your approach before writing the whole thing.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

How do you detect a cycle in a directed graph? Explain the data structures you would use.

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Choose the data structure from the access pattern, not from familiarity.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • Which test case would catch an off-by-one here?

Describe how you would reverse a singly linked list both iteratively and recursively.

Medium · data structures and algorithms

Approach

  1. Walk one small example through your approach before writing the whole thing.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • Which test case would catch an off-by-one here?

What are Python decorators, and how can they be used to modify the behavior of a function or class?

Medium · languages, concurrency and fundamentals

Approach

  1. Name what is shared across threads and what owns each piece of state.
  2. Distinguish a value from a reference to it, and say which one you handed out.
  3. Identify the window where an invariant is briefly untrue.

Follow-up

  • Where could this allocate more than you expect?
  • How would you prove the race exists rather than suspect it?

What are the core features of Java, and how does platform independence work under the hood?

Medium · languages, concurrency and fundamentals

Approach

  1. Identify the window where an invariant is briefly untrue.
  2. Reach for the cheapest primitive that closes the race, not the broadest lock.
  3. Name what is shared across threads and what owns each piece of state.

Follow-up

  • Where could this allocate more than you expect?
  • How would you prove the race exists rather than suspect it?

Stream a quoted roster CSV without loading the file

Easy · parsing · state machine · streaming

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

Worked solution (20 min)

  1. Draw the four-state transition table on paper, including what each state does on comma, quote, CR, LF, EOF and any other byte, before writing a line of code.
  2. Implement it over fixed 1 MB blocks, carrying the in-progress field and the in-progress record across block boundaries.
  3. Build a fixture file containing a field with an embedded comma, a field with an embedded CRLF, a field containing a doubled quote, a leading BOM, and a final record cut off mid-quote.
  4. Assert the parser yields exactly the four good records and then raises on the fifth, naming the byte offset.
  5. Re-run with the block size set to 7 bytes to prove no record depends on a field fitting inside one read.

Expected result: Four records with the embedded comma, embedded CRLF and single literal quote preserved exactly, the first header name free of the BOM, and a raised error identifying the unterminated quoted field with its offset rather than a fifth partial record.

Verify it yourself

  • Output is identical at block sizes of 7 bytes and 1 MB.
  • The record count matches a reference RFC 4180 parser over the same fixture.
  • A file with one stray quote near the start raises within a bounded number of bytes instead of allocating until the process dies.
  • Every yielded record has the same field count as the header, or the reader says which record did not.

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?

Replace per-retry outbox keys with derived ones on a live table

Hard · online migration · unique index · idempotency

score_publication holds 400 million rows and takes continuous writes: (publication_id, tenant_id, attempt_id, target, idempotency_key TEXT NOT NULL, score_given, score_timestamp, state, attempt_count, next_attempt_at). The key was minted as a fresh UUID per retry, so the same grade has several rows and duplicate deliveries have already happened. Migrate to a key derived from (attempt_id, score revision) with UNIQUE (tenant_id, target, idempotency_key), online. Give the ordered steps with the lock each takes, how you handle the existing duplicates, and what you do with rows currently in_flight.

Approach

  1. Add a plain nullable column, derived_key TEXT, with no default: a catalogue-only ALTER held for microseconds. Set lock_timeout to a second or two and retry, because the hazard is the queue rather than the statement — the ALTER waits behind one long reader and every query arriving afterwards stacks behind its pending ACCESS EXCLUSIVE. Resist the elegant version: adding a STORED generated column rewrites all 400 million rows under that same lock on current major versions.
  2. Deploy the dual write before the backfill so every new publication row populates derived_key from attempt_id and the score revision, while the relay still reads the old column. The backfill then chases a closed id range instead of a moving target, and you can bound it.
  3. Backfill in batches by publication_id range, on the order of 20,000 rows per statement, committing between batches, recording the high-water mark in its own table so a killed run resumes rather than restarts, and filtering WHERE derived_key IS NULL so a replayed batch is a no-op. Pace on replica lag and dead tuples, not CPU: each UPDATE writes a new row version and WAL volume tracks rows touched.
  4. Resolve the duplicates as a data decision, not a DELETE. Group by (tenant_id, target, derived_key) having count(*) > 1; within each group keep the row in state='succeeded' if one exists, since that is the delivery whose outcome is known, otherwise the newest 'pending'. Move the rest to state='dead' with last_error_code recording the migration, and keep the rows — support has to be able to explain a grade that posted twice, and a deleted row explains nothing.
  5. Leave in_flight rows alone and drain them. A row in_flight has an outstanding request whose outcome is unknown, so collapsing it either loses a delivery record or races the relay marking a row you just rewrote. Pause the claim loop or wait out the lease, assert zero in_flight for the affected keys, then finish the group.
  6. Build the constraint without a blocking scan and expect the first attempt to fail: CREATE UNIQUE INDEX CONCURRENTLY, which aborts if any duplicate remains and leaves an index with indisvalid = false that must be DROP INDEX CONCURRENTLY'd rather than reused. Once it builds clean, ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX adopts it under a brief ACCESS EXCLUSIVE without rebuilding — which is a second reason derived_key is a real column, since USING INDEX accepts neither an expression index nor a partial one.

Worked solution (45 min)

  1. Rehearse on a 10-million-row copy under concurrent relay load, measuring per-batch duration, WAL generated, and replica lag.
  2. Deploy the dual write, then run the batched backfill and kill it mid-run to confirm it resumes from the high-water mark with no batch applied twice.
  3. Run the duplicate-group query, classify each group by whether it contains a succeeded row, and apply the supersede update with in_flight rows excluded.
  4. Attempt CREATE UNIQUE INDEX CONCURRENTLY, observe the failure on any remaining duplicate, drop the invalid index, resolve, and rebuild.
  5. Adopt the index with ADD CONSTRAINT ... UNIQUE USING INDEX and switch the relay to claim on derived_key.

Expected result: A unique constraint on (tenant_id, target, idempotency_key) over 400 million rows, built with no statement holding ACCESS EXCLUSIVE for more than milliseconds, every historical duplicate retained in a terminal superseded state rather than deleted, and no in_flight row rewritten while its request was outstanding.

Verify it yourself

  • SELECT count(*) FROM score_publication WHERE derived_key IS NULL reaches exactly zero before the index build is attempted.
  • pg_index.indisvalid is true for the final index; an invalid one means the concurrent build failed and must be dropped, never left in place.
  • The count of rows moved to state='dead' by the migration equals the duplicate count computed beforehand, and no row was deleted.
  • During the backfill, relay inserts from another session return in single-digit milliseconds and no session waits on a relation lock beyond lock_timeout.

Follow-up

  • score_timestamp must strictly increase per line item and learner. Does this migration change what the receiver sees, and what would a stale retry do mid-migration?
  • A tenant's external endpoint has been failing for a week and has 40,000 rows queued. What does your duplicate collapse do to that backlog?
  • How do you prove afterwards that no grade was delivered twice as a result of the migration itself?

Enforce an attempt cap that two browser tabs cannot defeat

Medium · isolation levels · phantom reads · idempotency

attempt carries UNIQUE (tenant_id, assignment_id, person_id, attempt_no), and assignment.max_attempts SMALLINT. Starting an attempt today reads the learner's existing attempts, computes the next attempt_no, and inserts a row with server_deadline_at = now() + time_limit_seconds. A learner clicks Start in two tabs five milliseconds apart under READ COMMITTED. Give the statements each transaction runs, name the anomaly precisely, and give two fixes: one that changes isolation level and one that is a single conditional statement. Say what the losing path must do about server_deadline_at.

Approach

  1. Trace the interleaving. Under READ COMMITTED each statement takes a fresh snapshot, and both SELECTs run before either INSERT commits, so both transactions see two existing attempts and both compute attempt_no = 3. The unique index rejects one with SQLSTATE 23505, which feels like the cap held; then the retry re-reads, computes 4, and inserts it. The cap is a predicate over rows that do not exist yet — a phantom — and uniqueness on a different key cannot enforce it.
  2. Isolation fix: run the whole start at SERIALIZABLE. Postgres's serializable snapshot isolation takes predicate locks over what the count read and detects the read-write conflict when both transactions insert into that range, aborting one with SQLSTATE 40001. The preconditions matter: every transaction touching that predicate must also be SERIALIZABLE, and the caller needs a retry loop, which is affordable here because the conflict scope is one learner on one assignment.
  3. Single-statement fix, and the cheaper one: put the counter on a row and make the check part of the write. UPDATE attempt_allocation SET attempts_used = attempts_used + 1 WHERE tenant_id = $1 AND assignment_id = $2 AND person_id = $3 AND attempts_used < max_attempts RETURNING attempts_used — zero rows means the cap is reached. Under READ COMMITTED the blocked second UPDATE re-evaluates its WHERE against the updated row after the lock is released, so it correctly returns nothing; the identical statement under REPEATABLE READ raises 40001 instead of re-evaluating, so the retry loop is mandatory there and unnecessary here.
  4. Handle the double click as what it is — one intent — with idempotency rather than concurrency control: accept a client-supplied start key and add UNIQUE (tenant_id, assignment_id, person_id, start_key). The second tab conflicts, the handler returns the existing attempt, and the cap never sees two requests at all. Concurrency control is the backstop for genuinely distinct starts.
  5. Freeze server_deadline_at at the winning insert and make the losing path return the winner's stored value. Recomputing now() + time_limit_seconds on the loser, or on any subsequent resume, hands the learner a fresh clock on every reload, which is the precise mechanism by which a timed assessment leaks time.
  6. Say what you would not do: SELECT ... FOR UPDATE on the assignment row serialises every learner in the section behind a single row at the bell, converting a per-learner conflict into a section-wide queue at exactly the wrong moment.

Follow-up

  • A grader voids attempt 2. Does the cap free a slot, and what does the next attempt_no become?
  • How many times do you retry on 40001, with what backoff, and what does the learner see while it retries?
  • The allocation row is now a second write on the hottest path of the day. At what section size does it become the contended thing, and what would you do then?

Describe the difference between a primary key and a foreign key, and how they enforce referential integrity.

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. State your assumptions explicitly before working the problem.
  3. Say what you would check first and why it is the highest-information step.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Shape the launch bootstrap response for one round trip

Medium · response shape · caching · cache invalidation · stampede

At the start of a class period the embedded frontend needs, before first paint: the person's role in the section, the assignment policy, their current attempt if one exists, and the content manifest URL. Today that is four GETs. The caller renders inside someone else's page, so a failure is a blank frame, and it retries twice before giving up. Design one bootstrap response: its shape, what is embedded versus linked, its cache-control and validators, and how an enrollment change invalidates it. Tens of thousands of these arrive within sixty seconds.

Approach

  1. Decide embed versus link by lifetime, not convenience. Policy and role change on human timescales and are safe to embed; the attempt mutates every twenty seconds and must be linked or embedded with its own version so a stale copy is detectable; a short-TTL signed media URL must not sit in a cacheable envelope at all, because the signature's expiry and the envelope's max-age are different clocks and the shorter one wins.
  2. Split into two documents along the varies-by boundary rather than shipping one: a section-scoped envelope identical for every learner in the section, and a small private per-person document carrying role and the attempt handle. One shared fill amortised over thirty learners is the difference between thirty cold origin reads per section at the bell and one.
  3. Key and label them honestly. The per-person document is Cache-Control: private with a cache key carrying tenant, section and person; the shared document may be cached more widely only because it contains no person id, no role and no signed URL. A shared cache key that forgets the tenant dimension is a cross-tenant leak that no test will catch, since the response it serves is entirely plausible.
  4. Derive the ETag from the underlying versions (content_version_id, assignment updated_at, and a section roster generation counter) so a conditional request answers 304 without re-rendering. Bumping the generation on the enrollment-changed event invalidates every derived key at once, which is what makes invalidation a single write instead of an enumeration of keys.
  5. Protect the cold key: single-flight per key plus a short stale-while-revalidate window. Thirty thousand launches in sixty seconds against a cold shared key is thirty thousand identical origin reads, and the step is too short for a reactive autoscaler to help.
  6. Design partial failure into the shape. If the content service is slow but the attempt store is healthy, return what you have with an explicit per-part status rather than a 500; the frontend can render the assignment and poll for the rest, where a 500 leaves it nothing to draw inside a frame it does not own.

Worked solution (30 min)

  1. List every field first paint needs and label each with its lifetime and its varies-by dimensions (tenant, section, person, role, content version).
  2. Cut along that boundary: anything varying by person leaves the section-scoped document, and anything with its own expiry leaves the cached envelope.
  3. Write the ETag input tuple and prove that a due-date edit, a content republish and an enrollment change each change it.
  4. Do the arithmetic for the step: thirty thousand launches over sixty seconds across roughly a thousand sections is about a thousand shared fills plus thirty thousand small private reads, against a hundred and twenty thousand reads for the four-GET version.
  5. Add single-flight and measure origin reads with five hundred concurrent requests against one deliberately cold key.

Expected result: Two documents, a section-scoped ETag-validated envelope with a defensible max-age and a small private per-person document, a cache key carrying every varies-by dimension, invalidation by a section generation counter, and single-flight on the shared key, so first paint costs one round trip plus at most one conditional revalidation.

Verify it yourself

  • Five hundred concurrent cold requests for one section produce one origin fill, not five hundred.
  • Changing an enrollment changes the ETag of the affected documents without deleting keys one by one.
  • No response body that a shared cache may store contains a person id, a role, or a signed URL.

Follow-up

  • A learner opens the same assignment in two tabs. Which part of your response must not be cached in the browser, and what breaks if it is?
  • Give max-age a number and defend it against an instructor who edits the due date two minutes before the period starts.
  • What does the bootstrap return for a person holding two roles in the same section?

Autosave sequence goes backwards and loses learner work

Hard · lost update · read-modify-write · isolation levels · concurrency

About one attempt in 3,000 loses roughly 40 seconds of learner work. The audit log shows attempt.last_autosave_seq decreasing from 57 to 56 at 10:14:03.118. It only happens for learners with two tabs open or a second device, and it never reproduces in staging. The autosave handler reads last_autosave_seq, compares it to the incoming sequence in application code, then updates the payload and the sequence by primary key. The database runs READ COMMITTED. Give an ordered diagnostic checklist, the root cause, a deterministic reproduction, and the fix.

Approach

  1. Order the checklist so the decisive observation comes first. A monotonic counter that decreases is not a client problem and not a timing coincidence; it means two writers both decided they were the newest. So: first, confirm the two writes are close in time and from different sessions on the same attempt. Second, read the handler for a read-then-write across two statements. Third, check the isolation level and the WHERE clause of the write. Only after those would you look at the client's sequence generation, and here you will find it correct.
  2. State the interleaving exactly. Requests carrying sequences 56 and 57 arrive 8 ms apart from two tabs. Both SELECT and read 55. Both pass the application-level guard. The write for 57 commits first; the write for 56 then acquires the row lock, and because its WHERE clause names only the primary key it has nothing left to check, so it overwrites both payload and sequence. Final state is 56 with the older payload. The guard was evaluated against a snapshot that the write did not re-validate.
  3. Make it deterministic before fixing it, because a fix you cannot fail is a fix you cannot trust. Add a test hook that sleeps between the SELECT and the UPDATE, drive two concurrent autosaves from separate connections with a barrier, and the 1-in-3,000 becomes 1-in-1. The rarity was only the width of the window.
  4. Fix by moving the guard into the predicate of the write: UPDATE attempt SET payload = $2, last_autosave_seq = $3 WHERE attempt_id = $1 AND tenant_id = $4 AND last_autosave_seq < $3, and treat zero rows affected as a rejected stale write rather than an error. This is a single statement, so there is no window.
  5. Say what the engine does with that statement, because this is where candidates hand-wave. Under READ COMMITTED in Postgres, an UPDATE that finds a row a concurrent transaction has just committed re-evaluates its WHERE clause against the new row version and skips the row if it no longer matches, so the seq guard holds without explicit locking. Under REPEATABLE READ or SERIALIZABLE the same statement raises a serialization failure instead and the client retries, after which it sees the newer row and correctly rejects. Both outcomes are correct; the engine-independent principle is that the comparison must be in the write, not between two statements.
  6. Reject rather than merge, per the attempt's contract: a lower sequence is stale by definition and merging two autosave payloads invents state the learner never had. Return a response that tells the losing tab it is behind so it can re-read rather than keep sending. The same shape covers the neighbouring race: two concurrent submits both computing attempt_no + 1 are one bug, and the UNIQUE (tenant_id, assignment_id, person_id, attempt_no) constraint is one remedy, with the unique violation read as 'the other request won' rather than as a failure.

Follow-up

  • The losing tab keeps autosaving with sequences the server rejects. What should it do, and what should the server return so it can tell 'stale' from 'failed'?
  • Make submit idempotent on a client-supplied key. What is the outcome when a second submit arrives after grading has started?
  • This attempt is timed. How do you keep the server deadline monotonic when the same learner resumes on a second device?

Suggested practice checklist

For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.

Day 1 — Numbers before diagrams

  • Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
  • Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
  • Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.

Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.

Day 2 — One system, from requirements to schema

  • Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
  • Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
  • Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.

Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.

Day 3 — The consistency you are actually buying

  • Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
  • Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
  • Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.

Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.

Day 4 — Failure is the design

  • For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
  • Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
  • Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
  • Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.

Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.

Day 5 — Scaling the hot path

  • Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
  • Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
  • Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
  • Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.

Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.

Day 6 — Keep the coding hand in, at the bar that applies to you

  • Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
  • Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
  • Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.

Deliverable: One problem hardened to review standard, with its test list and one written review comment.

Day 7 — Defend it while being interrupted

  • Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
  • Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
  • Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.

Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.

Know where your numbers came from

Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.

How do you handle class-based exceptions in a multi-threaded application to prevent resource leaks?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Name the disagreement and how you resolved it with evidence.

Follow-up

  • How did you know your change caused the improvement?
  • What did you decide not to do, and why?

How do you handle conflicting requirements from stakeholders or team members when designing a software feature?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Give the blast radius: what could have broken, and what you measured.
  3. 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?

Reverse a deletion gate that operators stopped reading

Hard · reversing a decision · destructive change · operator trust · thresholds

You shipped a gate holding any roster run that proposes to soft-delete more than five percent of a tenant's active enrollments, recorded on roster_sync_run.gate_state as held_for_review. In the first two weeks of a term, when withdrawals are genuinely large, it fired on roughly forty percent of runs and operators began approving without opening the diff. Describe reversing a decision of this shape: the evidence that changed your mind, what you replaced it with, and how you avoided reversing into the failure the gate existed to prevent.

Approach

  1. Lead with the measurement that reversed you, not with the discomfort. Hold rate, and more importantly the distribution of time between a hold and its approval. A control approved in a median of a few seconds is not a control, it is a log line with a button, and that number is the argument. Say plainly that the gate was still technically doing its job and had already stopped being a defence.
  2. Separate the two situations the single threshold cannot tell apart. The gate exists to catch a truncated file or a changed identifier scheme; it is not meant to catch a real withdrawal wave. Both present as many rows missing, so proportion alone is the wrong discriminator. The signals that do separate them are rows_in against the tenant's trailing snapshot sizes, whether missing persons have same-shaped replacements present, and whether deletions cluster on whole orgs or spread thinly across sections.
  3. Replace one threshold with a small set of rules that keeps the fail-safe direction. Hold unconditionally when rows_in collapses against the trailing median or falls below an absolute floor, hold when a large share of missing sourcedIds have plausible replacements in a new format, auto-apply when the run looks like the tenant's own history. Keep the hold non-destructive: deletes are retained, the run stays restartable from snapshot_digest, and nothing is discarded by a rejection.
  4. Guard the reversal with a measurement rather than confidence. Run the old rule in shadow, log disagreements, and track hold precision, meaning of the runs held how many were genuinely bad, alongside hold rate. Loosening a safety control is only defensible if you can show it was loosened where it was wrong, and a shadow comparison is what turns that from an assertion into a number.
  5. Say what would reverse you again and when you look. Term-start weeks are the regime where this rule earns or loses its keep, so the review cadence is per term, not quarterly. Then name the original mistake without softening it: the threshold was designed and validated against the forty-six quiet weeks and met its real workload for the first time in production.

Follow-up

  • A district legitimately closes a school mid-year and every enrollment under one org ends. Does your rule hold it, and is that the behaviour you want?
  • What does an operator see that lets them decide in under a minute, and what would make them read it rather than approve it?
  • A run was approved that should not have been. What does unwinding it touch beyond the enrollment rows?