Event Topic Router With Named Placeholders and Tail Wildcards
GoogleSoftware Engineer · OnsiteLast updated
Build a router for a telemetry message bus that maps dot-separated topics to registered subscription patterns with named placeholders and a trailing multi-segment wildcard; tests trie design, precedence rules and backtracking.
A logistics company streams telemetry from its trucks over a message bus. Every message carries a topic made of dot-separated segments, such as fleet.t42.gps or fleet.depot.engine.e17. Services register patterns, and the bus must decide which single pattern handles each incoming topic.
Part 1 - Literals and named placeholders
Implement a class:
class TopicRouter:
def add(self, pattern: str) -> None: ...
def resolve(self, topic: str) -> tuple[str, dict[str, str]] | None: ...
- A pattern segment written as
<name>is a placeholder that matches exactly one topic segment. Any other segment must match literally. resolvereturns the matching pattern and the placeholder bindings, orNoneif nothing matches.- Precedence: if several patterns match, compare them segment by segment from the left; at the first position where they differ, a literal beats a placeholder.
- Registering the same pattern twice is a no-op. Registering a pattern that differs from an existing one only in placeholder names (e.g.
fleet.<truck>.gpsvsfleet.<vehicle>.gps) raisesValueError. - Empty segments (
fleet..gps) are invalid input.
add("fleet.<truck>.gps")
add("fleet.depot.gps")
add("fleet.<truck>.engine.<code>")
add("fleet.depot.engine.status")
resolve("fleet.t42.gps") -> ("fleet.<truck>.gps", {"truck": "t42"})
resolve("fleet.depot.gps") -> ("fleet.depot.gps", {})
resolve("fleet.depot.engine.e17") -> ("fleet.<truck>.engine.<code>", {"truck": "depot", "code": "e17"})
resolve("fleet.t42.brakes") -> None
Note the third case: the literal depot branch is tried first but dead-ends at e17, so the router must back up and try the placeholder.
Part 2 - Tail wildcard
Allow ** as the last segment of a pattern. It matches one or more remaining segments and binds them (joined with dots) under the key "**". At any position it has the lowest precedence: literal, then placeholder, then **.
add("fleet.**")
add("fleet.<truck>.engine.**")
resolve("fleet.t42.brakes") -> ("fleet.**", {"**": "t42.brakes"})
resolve("fleet.t9.engine.oil.temp")-> ("fleet.<truck>.engine.**", {"truck": "t9", "**": "oil.temp"})
resolve("fleet.t9.engine.e3") -> ("fleet.<truck>.engine.<code>", {"truck": "t9", "code": "e3"})
resolve("fleet") -> None
Part 3 - Analysis
State the time and space complexity of add and resolve, and argue why resolve cannot blow up exponentially on a fixed set of patterns.
Constraints
- Up to 10^5 patterns, up to 32 segments per topic, segment length up to 64.
resolveis called far more often thanadd.
Hints
Hint 1
Store patterns in a trie keyed by segment. Each node needs a map of literal children and at most one placeholder child (placeholder names don't affect matching).
Hint 2
Search depth-first: literal child first, then the placeholder child, and only return a hit once a full path succeeds.
Hint 3
A '**' pattern can be stored on the node *before* the wildcard - when you reach that node with segments left, it is a fallback match.
Hint 4
For bindings, re-walk the winning pattern's segments alongside the topic instead of storing names in the trie.
Follow-up questions
- Return all matching patterns in precedence order (fan-out delivery) instead of only the best one.
- How would you support a single-segment anonymous wildcard '*' alongside named placeholders, and what conflicts arise?
- How would you make add and resolve safe for concurrent use with millions of lookups per second?
- Add type constraints such as <truck:int>; how does that change precedence and the trie?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.