Implement a Callback Signaling Registry with Register, Unregister and Signal
Company: Candid Health
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: easy
Interview Round: Technical Screen
Implement a small in-process signaling system, essentially a minimal publish/subscribe registry: callbacks subscribe to a signal id, and signaling that id runs them. The system exposes three functions, and each of them must return `None`:
```python
def register_callback(id, callback) -> None: ...
def unregister_callback(id, callback) -> None: ...
def signal(id) -> None: ...
```
A `callback` is any Python callable (`Callable`). Intended usage:
```text
register_callback(10, fn_foo)
register_callback(10, fn_bar)
signal(10) # runs fn_foo and fn_bar
unregister_callback(10, fn_bar)
signal(10) # runs only fn_foo
signal(200) # nothing happens
```
The prompt is deliberately open-ended. The code itself is short; what is evaluated is whether you ask clarifying questions before coding, surface the failure modes and edge cases yourself, and defend the behavior you choose for each. The interviewer's follow-ups target exactly those choices.
### Constraints and Clarifications
- All three functions return `None`. Values returned by callbacks are not passed back to the caller of `signal`.
- Signaling an id that has no registered callbacks does nothing.
- Assume a single-threaded Python program unless the interviewer says otherwise.
### Clarifying Questions
- What arguments, if any, does `signal` pass to the callbacks?
- If the same callback is registered twice for the same id, should it run twice, and how many registrations should one `unregister_callback` call remove?
- What should `unregister_callback` do when the id or the callback was never registered: ignore the call or report an error?
### Part 1 — Registry API and storage
Implement the three functions. Explain how you store the callbacks for each id. Could you put them directly into a `dict` or a `set`? What would that require of the callbacks, and which kinds of callables would break it?
```hint Finding the registration again
`unregister_callback` receives a callable and must find the matching registration. Consider which kinds of callables make that lookup fail with each container you might choose, for example `obj.method` passed once to register and again to unregister.
```
#### What This Part Should Cover
- The three functions with the required `None` returns and the no-op behavior from the example
- A per-id container and the cost of register, unregister and signal with it
- Hashability and equality of callables, and the kinds of callables that break a `set` or `dict` design
- Duplicate registrations, and unregistering something that is not registered
### Part 2 — Execution order and failing callbacks
Must the callbacks run in the order in which they were registered? If one callback raises an exception, should the remaining callbacks for that signal still run, and what should the caller of `signal` observe?
```hint One broken subscriber
Think about what the other subscribers would expect when an unrelated callback ahead of them fails, and where that failure can become visible when `signal` returns nothing.
```
#### What This Part Should Cover
- A stated ordering guarantee and a data structure that actually provides it
- An exception policy (stop, continue, or continue and report) with its trade-off
- Where failures surface, given that `signal` returns `None`
- Which exceptions, if any, should not be caught
### Part 3 — Reentrant signals
A callback may itself call `signal()`, either for the id that is being dispatched or for another id whose callbacks signal back. That can recurse without end. How do you detect and prevent such reentrant loops?
```hint What is in flight
Consider what the registry would need to remember so that a nested `signal` call can be told apart from a top-level one.
```
```hint Not every nested signal is a bug
A callback for one id signaling a different id can be legitimate. Decide which nested calls to allow, and how to bound the rest.
```
#### Clarifying Questions for this Part
- Is a nested `signal` for a different id allowed, or is every nested call an error?
- When a loop is detected, should the nested call be dropped, deferred until the current dispatch finishes, or reported as an error?
#### What This Part Should Cover
- How reentrancy is detected, including indirect cycles across ids
- The policy applied when a loop is detected, and why
- An argument that every top-level `signal` call terminates, and tests that show it
### What a Strong Answer Covers
- Clarifying questions asked before coding, covering arguments, duplicates, unknown registrations, ordering, errors and reentrancy
- Working code whose behavior matches the example exactly
- An explicit, defended policy for each edge case rather than accidental behavior
- Tests that pin down each policy
- The cost of each operation
### Follow-up Questions
- A callback unregisters itself, or registers a new callback for the same id, while `signal` is running the callbacks. What does your implementation do, and what should it do?
- How would you make the registry safe to use from several threads without deadlocking when a callback calls back into it?
- The registry holds strong references to callbacks, which keeps the objects behind bound methods alive. How would you avoid that leak, and what would break?
Overview: Implement a minimal in-process signaling system with register_callback, unregister_callback and signal functions that all return None. Tests requirement clarification, callback storage and hashability, execution order, isolation of failing callbacks, and prevention of reentrant signal loops.
Read the full Candid Health Software Engineer interview experience this question came from