#engineering #architecture #networking
Rob Bogie

Stop Playing Priority Tetris: A Better Way to Handle Domain Routing

How a right-to-left hierarchical approach to domain resolution eliminates the need for manual priority weights and magic numbers

A tree with lit up branches (Photo by Rod Long on Unsplash)

Configuring a reverse proxy or a local development tool often feels like a chore. You start with one or two simple rules. Then you add a wildcard. Soon you have a list of fifty rules and a massive headache.

The main issue is ambiguity. If you have a rule for *.myapp.test and another for api.myapp.test, which one should the system choose? Many routing implementations solve this with manual weights. You spend your afternoon assigning priority numbers like 10, 50, or 100. If you get the order wrong, your API traffic ends up in the wrong service.

I wanted a system where the most specific match is chosen automatically. By combining a specific data structure with a literal scoring algorithm, you can make domain routing feel intuitive.

The Problem with Left-to-Right Thinking

To solve domain routing, you first have to realize that domains are written backwards. We read them from left to right. However, the actual hierarchy of a domain works from right to left.

In the domain api.staging.myapp.test, the .test part is the most general. The api part is the most specific tip of the leaf. If you try to process this as a standard string, you are fighting the natural shape of the data.

The solution is to split the domain by the dots and reverse the segments. Instead of a single string, you get a clear path: ["test", "myapp", "staging", "api"].

When you store these in a Reverse Radix Tree, every domain ending in .test shares the same root. As the resolver walks down the branches, it naturally narrows its search. It moves from the general Top-Level Domain (TLD) down to the specific subdomain. This structure is the foundation for everything else.

Literal Density: The Scoring Secret

How do we decide which pattern is “better” without asking the user for a priority number? We use a concept called Literal Density.

Every segment in a domain can be a static string, a wildcard, or a pattern. We calculate a specificity score based on how many literal characters are in that segment. A literal character is any fixed part of the string that is not a wildcard or a parameter placeholder.

PatternLogicScore
*Pure wildcard0
api-*Prefix with wildcard4
{tenant}Named parameter0
apiExact matchHigh (Implicit)

The resolver follows a simple rule at every level of the tree. It tries an exact match first. If no exact match exists, it looks at the available patterns. It then picks the pattern with the highest literal score. This is deterministic. There are no magic numbers or best guesses. A more “complex” pattern is objectively more specific.

The Five-Service Hierarchy

To see how this works in practice, consider five services that would typically cause a routing conflict. In a tree-based system, they live together without overlapping.

  1. bar.testService 1 (Exact match)
  2. *.bar.testService 4 (A wildcard subdomain)
  3. foo.*.testService 2 (A wildcard middle segment)
  4. bar.*.testService 3 (Another wildcard middle segment)
  5. *.testService 5 (The catch-all)

In many proxies, you would have to carefully order these rules in a configuration file. In a reverse radix tree, the hierarchy acts as a natural filter.

Scenario A: The Specific Subdomain

Imagine a request for random.bar.test. The resolver starts at test. It finds an exact match for bar. It moves into that branch. Once inside the bar branch, it looks for the next segment: random. It does not find an exact match. It falls back to the wildcard * that belongs specifically to the bar node. Result: It hits Service 4.

Scenario B: The Nested Match

Imagine a request for bar.something.test. The resolver starts at test. It looks for an exact match for something. It finds nothing. It falls back to the root wildcard * directly under test. Inside that wildcard branch, it looks for the next segment: bar. It finds an exact match. Result: It hits Service 3.

Scenario C: The Catch-all

Imagine a request for other.test. The resolver starts at test. It looks for other. It finds nothing. It falls back to the root wildcard *. Since there are no more segments in the request, it checks if this node is a terminal. Result: It hits Service 5.

The wildcards only compete with each other if they share the same parent. Service 3 and Service 4 never even meet during the search.

Dynamic Upstreams: Named Parameters

Matching a domain is only half the battle. You often need the routing to be dynamic. This is where Named Parameters come in.

You can define a rule like {tenant}.myapp.test. In the tree, {tenant} acts like a wildcard. However, it has a special job. When the resolver matches this segment, it captures the value.

These parameters are not just metadata. They are used to hydrate your service configuration. For example, you might set an upstream fallback to http://{tenant}.internal:8080.

If a user visits apple.myapp.test, the system identifies “apple” as the tenant. It then dynamically routes the request to http://apple.internal:8080. This allows you to create a single service rule that handles an infinite number of dynamic upstreams. You do not need to create a new rule for every single customer or project.

Performance and the Trade-offs

There is a common misconception that all proxies are equally fast at scale. Many routing libraries or simple middleware implementations use a linear search. They iterate through a list of regex patterns one by one. If you have 1,000 rules, the system might perform 1,000 checks for every single request.

The reverse radix tree is designed for speed, but the complexity depends on how you use it. For exact matches, the complexity is O(L), where L is the number of segments in the domain. You simply perform a hash map lookup at each step.

However, when you introduce patterns, you enter the Pattern Room. At any given level, the resolver must scan through the list of patterns. If you have dozens of patterns at the same level, the complexity for that segment becomes O(P), where P is the number of patterns. Even in the worst case, this is much faster than a global search. The tree isolates the search to only the patterns that are relevant to that specific branch.

Why We Gave Up “Depth-Blind” Regex

Most tools stick to linear searches because they allow for complex regular expressions that span multiple domain levels. A single regex can match across an entire hostname regardless of how many dots it contains.

By using a tree, we give up that “depth-blind” matching. You must specify your rules level by level. In practice, this is rarely a limitation. It actually mimics how real DNS rules work. In the real world, a wildcard only covers a single level in the domain hierarchy. By following this logic, we gain massive performance and predictability without losing the features you actually need.

Summary

We often try to solve complexity by adding more configuration. We add weights, priorities, and flags. By changing our perspective and using a data structure that mirrors the problem, we can remove that configuration entirely.

Reversing the domain and scoring the segments creates a set and forget experience. The system behaves exactly how a developer expects it to. You get to stop playing priority Tetris and get back to building your actual project.

Stop fixing these issues manually.

Lode automates everything you just read about. Join the beta to get a Free Lifetime Pro License.

Get Lode Pro