# Middleware Router — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks ("why is that a
separate class?") and the answer that earns the mark: **what change the seam anticipates, and what
the change would cost without it.**

Fourteen small files. `Router` holds the *sequence* of registering, matching and dispatching, and
delegates every rule that a requirement could reach — pattern parsing, precedence, chain wiring —
to something else.

| File | One reason to change |
|---|---|
| `Router` | the *sequence* of register / match / handle changes |
| `RouterFactory` | what a standard router is made of changes |
| `Segment`, `StaticSegment`, `ParamSegment`, `WildcardSegment` | what a segment kind is, or how many kinds exist |
| `PatternCompiler` | how a pattern string or a path is split and classified changes |
| `CompiledRoute` | what "compatible with this path" or "a duplicate" means changes |
| `MatchPolicy`, `SpecificityMatchPolicy` | how precedence decides between compatible candidates changes |
| `MiddlewareChain` | how a list of middleware becomes one pipeline changes |
| `Demo` | what we show when we run it changes |

## The measurement, first, because it is the argument

Three requirement changes, each absorbed independently from the base reference, each measured by
`server/lib/diff.mjs`:

| Curveball | Where it landed | Measured cost |
|---|---|---|
| a catch-all `**` matching one or more trailing segments | a new `Segment`, plus the one method each of `PatternCompiler` and `CompiledRoute` that had to state the new fact | **16 lines** |
| method-aware routing (GET/POST as part of the key) | a new field on `CompiledRoute`, and `Router.dispatch()` gaining a real second precedence dimension | **47 lines** |
| an auth gate that must short-circuit before an access log ever sees a rejected request | nothing — `use()` already meant this | **0 lines** |

`MatchPolicy` and `PatternCompiler` were touched by **at most one** of the three curveballs each,
and `MiddlewareChain` — the entire chain/decorator seam — was touched by **none of them.** That is
the claim this design makes about where the two halves of the problem, routing and middleware,
actually live, and it is measurable rather than aesthetic.

---

## Seam 1 · `Segment` (sealed) — a closed model of an open-looking problem

**Anticipates:** a new kind of pattern segment.

`StaticSegment`, `ParamSegment`, `WildcardSegment` are a `sealed interface` on purpose: the
contract names exactly three kinds, so exhaustiveness is real today, not aspirational. Every place
that inspects a segment's kind does it with `instanceof` pattern matching against the sealed set,
never a `String` tag or an `int` code.

**The one thing deliberately not added ahead of time:** a fourth permitted type,
`CatchAllSegment`, for the catch-all curveball. Adding it before the requirement existed would
have made `rank()` a method with one real caller and one invented one — the same
single-implementation smell `shopping-cart`'s decision log calls out in its own rejected
`StackingRule`. Sealing the interface costs one line to extend (`permits ... CatchAllSegment`)
when the requirement actually lands, and that line is exactly and only what changed — see the
curveball 1 patch.

## Seam 2 · `PatternCompiler` — one place a slash means one thing

**Anticipates:** any future disagreement between how a pattern is split and how a path is split.

`split(String)` is called by both `compile(String)` (patterns) and, via `Router`, every incoming
path. There is no second implementation anywhere that also happens to split on `/` — if there
were, "does a trailing slash matter" could quietly get two different answers depending which
copy a change landed in. This is what makes "a leading or trailing slash makes no difference"
provable rather than merely tested: there is exactly one function capable of disagreeing with
itself, and it can't.

## Seam 3 · `CompiledRoute` — one registered pattern's whole story

**Anticipates:** anything about what makes a route *compatible* with a path, and what makes two
registrations the same route.

Three responsibilities share one small object because they are three questions about the same
fact — "this pattern, compiled" — not three unrelated concerns bolted together:
`shapeKey()` (is this a duplicate), `specificity()` (how good a match is this), `tryMatch(...)`
(does this match at all, and what did it capture). The method-aware curveball needed a fourth
question of the same kind — "does this route accept this request's method" — and it landed here,
next to the other three, rather than in a side table `Router` would have had to keep in sync.

## Seam 4 · `MatchPolicy` — the matcher policy, injected

