Skip to content
Bhavansh Gupta
Back

HTTP Router from First Principles

Oct 11, 2026- Oct 11, 2026
•
Java 21, Data Structures, Trie, Radix Tree, Benchmarking

HTTP Router from First Principles: Linear Scan vs. Trie vs. Radix Tree

A hands-on exploration of how HTTP routers work under the hood, why naive routing breaks at scale, and how Prefix Trees (Tries) and Compressed Trees (Radix Trees) eliminate algorithmic bottlenecks.


1. The Core Problem: Why Routers Are Hard

An HTTP router maps an incoming request (e.g. GET /api/v1/users/42/items/5) to a handler function.

Why not just use a HashMap<String, Handler> for O(1)O(1) instant lookups?

To support dynamic parameters, routers must perform pattern matching. The choice of data structure determines whether your router handles 10,000 routes with ease or collapses under load.


2. The Three Architectures

                    ┌───────────────────────────┐
                    │   interface HttpRouter    │
                    └─────────────┬─────────────┘
                                  │
         ┌────────────────────────┼────────────────────────┐
         ▼                        ▼                        ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ LinearScanRouter │    │    TrieRouter    │    │ RadixTreeRouter  │
│  (Brute Force)   │    │  (Segment Trie)  │    │ (Compressed Tree)│
│       O(N)       │    │       O(L)       │    │       O(L)       │
└──────────────────┘    └──────────────────┘    └──────────────────┘

Architecture 1: Linear Scan (LinearScanRouter) — O(N)

Stores all registered routes in a list or insertion-order map (LinkedHashMap).

Incoming: GET /api/v1/users/usr_1005/items/5

Check Route 1:    /api/v1/services/svc0/health          -> Compare & Fail
Check Route 2:    /api/v1/services/svc1/health          -> Compare & Fail
...
Check Route 1000: /api/v1/users/:userId/items/:itemId   -> Match! (Checked 1,000 routes)

Architecture 2: Segment Trie (TrieRouter) — O(L)

URLs are naturally hierarchical (/ delimited). A Trie (Prefix Tree) decomposes the URL into path segments, where every node represents one segment:

[root]
   └── "api"
         └── "v1"
               ├── "services"
               │     └── "svc0"
               │           └── "health" (GET Handler)
               └── "users"
                     └── [:userId]
                           └── "items"
                                 └── [:itemId] (GET Handler)

Architecture 3: Radix Tree (RadixTreeRouter) — O(L) Compressed

In a Segment Trie, unbranched chains like root -> "api" -> "v1" -> "services" require multiple nodes, multiple HashMap lookups, and memory pointer hops.

A Radix Tree (used in production engines like Go’s httprouter, Gin, and Echo) compresses single-child chains into a single edge:

Segment Trie (4 nodes, 4 pointer hops):
[root] ──► ["api"] ──► ["v1"] ──► ["services"]

Radix Tree (1 node, 1 pointer hop):
[root] ──────────────("api/v1/services")──────────────► [Node]

3. Empirical Benchmark Results

We benchmarked all three routers under strict JVM container limits simulating a cloud environment:

Master Throughput Comparison Table

RouterBest Case (Route #1)Worst Case (Route #1000)404 (Unmatched)Random Traffic (Average)
LINEAR4,192.97 req/s650.54 req/s (85% drop!)686.99 req/s1,361.82 req/s
TRIE4,156.94 req/s4,323.80 req/s3,892.90 req/s4,166.26 req/s
RADIX4,346.57 req/s4,294.34 req/s4,054.26 req/s4,302.22 req/s

What the Data Proves

  1. The Linear Scan Cliff: In LinearScanRouter, throughput collapsed from 4,192 req/s down to 650 req/s on Route #1,000 because each request evaluated all 1,000 route patterns.
  2. The Trie Flatline: In TrieRouter and RadixTreeRouter, Route #1,000 performed at ~4,300 req/s—virtually identical to Route #1! Path depth (LL), not route count (NN), dictates latency.
  3. Radix Efficiency: RadixTreeRouter delivered the highest overall throughput across all traffic modes due to edge compression and zero-allocation path walking.

Profiling Insights (Java Flight Recorder)

Using built-in JDK Flight Recorder (JFR), we profiled where the CPU spent time:


4. Running the Project & Benchmarks

1. Compile the Sources

cd http-router
javac -d out src/router/*.java

2. Run an Individual Server

You can start any of the three servers on http://localhost:8080:

# Run Linear Router Server
java -cp out router.LinearServer

# Run Trie Router Server
java -cp out router.TrieServer

# Run Radix Tree Router Server
java -cp out router.RadixServer

(You can customize port and route count: java -cp out router.Server --router=radix --port=9090 --routes=2000)

3. Test with curl

# Static route:
curl http://localhost:8080/api/v1/services/svc0/health

# Parameterized route (:userId):
curl http://localhost:8080/api/v1/users/usr_1005/items/5

# Wildcard route (*):
curl http://localhost:8080/static/images/avatar.png

# HTTP 405 Method Not Allowed:
curl -i -X POST http://localhost:8080/api/v1/users/me

5. Automated Benchmark Suite

We provide an automated benchmark runner that manages compilation, JVM resource constraints, server lifecycle, load testing, JFR profiling, and report generation:

# Benchmark a single router:
./benchmark/run_benchmark.sh linear
./benchmark/run_benchmark.sh trie
./benchmark/run_benchmark.sh radix

# Run all three routers sequentially and print the master comparison table:
./benchmark/run_benchmark.sh all

# View the summary comparison table from existing results:
./benchmark/run_benchmark.sh summary

Result Artifacts

Every run writes to its own isolated folder under benchmark/results/<router_name>/:


6. Project Structure

http-router/
├── src/router/
│   ├── HttpRouter.java        # Common interface contract
│   ├── Handler.java           # Functional interface: Response handle(Request req)
│   ├── Request.java           # Request record (method, path, params, query)
│   ├── Response.java          # Response record (status, body)
│   ├── RouteGenerator.java    # Generates 1,000 realistic API routes
│   │
│   ├── LinearScanRouter.java  # O(N) linear brute-force router
│   ├── LinearServer.java      # Server launcher for LinearScanRouter
│   │
│   ├── TrieNode.java          # Segment trie node
│   ├── TrieRouter.java        # O(L) segment prefix tree router with backtracking
│   ├── TrieServer.java        # Server launcher for TrieRouter
│   │
│   ├── RadixNode.java         # Edge-compressed radix node
│   ├── RadixTreeRouter.java   # O(L) compressed prefix tree with edge splitting
│   ├── RadixServer.java       # Server launcher for RadixTreeRouter
│   │
│   └── Server.java            # Shared virtual-thread HTTP server engine
│
├── benchmark/
│   ├── load_test.py           # High-concurrency benchmark client (Python stdlib)
│   ├── run_benchmark.sh       # Automated benchmark & JFR profiling runner
│   └── results/               # Benchmark outputs & flamegraph dumps
│       ├── linear/
│       ├── trie/
│       └── radix/
│
└── README.md                  # This guide

Share this post: