Extending an existing codebase live
Find the nearest existing feature, build yours in its shape through the APIs as they are, and say improvements instead of making them.
Predict first
Before you read: predict
Pair round, 45 minutes, on a seeded ExamHall service. The interviewer says: “Add ‘extend time for one candidate’. Don't change the timer module's public API.” When you finish, they will check that one candidate gets ten more minutes and one candidate.extended audit entry, and that an unknown candidate gets 404. The timer module exports startTimer, remaining, addTime, pause and resume; sessions.service.mjs and the proctor dashboard's countdown.mjs call it. In the service:
The controller turns a NotFound into 404 and any other error into 500. The dashboard's tile:
export function countdownLabel(timer, t, now) {
if (t.pausedAt !== null) return 'paused';
const s = Math.ceil(timer.remaining(t, now) / 1000);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
}
Which change passes the existing tests and both checks, and keeps the timer's API?
candidate(examId, candidateId) { const c = this.store.get(examId)?.get(candidateId); if (!c) throw new NotFound(`candidate ${candidateId} is not in ${examId}`); return c; } extendAll(examId, minutes, by) { for (const c of this.store.get(examId).values()) c.timer = this.timer.addTime(c.timer, minutes * 60_000); this.audit.push({ type: 'exam.extended', examId, minutes, by }); } pauseCandidate(examId, candidateId, by) { const c = this.candidate(examId, candidateId); c.timer = this.timer.pause(c.timer, this.clock.now()); this.audit.push({ type: 'candidate.paused', examId, candidateId, by }); }The 30-second version
In an existing codebase, the round checks whether you can fit in. Find the feature most like the one asked and build yours in its shape: same layer, same errors, same audit, a test beside its test. Call modules through the API they already export. Make the smallest change that fits, then prove nothing else moved.
Find the nearest existing feature, build yours in its shape through the APIs as they are, and say improvements instead of making them.
How it works
Step 1 of 10
pause for extend): a method name, a route, an audit type.Analogy: An electrician adding one socket to a lived-in house
Asked for one more socket in a bedroom, she finds the sockets already on that room's circuit and works from the nearest, rather than running a new cable from the fuse board. She uses the house's cable size and the same white faceplate as the other sockets. The breakers and their labels stay as they are, because the household switches by them. She would have wired the room differently, but she writes “old kitchen radial undersized, worth a look” on her handover note instead of rewiring it. Then she tests the new socket and the whole circuit, and adds the readings to the house's existing test schedule.
| In the house | In the codebase |
|---|---|
| The sockets already on the room's circuit | The sibling feature and its path |
| The house's cable and faceplate | The sibling's layer, errors and audit |
| Breaker labels left as they are | The module's public API unchanged |
| The note about the kitchen radial | An improvement said, not made |
| Readings added to the existing schedule | A test beside the sibling's, and the whole suite re-run |
Breaks down when: wiring regulations are national, so the house's way is usually also the right way; a codebase's conventions are local and can be wrong. Wrong inference: “copying the sibling is always correct.” If the sibling has a bug, copy its shape, not the bug, and say so.
In real terms: before typing, say the sibling's path aloud: where its route, rule and test live. That is where yours go.
Examples, run for real
EXH-3150 is a synthetic practice round on a seeded ExamHall sessions service, with the opening question's task. The candidate's first attempt began by turning the timer module into a class “so the feature has a clean home”. MR !3450 is the re-run.
EXH-3150 · first attempt · the timer module turned into a Timer class first, then the feature
before the change: suite not run
after: suite 9 of 10 · red: dashboard: the tile shows m:ss left (timer.remaining is not a function)
interviewer's checks 2 of 2 · timer API Timer/class (was addTime/2 pause/2 remaining/2 resume/2 startTimer/2) · diff 4 files +31 −15
MR !3450 · re-run · search for "Candidate(": sessions.controller.mjs pauseCandidate · sessions.service.mjs pauseCandidate
before the change: suite 9 of 9
her test added beside the pause test: suite 9 of 10 · red: service: extendCandidate extends one candidate only (svc.extendCandidate is not a function)
method beside pauseCandidate, route beside the pause route: suite 10 of 10 · interviewer's checks 2 of 2 · timer API unchanged
diff 3 files +13 −0 (sessions.controller.mjs, sessions.service.mjs, suite.mjs)
At 25 minutes the first attempt had a cleaner timer and a broken dashboard: countdown.mjs, a caller the candidate never opened, still calls remaining, which the class no longer exports. With no count from before, she could not tell the interviewer whether the dashboard had ever passed.
The re-run starts by searching for the sibling: the service's one candidate-level method, pauseCandidate, and its route. She says the count before typing, adds her test beside the pause test and shows it failing, then writes a method shaped like pauseCandidate, on addTime, and a route beside the pause route. The timer is untouched. Her one remark about the timer becomes a sentence: “I'd make startTimer take minutes; not now.”
The script counts tests and exports on the author's fixture; it does not judge what a real interviewer would score.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
Pair round on a streaming service. The task: “Titles now carry a regions list. A title must not play for a viewer whose country is not in it.” playback.mjs:
The web app plays through routes.mjs, which answers a PlaybackDenied with 403 and its reason. The TV app's Continue watching row calls startPlayback directly. browse.mjs builds the catalogue a viewer scrolls.
Which change makes both apps refuse such a title the way they refuse the others?
export const ageGuard = (viewer, title) => (title.minAge > viewer.age ? 'age' : null);export const planGuard = (viewer, title) => (title.premium && viewer.plan !== 'premium' ? 'plan' : null);export const GUARDS = [ageGuard, planGuard]; export function startPlayback(ctx, viewer, titleId) { const title = ctx.catalog.get(titleId); for (const guard of GUARDS) { const reason = guard(viewer, title); if (reason) throw new PlaybackDenied(reason); } ctx.streams.push({ viewer: viewer.id, titleId }); return { titleId, from: viewer.positions[titleId] ?? 0 };}Question 2 of 7
MR !3451 on the same streaming service. The task: “Add skipIntroAt (seconds, or null if the episode has no intro) to GET /episodes/:id.” The service has 13 source files. The candidate's diff touches two of them, and the episode tests she ran pass:
// episodes.mjs
-export function getEpisode(ctx, id) { return ctx.episodes.get(id); }
+export function fetchEpisode(ctx, id) { return ctx.episodes.get(id); }
export function fetchSeries(ctx, id) { return ctx.series.get(id); }
export function runtime(ep) {
- let total = 0;
- for (const part of ep.parts) total += part.seconds;
- return total;
+ return ep.parts.reduce((total, part) => total + part.seconds, 0);
}
export function toEpisodeDto(ep) {
- return { id: ep.id, title: ep.title, seconds: runtime(ep) };
+ return { id: ep.id, title: ep.title, seconds: runtime(ep), skipIntroAt: ep.introEnd ?? null };
}
// episode.controller.mjs
- return episodes.toEpisodeDto(episodes.getEpisode(ctx, id));
+ return episodes.toEpisodeDto(episodes.fetchEpisode(ctx, id));
Before the review, a search of the service for getEpisode( printed:
$ grep -n "getEpisode(" *.mjs
downloads.mjs:3: const ep = episodes.getEpisode(ctx, id);
episode.controller.mjs:3: return episodes.toEpisodeDto(episodes.getEpisode(ctx, id));
episodes.mjs:2:export function getEpisode(ctx, id) { return ctx.episodes.get(id); }
The episode test checks the reply's seconds, and the TV app reads the reply with const { id, title, seconds } = JSON.parse(json);. Run against the whole service's tests, one of the three changes turns a test red.
Which one?
Question 3 of 7
Back on the ExamHall service, her extendCandidate is in and every test passes; the interviewer has asked for nothing more. She will name one improvement to the existing code as a follow-up, with its reason, and change nothing. The timer module:
Two of the service's methods:
extendAll(examId, minutes, by) {
for (const c of this.store.get(examId).values()) c.timer = this.timer.addTime(c.timer, minutes * 60_000);
this.audit.push({ type: 'exam.extended', examId, minutes, by });
}
pauseCandidate(examId, candidateId, by) {
const c = this.candidate(examId, candidateId);
c.timer = this.timer.pause(c.timer, this.clock.now());
this.audit.push({ type: 'candidate.paused', examId, candidateId, by });
}
The controller turns a NotFound into 404 and any other error into 500.
Which follow-up gives a true reason?
export function startTimer(now, durationMs) { return { endsAt: now + durationMs, pausedAt: null }; }export function remaining(t, now) { return Math.max(0, t.endsAt - (t.pausedAt ?? now)); }export function addTime(t, ms) { return { ...t, endsAt: t.endsAt + ms }; }export function pause(t, now) { return t.pausedAt === null ? { ...t, pausedAt: now } : t; }export function resume(t, now) { return t.pausedAt === null ? t : { endsAt: t.endsAt + (now - t.pausedAt), pausedAt: null }; }Question 4 of 7
Later in the same round, with extendCandidate in, the interviewer adds: “Fire alarms: let a proctor pause the whole exam with one request.” They will call POST /exams/econ-201/pause at minute 30, then check that at minute 40 every candidate still has the time left they had at 30, and that the audit trail gained one entry, of type exam.paused. The routes:
The service's methods after candidate:
extendAll(examId, minutes, by) {
for (const c of this.store.get(examId).values()) c.timer = this.timer.addTime(c.timer, minutes * 60_000);
this.audit.push({ type: 'exam.extended', examId, minutes, by });
}
pauseCandidate(examId, candidateId, by) {
const c = this.candidate(examId, candidateId);
c.timer = this.timer.pause(c.timer, this.clock.now());
this.audit.push({ type: 'candidate.paused', examId, candidateId, by });
}
extendCandidate(examId, candidateId, minutes, by) {
const c = this.candidate(examId, candidateId);
c.timer = this.timer.addTime(c.timer, minutes * 60_000);
this.audit.push({ type: 'candidate.extended', examId, candidateId, minutes, by });
}
The timer module:
export function startTimer(now, durationMs) { return { endsAt: now + durationMs, pausedAt: null }; }
export function remaining(t, now) { return Math.max(0, t.endsAt - (t.pausedAt ?? now)); }
export function addTime(t, ms) { return { ...t, endsAt: t.endsAt + ms }; }
export function pause(t, now) { return t.pausedAt === null ? { ...t, pausedAt: now } : t; }
export function resume(t, now) { return t.pausedAt === null ? t : { endsAt: t.endsAt + (now - t.pausedAt), pausedAt: null }; }
The proctor dashboard's tile, countdownLabel, shows paused for a candidate whose timer has pausedAt set. Before typing, she picks the existing code her feature will follow.
Which should it be?
'POST /exams/:examId/extend': (p, body, user) => svc.extendAll(p.examId, body.minutes, user), 'POST /exams/:examId/candidates/:candidateId/pause': (p, body, user) => svc.pauseCandidate(p.examId, p.candidateId, user), 'POST /exams/:examId/candidates/:candidateId/extend': (p, body, user) => svc.extendCandidate(p.examId, p.candidateId, body.minutes, user),Question 5 of 7
Work in a copy of the ExamHall fixture.
Keeping the timer module's public API as it is, add “resume one paused candidate”, with its route and a test, in 10 minutes.
Question 6 of 7
On the streaming service, the task is to show an episode card's running time as 1:05 instead of 1 h 5 min. The candidate changes the shared helper's default, compact = false, to compact = true:
Four screens use format.mjs:
export const cardLine = (format, ep) => `${ep.title} · ${format.formatDuration(ep.seconds)}`;
export const overlayLine = (format, left) => `${format.formatDuration(left, { compact: true })} left`;
export const resultLine = (format, ep) => `${ep.title} (${format.formatRuntime(ep)})`;
export const downloadLine = (format, d) => `${d.title} · ${format.formatDuration(d.seconds, { compact: false })} · ${d.mb} MB`;
For the same episode, which screens now show a different running time than before the change?
// format.mjsexport function formatDuration(seconds, { compact = true } = {}) { const h = Math.floor(seconds / 3600), m = Math.floor((seconds % 3600) / 60); if (compact) return `${h}:${String(m).padStart(2, '0')}`; return h ? `${h} h ${m} min` : `${m} min`;}export function formatRuntime(ep) { return formatDuration(ep.seconds); }Question 7 of 7
A learner aiming at 2026 interviews at a video-streaming company practises only by solving puzzles in blank files.
Which finding of CoderPad's State of Tech Hiring 2026 report (read 2026-10-06) bears most directly on that plan?
Go deeper
Pro tip
In a practice repo you have never seen, give yourself two minutes before typing: find the feature most like the ask and write its path as three file names. If you cannot, keep looking before you write code.
Related lessons
Checked against the tools and docs as of 2026-10-06.