**Anticipates:** a different precedence rule, and — more importantly here — a
*second, narrower* question that should not corrupt the first.

`MatchPolicy.pickBest(List<CompiledRoute>)` is the one place "which candidate wins" is decided,
and `Router` calls it rather than comparing candidates itself. The payoff was not a hypothetical
alternate precedence rule — it was that `Router.dispatch()` could add a method-specific-beats-
any-method rule **in front of** path precedence, by filtering the candidate pool before handing
it to the unchanged policy, instead of teaching the policy about a request it is not given. A
version of this reference that had `dispatch()` reach into `SpecificityMatchPolicy`'s comparator
directly measured a few lines smaller and was rejected for exactly that reason — see curveball 2's
`PATCH.md` for the full trade.

**Why `match(String)` stays untouched by both curveballs that could plausibly have reached it.**
It answers one question — "what would match this path" — and nothing in either curveball needed
that question to change; catch-all changes what "compatible" means for one pattern shape (that
lives in `CompiledRoute`), and method-awareness changes a question `match` was never asked
(`dispatch` alone knows the method). A `match` that had somehow grown a method parameter "just in
case" would have been exactly the kind of speculative generality this log elsewhere argues
against — see `MiddlewareChain`, below, for the same argument the other direction.

## Seam 5 · `MiddlewareChain` — the whole chain/decorator seam, and only that

**Anticipates:** everything about how middleware compose, none of which turned out to require a
change across three curveballs.

`MiddlewareChain.wrapping(List<Middleware>, Chain terminal)` is one static method: fold the list
right-to-left around a terminal step, in registration order. It has no idea what a middleware
does, whether it short-circuits, or whether the terminal step is a router dispatch or something
else entirely — that is what let curveball 3 (the auth-gate-then-audit-log requirement) cost
**zero lines**: composing two `Middleware` instances in the right order was already the entire
capability `use()` promised, and the requirement's "the log must never see a rejected request" is
not a rule this class enforces — it is what "did not call `chain.next`" has always meant, stated
from a different direction.

**The rejected alternative, because it is the one a template-method instinct reaches for.** An
earlier draft considered giving `Middleware` a second lifecycle hook — `beforeRoute` /
`afterRoute` — so "things that run after a match" would not have to be expressed as
post-processing inside `handle`. It was dropped: the contract already lets a middleware inspect
and alter whatever `chain.next(...)` returns, which *is* an after-hook, and a second, named
mechanism for the same capability would have been two ways to do one thing with no requirement
asking for the second. See `B8`, dropped below, for the same call made explicit.

## Seam 6 · `RouterFactory` — construction has an owner

**Anticipates:** a different kind of standard router — a different `MatchPolicy`, chiefly.

`Router` has no no-argument constructor. The one thing `RouterFactory.standard()` currently
decides — which `MatchPolicy` to wire in — was never asked to vary by any of the three curveballs,
which is worth stating rather than treating as evidence the seam was unnecessary: a factory that
is not yet exercised by a scale-up is not the same failure as a builder invented for a scale-up
that never came. `Entry.create()` points here so that "what a router is made of" was never a fact
`Router` itself had to know.

## Errors — reused, not invented

The contract fixes `IllegalArgumentException` for a malformed pattern and
`IllegalStateException` for a duplicate registration, matching `shopping-cart`'s convention: a bad
*argument* against a bad *situation*. No custom exception hierarchy was built for this problem —
see `A6`, dropped below — because the two stock exceptions already say everything this problem
needs said, and a hierarchy over two members is ceremony.

## What was deliberately not built

Named so the scope is explicit rather than forgotten:

- **No trie.** The research that shaped this problem's requirements names a trie (`RouteNode`) as
  the natural structure, and it is a defensible one — but it optimises for a route count this
  problem never has. A linear scan over `CompiledRoute` plus an injected `MatchPolicy` gives the
  identical precedence guarantee, is easier to reason about position by position, and — the
  deciding factor — makes precedence a single, independently testable comparator rather than
  logic folded into how nodes are walked. The honest boundary: a router with tens of thousands of
  patterns wants the trie's lookup complexity, and this one does not have tens of thousands of
  patterns.
