Callback Signal Registry
Reported by candidates from Candid Health's online assessment. Pattern, common pitfall, and the honest play if you blank under the timer.
The edge case that breaks a naive Callback Signal Registry solution is reentrancy, and Candid Health put it in an OA reported in October 2026. You register callbacks, unregister them, and dispatch signals. Easy on paper. Then a callback fires a nested signal, another throws, and the order has to stay exact. This is a simulation problem with a hash table and an ordered list under it. If you've got the OA in a day or two, the work is in the queue and snapshot rules, not the algorithm. StealthCoder sits invisibly on your screen as a safety net if you blank mid-assessment.
The problem
Implement a callback registry keyed by signal ID. Process the finite batch operations in order. Every callback ID has one row in callbackBehaviors. Operations ["REGISTER", signalId, callbackId]: add the callback to that signal. A signal contains each callback ID at most once. Return ["REGISTERED"], or ["ALREADY_REGISTERED"] when it is already present. ["UNREGISTER", signalId, callbackId]: remove that callback identity. Return ["UNREGISTERED"], or ["NOT_FOUND"] when it is absent. ["SIGNAL", signalId]: dispatch the signal and return its invocation events as described below. Registration order is significant. Removing a callback preserves the relative order of all others; registering it later appends it to the end. Callback behaviors Each behavior row is [callbackId, outcome, nestedSignalId, nestedSignalLimit]. outcome is "OK" or "THROW". A nested signal ID of "-" means that callback never signals another key. Otherwise, its first nestedSignalLimit invocations enqueue that signal. The invocation count is global across the whole operation batch. A SIGNAL operation uses a FIFO queue initialized with its signal ID. For each dequeued signal, take a snapshot of its callback IDs in current registration order and invoke that snapshot completely. Nested signals join the back of the queue, so reentrant work never interrupts the current snapshot. Each later queued signal takes a fresh snapshot. When a callback invokes a nested signal and has outcome "THROW", the nested signal is enqueued before the callback error is recorded. An error never stops the rest of the current snapshot or the queued signals. Record each invocation as signalId:callbackId:OK or signalId:callbackId:ERROR. One SIGNAL row contains every event produced while its queue drains. Signaling a key with no callbacks returns an empty row. Return exactly one output row per input operation. Function processCallbackSignals(operations: String[][], callbackBehaviors: String[][]) → String[][] Examples Example 1 operations = [["REGISTER","invoice","audit"],["REGISTER","invoice","notify"],["REGISTER","invoice","metrics"],["SIGNAL","invoice"],["UNREGISTER","invoice","notify"],["SIGNAL","invoice"]] callbackBehaviors = [["audit","OK","-","0"],["notify","THROW","-","0"],["metrics","OK","-","0"]] return = [["REGISTERED"],["REGISTERED"],["REGISTERED"],["invoice:audit:OK","invoice:notify:ERROR","invoice:metrics:OK"],["UNREGISTERED"],["invoice:audit:OK","invoice:metrics:OK"]] Callbacks run in registration order. The failing notify callback is recorded as an error, but metrics still runs. After removal, the remaining order is unchanged. Example 2 operations = [["REGISTER","ready","first"],["REGISTER","ready","second"],["REGISTER","ready","first"],["UNREGISTER","ready","first"],["REGISTER","ready","first"],["SIGNAL","ready"]] callbackBehaviors = [["first","OK","-","0"],["second","OK","-","0"]] return = [["REGISTERED"],["REGISTERED"],["ALREADY_REGISTERED"],["UNREGISTERED"],["REGISTERED"],["ready:second:OK","ready:first:OK"]] A duplicate registration is a no-op. Removing and registering first again appends it after second. Example 3 operations = [["REGISTER","alpha","a"],["REGISTER","alpha","b"],["REGISTER","beta","c"],["SIGNAL","alpha"]] callbackBehaviors = [["a","OK","beta","1"],["b","THROW","alpha","1"],["c","OK","-","0"]] return = [["REGISTERED"],["REGISTERED"],["REGISTERED"],["alpha:a:OK","alpha:b:ERROR","beta:c:OK","alpha:a:OK","alpha:b:ERROR"]] The first alpha dispatch queues beta and then another alpha. FIFO draining finishes the current snapshot first. Each callback's one-signal limit then prevents another cycle. Constraints 0 <= operations.length <= 100000. 0 <= callbackBehaviors.length <= 100000. Every operation has exactly one of the three forms above, and every registered callback ID has exactly one behavior row. Signal IDs and callback IDs contain 1 to 20 lowercase ASCII letters, digits, or underscores, and do not contain colons. Callback IDs are unique in callbackBehaviors. outcome is "OK" or "THROW". nestedSignalLimit is a decimal integer from 0 through 10; it is 0 when nestedSignalId is "-". The sum of all nested-signal limits is at most 100000. The total number of callback invocation events produced by the batch is at most 200000.
Reported by candidates. Source: FastPrep
Pattern and pitfall
The trick is separating three pieces of state. Keep, per signal, an insertion-ordered collection of callback IDs with O(1) membership checks. Keep a global invocation counter per callback, since the nested limit spans the whole batch, not one SIGNAL. Then run each SIGNAL with a FIFO queue. For each dequeued signal, copy its callbacks into a snapshot first, then iterate that copy. The common pitfall is iterating the live list, or using recursion, so nested signals cut in line. Another trap is the THROW case: enqueue the nested signal first, then record ERROR, and never stop the loop. Unregister then register must append to the end. An insertion-ordered dict or a linked hash set handles it. With 100000 operations, avoid O(n) list removal. If you freeze on the ordering rules during the live OA, StealthCoder is the hedge that reads the problem and hands you the structure.
StealthCoder is the hedge for the one pattern you didn't drill. It runs invisibly during the screen share.
You can drill Callback Signal Registry cold, or you can hedge it. StealthCoder runs invisibly during screen share and surfaces a working solution in under 2 seconds. The proctor sees the IDE. They don't see what's behind it. If you're reading this with an OA window open, you're who this was built for.
Get StealthCoderYou've seen the question.
Make sure you actually pass Candid Health's OA.
Candid Health reuses patterns across OAs. If you're reading this with an OA window open, you're who this was built for. Works on HackerRank, CodeSignal, CoderPad, and Karat.
Callback Signal Registry FAQ
How hard is the Callback Signal Registry problem really?+
Medium on difficulty, high on detail. There's no clever algorithm. You're simulating rules exactly. Most failures come from misreading snapshot timing, global invocation counts, or the order of enqueue versus error recording. Read the three examples twice before coding.
What's the trick to getting the nested signal order right?+
Use a FIFO queue per SIGNAL operation, seeded with the signal ID. Dequeue one signal, snapshot its callbacks, run the whole snapshot, and push nested signals to the back. Never recurse. That one structure reproduces Example 3 exactly.
Which data structure holds the callbacks for each signal?+
Use an insertion-ordered map or set per signal ID, like a Python dict or a Java LinkedHashSet. You need O(1) duplicate checks, O(1) removal, and stable order. Re-registering after removal appends to the end automatically. A plain list makes removal slow at 100000 operations.
How does the nestedSignalLimit counter work?+
It's global across the whole batch, not reset per SIGNAL. Keep a count per callback ID that increments on each invocation. If the count is within the limit, enqueue the nested signal. Resetting it per operation is the most common bug and fails Example 3 style cycles.
How do I prepare for this in 48 hours?+
Write the solution once from scratch using the examples as tests. Focus on snapshot semantics, THROW handling, and unregister-then-register order. Then try a few variants, like a callback unregistering itself mid-dispatch. The snapshot should still run it. That covers the edge cases.