skip to content

How do you decide whether http.ServeMux is enough to replace a service's third-party router, and who owns that call?

level: principalimportance: nice to knowfreq 26%

answer

  1. compare features, not preferences
  2. what does the dependency still earn
  3. the semantics differ more than the syntax
  4. encode today's behaviour before you change it
  5. the toolchain floor and the platform group

basics

~20 s

Inventory what the router does that the standard mux does not, price the migration against a route-behaviour test, and weigh one less dependency against churn with no user-visible gain. The service lead proposes; a platform group can overrule.

solid answer

~50 s

Make it an inventory, not a preference. Since Go 1.22 `http.ServeMux` matches methods, single-segment and trailing wildcards, host patterns, resolves overlaps by specificity and answers a wrong method with 405 plus `Allow` — which covers a plain resource API completely. List what your current router does beyond that: typed or regular-expression constraints on segments, mount hierarchies, route-level configuration, ordered first-match semantics a handler quietly depends on. If that list is empty, the migration buys a smaller dependency surface and one fewer thing a new engineer must learn; if it is not, the router is earning its place. Price the risk honestly: the semantics differ, so a mechanical port can move traffic silently, and it only lands behind a route table test written against the *current* router first. Then name the constraints you do not own — a Go 1.22 toolchain floor, and a platform standard that can override the service.

go deeper

for a junior

Know that since Go 1.22 the standard library matches methods and path variables, so reaching for a routing dependency is no longer the automatic first move on a new service.

for a middle

Explain the semantic differences a port would hit: specificity instead of first-match, a panic on conflicting patterns, and the trailing-slash subtree redirect.

for a senior

Describe the safe sequence — encode today's routing in a test against the current router, put registrations behind one constructor, then swap — and name what the standard mux cannot do.

for a principal

Own the decision and its stakeholders: dependency budget, toolchain floor, review surface for new engineers and opportunity cost, and accept that a platform standard can override your technical preference.

## Why this is a real decision and not a style argument Until Go 1.22 the standard mux matched path prefixes only: no methods, no path variables. Every service with a REST-shaped API therefore either hand-rolled dispatch or took a routing dependency, and taking one was the obvious call. Go 1.22 changed the premise, so services carrying a router now carry it by inertia rather than by necessity — and inertia is exactly the kind of thing a lead is expected to periodically re-examine and, often, to leave alone. ## Step one: inventory, from the code and not from memory Enumerate every routing feature the service actually uses. The standard `ServeMux` covers: - method-constrained patterns, with GET also serving HEAD; - `{name}` single-segment and `{name...}` trailing wildcards, read back by name; - host-qualified patterns; - subtree patterns via a trailing slash, plus `{$}` to anchor an exact path; - overlap resolution by specificity, with conflicting pairs rejected at registration; - 405 with an `Allow` header when a path exists under other methods. What it does not offer, and what you must therefore find in your code: constraints on the *shape* of a path segment (numeric-only, a regular expression), route grouping with shared configuration attached to a subtree, route-level hooks, and any behaviour that depends on ordered first-match evaluation. Also count the places that read routing metadata the standard library exposes differently. If that list comes back empty, the case for migrating is a dependency you no longer patch, upgrade or vet, and a routing model a new hire already knows from the standard library. If it comes back with three entries you would have to reimplement, migrating means writing and owning routing code, which is strictly worse than importing it. ## Step two: price the risk, which is behavioural, not mechanical The patterns look similar enough to invite a find-and-replace, and that is the trap. Two semantic differences bite: 1. **Precedence.** Many routers evaluate routes in registration order and take the first match. `ServeMux` compares patterns and takes the most specific one, regardless of order. A port therefore changes which handler serves overlapping paths, and it does so without any error, because a narrower pattern beating a broader one is legal and silent. 2. **Trailing slashes.** A trailing slash makes a pattern a subtree, and a request for the slash-less root gets a redirect rather than a direct match. That converts one request into two and, on older toolchains, can rewrite a POST into a GET. The mitigation is a sequencing rule: write the route-behaviour test **against the existing router first**, so it encodes today's production behaviour rather than tomorrow's intention, then swap the implementation and require the same table to stay green. Any row that has to change during the swap is a deliberate behaviour change that belongs in the pull-request description and, if it touches a public URL, in a release note. ## Step three: the constraints you do not own - **Toolchain floor.** The pattern syntax needs Go 1.22 or later; the subtree redirect is 307 rather than 301 only from Go 1.26. If the fleet's minimum is older, the toolchain upgrade is the real project and routing is a rider on it. Pin the floor in `go.mod` and say why. - **Platform standards.** If a platform or infrastructure group has standardised the stack — a shared service template, a common set of libraries, a security review keyed to specific dependencies — then a service diverging in either direction has a cost the service team does not pay. That group can overrule you, and the correct posture is to bring the inventory and the test plan as a proposal rather than to land the change and defend it afterwards. - **Opportunity cost.** Routing churn is invisible to users. In a quarter with a reliability or delivery goal, a migration with no user-visible benefit is the wrong use of the change budget, however sound the technical argument. "Correct, and not now" is a complete answer. ## Step four: sequence it so it is reversible Put all registrations behind one constructor returning the assembled handler. That single seam makes the swap a one-file change, keeps the test pointed at the real thing, and makes reverting a revert rather than an archaeology exercise. Land the test, land the constructor, then land the swap as its own commit. ## What a strong answer sounds like It names the specific capabilities on both sides rather than arguing "fewer dependencies is better", it identifies the silent-reroute risk and the test that removes it, it states the toolchain floor as a hard constraint, and it is explicit about who decides: the service's lead proposes and owns the outcome, the platform group can veto on standardisation grounds, and the honest answer is sometimes to leave a working router alone.

  • What would keep you on the third-party router?
    Concrete features the standard library lacks: constraints on the shape of a path segment, route grouping with shared configuration, route-level hooks, or handler code that relies on ordered first-match evaluation. Also a stable, well-maintained dependency and a team with no capacity for invisible churn — a router that is not costing anything is not a problem to solve.
  • How do you sequence the migration so it stays reversible?
    Put every registration behind one constructor that returns the assembled handler, then write the route-behaviour test against the current router so it captures today's behaviour. Swap the implementation as its own commit with the table unchanged, so any row you must edit is a visible, deliberate behaviour change and a revert is a single-file revert.
  • What version cost does standardising on the standard mux carry?
    A Go 1.22 minimum for the pattern syntax itself, and a behavioural difference between 1.25 and 1.26 in the subtree redirect status. A team on an older toolchain has to do that upgrade first, so pin the floor in `go.mod` and record the reason where the next reader will find it.

saying these in an interview costs you the question

  • Argues fewer dependencies is always worth the churn
  • Ports the routes with no test pinning which route matches
  • Assumes the standard mux evaluates routes in registration order
  • Treats the choice as personal taste and skips the platform group
  • Claims the standard library still cannot match methods or path variables
  • Ignores the toolchain floor the pattern syntax requires