- **No per-route middleware.** Every `Middleware` applies to every request; there is no way to
  attach one to a single registered pattern. Nothing in the requirements asked for it, and adding
  it would have meant `CompiledRoute` growing a list of its own, with its own ordering question,
  for a capability the base problem, and all three curveballs, never needed.
- **No headers, no query string, no body.** `Request` carries a path and a method; `Response`
  carries a status and a body. A real request has more shape than that, and the honest boundary
  is that the day query-string-based routing or a header-based feature flag is asked for, it is
  `Request` that grows, at the one place records cross the boundary — not a workaround bolted
  onto `Router`.
- **No caching of a compiled route table keyed by path.** Every `match` and every `dispatch`
  recompiles nothing (patterns are compiled once, at `register` time) but does re-walk the route
  list on every call. For the scale this problem states, that is a handful of comparisons; an
  index would be an optimisation with no requirement asking for it and a real invalidation
  question the day a route is ever removed — which nothing here supports either, for the same
  reason.

## Syllabus items assigned to this problem, and honestly not tagged

**Tagged:** A2, A3, A5, A7, B1, B2, B7, C1, C2, C3, D1, D2, D3, F1–F5.

**Dropped, with reasons — do not re-tag without re-checking the design actually exercises them:**

- **A1** (enum carrying state and behaviour) — there is no stateful enum in this problem; the
  closest candidate, `Segment`'s kinds, is a sealed interface (tagged as A3) rather than an enum,
  because a `ParamSegment` carries a name no enum constant can.
- **A4** (entity vs value, identity) — every type here is a value; nothing in this problem has an
  identity distinct from its data.
- **A6** (domain exception hierarchy) — the contract fixes two stock exceptions; see "Errors"
  above. A hierarchy over two members would be invented, not exercised.
- **B3** (builder) — `RouterFactory.standard()` is a factory method, not a builder; nothing about
  a router's construction is wide enough (one collaborator today) to want one.
- **B4** (observer/listener) — there is no notification mechanism distinct from the middleware
  chain itself; modelling `Middleware` as an "observer" would be the same object wearing a second,
  redundant name.
- **B5** (repository/storage abstraction) — registered routes live in a plain `List`, in memory,
  for the process's lifetime. There is no persistence question this problem asks.
- **B6** (state machine as data) — this router has no state machine; a request either matches or
  it does not, once, per call.
- **B8** (template method / lifecycle hook) — deliberately rejected; see the "rejected
  alternative" under `MiddlewareChain` above. `Middleware` composition already provides a before-
  and-after hook via `chain.next(...)`'s return value, and adding a second, named mechanism for
  the same capability would be redundant machinery, not a genuine exercise of this item. This is
  the weakest-considered tag in this log precisely because it was close, and it is being named
  and declined rather than quietly omitted.
- **C4** (composition over inheritance) — nothing here uses inheritance at all (no class extends
  another concrete class), so there is no inheritance being *avoided* in favour of composition —
  the contrast the item is about does not arise.
- **C5** (interface segregation) — `MiddlewareRouterApi` is already four cohesive methods; there
  is no fat interface here being split into role-specific ones.
- **D4** (bulk/pagination shape) — nothing in this problem is a collection a caller would want a
  page or a batch of.
- **E1–E5** (concurrency) — the contract states the grader is single-threaded, matching this
  problem's assignment (no stress suite). No lock, no atomic, no thread-safe collection choice is
  exercised, and adding one would be unrequested and untested generality.

## The weakest tag actually kept, named rather than hidden

**A3** (sealed hierarchy for a closed set of variants). It is genuinely exercised — `Segment` is a
real `sealed interface` with exhaustive `instanceof` handling and a `permits` clause the catch-all
curveball has to extend — but it is also the most mechanical of the tags kept here: modelling three
or four pattern kinds as a sealed type is close to the default choice once Java 21 is on the table,
not a decision that took judgement the way the `MatchPolicy` injection or the `match`/`dispatch`
split did. It earns the tag; it is the one a reviewer should press on first if this problem's
coverage is ever re-checked.
