Katkat

Build a ChatOps Command Dispatcher That Matches Named Arguments

GoogleSoftware Engineer · Technical Screen

Last updated

Implement a command dispatcher for a chat ops bot that resolves slash-separated commands with named arguments to a registered handler. Tests trie-based matching, precedence between literal and parameter segments, and handling of ambiguous or dead-end routes.

You're building the command dispatcher for a chat ops bot. Teammates type commands like /deploy prod status in a chat channel, and the bot must find the right handler for each one. Internally, a command is represented as a slash-separated string, and you need to support two operations:

  • register(pattern, handler) — registers a handler for a command pattern.
  • resolve(command) — finds the handler whose pattern matches a concrete command string, and returns it together with the values captured by any named arguments.

A pattern segment written as :name is a named argument and matches exactly one token: the pattern deploy/:service/status matches the command deploy/prod/status with service = "prod". A segment without a leading colon must match literally.

Assume a helper already exists to split a command into tokens:

def tokenize(command: str) -> list[str]:
    """'deploy/prod/status' -> ['deploy', 'prod', 'status']"""

A starting interface (feel free to adjust it once you've made your design decisions):

from typing import Callable

Handler = Callable[..., object]


class CommandRouter:
    def register(self, pattern: str, handler: Handler) -> None: ...
    def resolve(self, command: str) -> tuple[Handler, dict[str, str]] | None: ...

Example

register("health", health_handler)
register("deploy/prod/status", status_prod_handler)
register("deploy/:service/status", status_any_handler)
register("deploy/prod/logs", logs_prod_handler)
register("deploy/:service/restart", restart_any_handler)

resolve("health")                 -> (health_handler, {})
resolve("deploy/prod/status")     -> (status_prod_handler, {})
resolve("deploy/staging/status")  -> (status_any_handler, {"service": "staging"})
resolve("deploy/prod/restart")    -> ???
resolve("deploy/staging/restart") -> (restart_any_handler, {"service": "staging"})

Constraints

  • Up to 10,000 registered patterns, each with up to 20 segments, and up to 10^5 resolve calls. A resolve should not need to test the command against every registered pattern.
  • tokenize has already removed any surrounding slashes; you don't need to write it.

Decide and State

  • When both a literal segment and a named argument could match at the same position — like deploy/prod/status versus deploy/:service/status matching the command deploy/prod/status — which one wins?
  • If a literal branch matches the first segments but has no route for the rest, as in resolve("deploy/prod/restart") above (where deploy/prod/logs exists but not deploy/prod/restart), should the router fall back and try deploy/:service/restart instead? Pick a behavior, implement it, and be ready to defend the trade-off.

Hints

Hint 1

Many patterns share a prefix. Build a structure that lets a lookup walk the command once, segment by segment, instead of testing it against every registered pattern.

Hint 2

At each node, store at most one literal child per segment value and at most one named-argument child, plus its parameter name.

Hint 3

Pick one precedence rule for literal vs. named argument at the same node and apply it consistently everywhere, including at the final segment.

Hint 4

Decide whether a dead end on a literal branch should backtrack into a sibling named-argument branch, and know what that trade-off costs you.

Follow-up questions

  • How would you add a trailing wildcard segment, like `files/*rest`, that captures everything remaining?
  • How would you add backtracking so `deploy/prod/restart` could still fall through to `deploy/:service/restart` when the literal branch dead-ends, without turning every lookup into a full search?
  • How would you extend this to dispatch on both a command and a method, like a GET and a POST on the same pattern?
  • How would you support registering and unregistering patterns at runtime while other threads are calling resolve?