For Roku software roles, the useful preparation split is between general coding and the systems knowledge required by your particular team. A device or embedded role may need ownership, bounded buffers and synchronization; a backend role may need caching, retries and service design. Ask which track applies before turning every low-level topic into a mandatory study task.
The Roku question collection provides additional practice context. The two worked implementations below are original Python models with explicit invariants; they are not claimed as newly verified Roku questions or tested C++ implementations.
Practice timings and scorecards here are editorial. Confirm the team, level, language, format and allowed tools with your recruiter. Existing compiled reports do not establish a universal current sequence.
Confirm team and language
editorialPreparation checkpoint; confirm whether this component is in your actual assessment.
What to demonstrate
- Ask which team, level, language and assessment format apply.
- Prepare only technologies you can explain beyond their API.
How to prepare
- Ask which team, level, language and assessment format apply.
- Prepare only technologies you can explain beyond their API.
Implement and explain systems code
editorialPreparation checkpoint; confirm whether this component is in your actual assessment.
What to demonstrate
- Run the ring buffer and LRU examples; trace wraparound, eviction and cached None.
- For C++, port the models and discuss ownership, iterator validity and synchronization; the Python examples do not validate C++ behavior.
How to prepare
- Run the ring buffer and LRU examples; trace wraparound, eviction and cached None.
- For C++, port the models and discuss ownership, iterator validity and synchronization; the Python examples do not validate C++ behavior.
Design, debugging and project decisions
editorialPreparation checkpoint; confirm whether this component is in your actual assessment.
What to demonstrate
- Choose an actual role-relevant design and test failure handling.
- Explain a project decision and the evidence that changed it.
How to prepare
- Choose an actual role-relevant design and test failure handling.
- Explain a project decision and the evidence that changed it.
PracHub editorial advice for the preparation topics above.
Giving approximate answers on concurrency primitives and integer representation
Reported systems questions ask for mechanics: how a semaphore differs from a mutex, when a spinlock fits in embedded code, how to find the integer size on an unknown architecture. Vague answers here are easy to probe, so prepare one exact sentence for each. A mutex has an owner and guards a critical section. A counting semaphore tracks available units and can be signalled by a different thread than the one that waits. A spinlock busy-waits, so it fits very short critical sections on multi-core systems or contexts that cannot sleep, such as interrupt handlers, and wastes CPU anywhere else. For bit width, give sizeof(int) * CHAR_BIT, or count left shifts of an unsigned 1 until it becomes zero.
Listing a technology on your resume that you cannot explain below the API
Candidates report that the technical screen often includes a resume deep dive, so expect follow-up questions on anything you list. Before that stage, cut or prepare anything you cannot discuss in depth. For Kubernetes, know what the scheduler does with a pending pod. For C++, know what a unique_ptr destructor does and why shared_ptr needs a control block. For a database you list, know which index a given query would use. If you would not want a follow-up question on it, take it off the page.
Submitting code in the shared editor that you never ran against edge cases
You may be asked to write, run and debug code in an editor such as HackerRank or CoderPad, so code that reads well but fails on an input you never tried shows up right away. For string-to-integer, test leading whitespace, a sign with no digits, a non-digit mid-string and values past the 32-bit range. For linked lists, test an empty list, a single node and a cycle back to the head. Trace one small input out loud before you run it. When a test fails, state your hypothesis before you change any code.
Treating 'handles concurrency' as an afterthought in a data-structure question
One reported coding question asks for a Max Stack that handles concurrency, and a reported systems question asks you to fix a race condition. First ask which operations must be atomic together, for example push together with the max update, or pop followed by peekMax as a pair. A single lock around each operation is the correct baseline. Say so, name its contention cost, and only then discuss finer-grained options. Do not reach for lock-free structures you cannot prove correct on the spot.
Designing the subscription notification scheduler as one job that fires everything at once
Clarify due-time precision and tolerated lateness, then use durable events, leased workers and bounded dispatch. A stable local send identity supports replay control, but a provider timeout can leave delivery ambiguous. Require provider-supported idempotency or a reconciliation/deduplication contract before promising that a retry cannot notify twice. Explain worker death and the accepted delivery semantics.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Clone a directed graph with cycles
Write a program to deep clone a graph with multiple nodes and directed edges.
Approach
- Map each original node to one clone before traversing its edges.
- Use the map to preserve cycles and shared neighbors.
- Test self-cycle, disconnected start and two parents sharing one child.
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?
Parse an integer with explicit bounds
Write a function to convert an ASCII string to an integer without using built-in parsing libraries.
Approach
- Specify whitespace, optional sign, stop character and overflow policy.
- Check the bound before multiplying and adding the next digit.
- Test sign-only input, both limits, overflow and trailing nondigits.
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?
Synchronize a Max Stack
Design and implement a custom Max Stack data structure that handles concurrency issues.
Approach
- Define push, pop, maximum and equal-maximum behavior first.
- Use a data structure whose maximum state is updated atomically with the stack.
- State the lock scope; test concurrent calls and duplicate maxima without claiming lock-free behavior.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Compare anagrams under a fixed alphabet
Solve an array-based anagram detection problem in constant space, and discuss how to scale the solution.
Approach
- For a fixed alphabet, compare length and a fixed-size frequency array.
- Explain why Unicode or unbounded alphabets change the constant-space assumption.
- For large inputs, define encoding and streaming/chunk boundaries before discussing parallelism.
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?
Build a bounded character FIFO
Implement a simple character FIFO buffer and write the logic for the IsFIFOFull state check.
Approach
- Choose a count or sacrificed-slot representation so full and empty are distinct.
- Trace wraparound and reject overflowing writes.
- Explain value ownership and the synchronization model before adding concurrency.
Follow-up
- Where could this allocate more than you expect?
- What happens if two callers reach this at the same time?
Explain synchronization primitives precisely
Detail the mechanics of semaphores, mutexes, and spinlocks, and when to use each in embedded systems.
Approach
- Distinguish mutex ownership, semaphore capacity and spin waiting.
- Name a context that can sleep and one that cannot.
- Explain memory visibility and critical-section duration rather than claiming one primitive is always faster.
Follow-up
- Where could this allocate more than you expect?
- How would you prove the race exists rather than suspect it?
Track a rolling failure rate per destination for circuit decisions
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
Approach
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
- Expire idle destinations, or memory grows with every destination ever seen rather than with the live set. Hold the rings in a bounded LRU keyed on destination_id and treat a miss as no history, which is the correct default for an endpoint that has been silent for a minute.
- Keep the half-open probe out of the window arithmetic. After the circuit opens, one probe per interval decides whether to close it, and folding that single success into a window that still holds a 100 percent failure history would reopen the destination on one data point.
Worked solution 20 min
- Define the bucket struct and the advance step: take floor(finished_at_ms / 1000), compare with the ring's current second, zero min(delta, 60) buckets forward, then write into the new head.
- Trace a destination that receives 5 attempts, goes silent for 90 seconds, then receives one more, and confirm the rate is computed from one attempt rather than six.
- Compute total memory for 40,000 destinations at 60 buckets of two 4-byte counters, and state what changes if the window widens to 300 seconds.
- Write the open rule as a single predicate combining the minimum-attempt floor with the rate threshold.
Follow-up
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
- How would you make the window survive a process restart, and is it worth the cost?
A bounded playback FIFO
Implement a fixed-capacity single-threaded FIFO that rejects empty pop and full push, preserving arrival order through wraparound.
Approach
- State head, size and tail invariants.
- Reject full push and empty pop without corrupting state.
- Trace wraparound and explain producer backpressure.
Worked solution 25 min
class Ring:
def __init__(self, capacity):
if type(capacity) is not int or capacity <= 0:
raise ValueError("positive integer capacity required")
self.slots = [None] * capacity
self.head = 0
self.size = 0
def push(self, value):
if self.size == len(self.slots):
raise BufferError("full")
tail = (self.head + self.size) % len(self.slots)
self.slots[tail] = value
self.size += 1
def pop(self):
if self.size == 0:
raise BufferError("empty")
value = self.slots[self.head]
self.slots[self.head] = None
self.head = (self.head + 1) % len(self.slots)
self.size -= 1
return value
r = Ring(2)
r.push("A"); r.push("B")
try:
r.push("lost")
raise AssertionError("full push accepted")
except BufferError:
pass
assert r.pop() == "A"
r.push("C")
assert [r.pop(), r.pop()] == ["B", "C"]
try:
r.pop()
raise AssertionError("empty pop accepted")
except BufferError:
pass
Scroll sideways to view long lines.
Follow-up
- What changes with concurrent callers?
- How is payload memory bounded?
An LRU with explicit miss semantics
Implement an entry-count LRU with get returning (found, value), recency updates on reads and writes, and no TTL or concurrency promise.
Approach
- Use a map and a recency structure.
- Distinguish a cached None from a miss.
- Move hits and updates to most recent and evict the oldest entry.
Worked solution 25 min
from collections import OrderedDict
class LRU:
def __init__(self, capacity):
if type(capacity) is not int or capacity <= 0:
raise ValueError("positive integer capacity required")
self.capacity = capacity
self.items = OrderedDict()
def get(self, key):
if key not in self.items:
return False, None
self.items.move_to_end(key)
return True, self.items[key]
def put(self, key, value):
self.items[key] = value
self.items.move_to_end(key)
if len(self.items) > self.capacity:
self.items.popitem(last=False)
c = LRU(2)
c.put("A", None); c.put("B", 2)
assert c.get("A") == (True, None)
c.put("C", 3)
assert c.get("B") == (False, None)
c.put("A", 9)
assert c.get("A") == (True, 9)
assert len(c.items) == 2
Scroll sideways to view long lines.
Follow-up
- What changes with concurrent callers?
- How is payload memory bounded?
Hold a per-tenant active cap against concurrent creates
A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.
Approach
- Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
- Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
- Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
- State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.
- For the job case the invariant is expressible per row, so let the database hold it: a partial unique index on job_run (tenant_id, job_type) WHERE status IN ('queued','running') makes a second running export unwritable and the loser takes 23505, mapped to 409. That is strictly better than a counter — no drift, no reconciliation — and it is available only because the cap is one rather than fifty.
- Add the retry discipline each route demands: under SERIALIZABLE both 40001 and deadlock 40P01 are retryable and the retry must re-execute the read, while under READ COMMITTED with the counter nothing retries, because the conflict is reported to the caller rather than raised as an error.
Follow-up
- A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
- The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?
- How do you detect after the fact that the counter drifted, without locking the table?
Explain why the owner filter ignores the listing index
The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.
Approach
- Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
- For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
- Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
- Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.
- Answer the two negatives explicitly. owner_user_id alone is not a left prefix of the new index, so it degrades to a full scan of the index at best. Ordered by updated_at, the query still seeks on the (tenant, owner) pair but must sort, because only created_at is ordered within that pair.
- Measure both sides with EXPLAIN (ANALYZE, BUFFERS) and compare estimated against actual rows at the lowest node — a 2M-versus-200 misestimate there is usually what chose the plan, and adding an index will not fix a statistics problem.
Worked solution 25 min
- Load 2M resource rows across 5k owners in one tenant, run the query under EXPLAIN (ANALYZE, BUFFERS), and record the node reading the most rows plus the Sort Method line.
- Create (tenant_id, owner_user_id, created_at DESC) concurrently and re-run, confirming the Sort node is gone and actual rows fall to about 20.
- Run the two negative cases and capture the plan for each.
- Re-run the original tenant listing query to confirm the new index has not displaced the index that query depends on.
Follow-up
- 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
- A dashboard runs this for 40 owners in one page load. What changes about the design?
- How do you roll this index out on a table taking 1.2k writes/second, and what does it cost on every insert from then on?
Schedule notifications through retries
Design a one-time event scheduler that notifies millions of users when their movie subscriptions start.
Approach
- Clarify schedule identity, due-time precision and tolerated lateness.
- Use durable due events and leased workers with bounded provider dispatch.
- A provider idempotency key or reconciliation is needed after an ambiguous acknowledgement; a local key alone cannot guarantee one notification.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Design a recommendation serving path
Describe the architecture of a personalized content recommendation engine that processes high-throughput user interaction data.
Approach
- Define the product objective and separate candidate retrieval from ranking.
- Discuss feedback freshness, cold start and fallback behavior.
- Measure relevance and latency together and identify leakage or feedback-loop risks.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Rebuild the search projection while it serves nine thousand queries
The read-model service answers about 9k queries/second at a 120 ms p99 from a projection built off the event log, normally under 2 seconds behind. A mapping change forces a full rebuild from resource_revision, during which apply lag rises to minutes. Writes continue at 1.2k/second and events at 4k/second. Design the rebuild: how the new index is populated and cut over, how position is tracked per log partition, what the API returns alongside results so a client can tell a stale answer from a current one, and the criterion for cutting over.
Approach
- Build into a second index and swap an alias rather than mutating the live one. The rebuild is then reversible by pointing the alias back, so a bad mapping costs a wasted rebuild instead of an outage. The price is peak storage for two full copies and double apply load during catch-up, and both numbers should be stated up front rather than discovered when disk fills.
- Track position per log partition, not globally. Order is guaranteed only within an aggregate's partition, so progress is a vector, and the only number safe to publish is taken from the least advanced partition - the slowest one is what bounds completeness. Publishing the most advanced partition's position declares the projection current while another partition sits twenty minutes behind.
- Make apply idempotent so the backfill and the live tail can overlap without a freeze. Each document records the aggregate_version it reflects, and any event at or below that version is discarded. This is why the event carries the full fact and not a delta: a delta cannot be discarded safely, and a consumer that calls back to read current state applies a state newer than the event it is processing, which is how a projection ends up with changes applied out of order.
- Turn lag into a contract instead of a surprise. Return the watermark with every result set, and return the version produced by a write so the client can compare the two. A client that wrote version 7 and receives results at a watermark older than its own commit can show that its change is still landing, rather than rendering the previous value as current. Blocking the read until the projection catches up would convert a staleness problem into an availability problem at 9k queries/second, and choosing not to do that is the trade.
- Define the cutover numerically. Writes are untouched by the rebuild - they commit to the primary and land in the outbox - so the only coupling is apply throughput. If catch-up applies slower than the 4k events/second arriving, it never converges. The cutover criterion is that lag is measurably decreasing and below a stated threshold, not that the backfill loop reached the end of its range.
Worked solution 30 min
- Write the rebuild sequence: snapshot source, populate the second index, attach the live tail, verify, swap alias, retire the old index - naming what is reversible at each step.
- Define the per-partition position record and the single watermark derived from it, and show what each would report when one partition stalls.
- Write the idempotent apply rule using the document's recorded aggregate_version, and replay one event twice against it.
- State the cutover threshold in lag terms and the measurement that shows convergence rather than completion.
Follow-up
- The rebuilt index disagrees with the primary tables for 300 documents. Which is authoritative, and how do you decide without freezing writes?
- The rebuild doubles load on the log and pushes the projection p99 from 120 ms to 400 ms. What do you throttle, and which signal sets how much?
- Clients start polling until the watermark passes their write. What does that do at 9k queries/second, and what do you offer instead?
Every query on one table stalls for forty seconds mid-deploy
During a release on PostgreSQL, every query touching resource times out for about 40 seconds and then recovers with no intervention. The release ran one migration, ALTER TABLE resource ADD COLUMN archived_reason TEXT, and the migration log shows it completing in 6 ms. Unrelated tables showed no change in error rate. Explain how a 6 ms statement caused a 40-second stall, give the ordered checks you would run on a live system to confirm it, and give the migration procedure that prevents a repeat.
Approach
- Separate the statement's duration from the lock's duration. ADD COLUMN with no default is a catalogue-only change and genuinely runs in milliseconds, but it requires ACCESS EXCLUSIVE, and it cannot acquire that until every transaction already touching the table has finished.
- Account for the queueing, which is the part that surprises people. A lock request that is waiting blocks later requests for conflicting modes behind it rather than letting them overtake, so one long-open transaction holds the DDL and the DDL holds all the traffic. The stall length is set by the longest open transaction, not by the size of the change.
- Confirm on a live system in this order: pg_stat_activity for that table ordered by xact_start, looking for the oldest transaction and specifically for state = idle in transaction; then pg_locks where granted = false to find the waiter; then join them on pid to name blocker and blocked. pg_blocking_pids() does that join for you and is the fastest single call.
- Prevent rather than merely time it better. Set lock_timeout to a second or two on the migration session so the DDL abandons the queue after a bounded wait and is retried, instead of holding it for as long as the oldest transaction lives. Be exact about what that buys: queries arriving during the wait still queue behind the pending ACCESS EXCLUSIVE request, so each attempt costs them up to one lock_timeout of added latency. The outage goes from 40 seconds to about one second per attempt, not to zero. Also run migrations away from deploy-time peaks, and put a statement timeout and an idle-in-transaction timeout on the analytics role that opens the long transactions.
- Know the lock each change takes, since the mitigation differs by change. A column with a non-volatile default is a metadata-only change from PostgreSQL 11 and still needs the brief ACCESS EXCLUSIVE; an index needs CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an INVALID index to drop if it fails; a check or foreign key is added NOT VALID and then VALIDATE CONSTRAINT as a separate statement under a weaker lock.
Follow-up
- The same release also wants NOT NULL on that column. What is the sequence that gets there without a long lock?
- Your lock_timeout retry fails ten times in a row because the analytics transaction is always open. What do you change?
- How does this differ on MySQL with InnoDB online DDL, and what is the equivalent of the waiting-lock queue there?
Editorial rehearsal plan. Adapt it to the confirmed team, level and assessment.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Role and language
- Ask which team, level, language and assessment format apply.
- Prepare only technologies you can explain beyond their API.
Deliverable: Recruiter questions and team route
02Buffer baseline
- State head, size and tail invariants.
- Reject full push and empty pop without corrupting state.
- Trace wraparound and explain producer backpressure.
Deliverable: Runnable FIFO and wraparound trace
Practice prompt ↗Worked solution ↗03Cache baseline
- Use a map and a recency structure.
- Distinguish a cached None from a miss.
- Move hits and updates to most recent and evict the oldest entry.
Deliverable: Runnable LRU and eviction trace
Practice prompt ↗Worked solution ↗04Ownership and concurrency
- State who owns each returned value.
- Define the atomic critical section and condition-variable wait predicate.
Deliverable: C++ port plan and lock invariants
Practice prompt ↗05OS and networking
- Explain process versus thread and stack versus heap.
- Trace a queue stall or network retry with one discriminating probe.
Deliverable: Mechanism explanations and one failure probe
Practice prompt ↗06Team-specific design
- Choose an actual role-relevant design and test failure handling.
- Explain a project decision and the evidence that changed it.
Deliverable: Requirements, failure trace and measurable target
Practice prompt ↗07Combined mock
- Spend 20 minutes on each model and five on its tests.
- Record the first invariant you broke and how a test caught it.
Deliverable: Two timed attempts and corrections
Practice prompt ↗Practice prompt ↗Worked solution ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
The reported behavioral questions for this role cover debugging depth, ambiguity and disagreement. Answer with specifics: the symptom, the hypotheses you ruled out and how, the decision you made, and what changed afterwards. For the disagreement question, the stronger answer shows the evidence that settled it, and what you did if the decision went against you.
Unblock an engineer without taking the keyboard
A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.
Approach
- Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
- Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
- Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
- Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.
- Close on the systemic gap that let two days pass, which is usually a missing dashboard for attempt counts or an undocumented at-least-once contract, and fix that rather than only the bug.
Follow-up
- How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
- Their handler calls an external endpoint before recording that it did. What do you tell them to change first?
- What do you do the third time the same person brings you the same class of bug?
Reverse your own decision and price the reversal
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
- Finish on the process change: the smallest experiment that would have produced the same measurement in a day, and why you did not run it the first time.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?
- What do you now measure before committing to a change of this size?
Tell callers you do not own that their integration breaks
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
Approach
- Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
- Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
- Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
- Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.
- State the removal criterion as a measurement with a duration attached, such as observed reads at zero across a full billing cycle, and keep the change reversible for one release after removal.
Follow-up
- How would you detect a consumer that reads the field only during a monthly export?
- One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?
- After removal, what makes the change irreversible, and how long before you cross that line?
- 01
Walk me through the most technically challenging bug you have ever tackled. How did you diagnose it, and what did you learn?
- 02
Describe a situation where you had to work with vague or ambiguous requirements. How did you clarify the scope and deliver the solution?
- 03
What do you like and dislike about your current development environment and technical stack?
- 04
How do you balance cross-platform consistency with platform-specific optimization when building consumer applications?
- 05
Tell me about a time you had a technical disagreement with a peer or manager. How did you resolve it?
Is head equal to tail enough to identify a full ring?
Not by itself in a design where both empty and full can share that position. This model also records size. An alternative sacrifices one slot, but then usable capacity differs from allocated capacity.
PracHub interview preparation framework ↗Is an LRU cache automatically thread-safe?
No. A read changes recency and therefore mutates shared state. Concurrent operations need a synchronization policy that covers lookup, recency changes and eviction together.
PracHub interview preparation framework ↗Should every candidate prepare embedded topics?
Prepare the topics that match the posting and confirmed assessment. Keep a general coding foundation, then spend deeper preparation time on the role’s work.
PracHub interview preparation framework ↗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 ↗
Historical compiled PracHub material; not an employer publication. Current timing and stage counts were not independently verified.
PracHub page · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-24