The server/client split & the lifecycle seam¶
transport is the last module extracted from go-tool-base's transport layer, and it is
the one that draws two deliberate lines: between server and client, and between the
framework-free server code and the framework config that drives it.
Why a server module at all¶
The client factories shipped first — go/httpclient
and go/grpcclient — precisely because a client is
light: it needs TLS and the transit middleware, nothing more. A server is heavier. It
needs a lifecycle: ordered startup, health reporting, graceful shutdown. That lifecycle
is go/controls, and transport is the one extracted
module that legitimately depends on it. Keeping the servers out of the client modules is
what lets a client-only consumer stay free of controls, authn and the gateway — a
boundary each client module's depfootprint test enforces.
So the split is not cosmetic: it is the reason the dependency graphs stay honest. transport
carries the heavy transport dependencies because it is the thing that actually runs a
service; everything lighter was extracted first so it never has to.
The framework seam: what's here vs. what stays in go-tool-base¶
This is a clean break, not a facade. The pure server API lives here: constructors
that take typed ServerSettings, health handlers, auth middleware, security headers, the
gateway. What stays in go-tool-base is only the config glue — the
*FromContainable / *FromConfig adapters that read a service's Viper-backed
configuration, derive a ServerSettings, and call into this module.
go-tool-base/pkg/http gitlab.com/phpboyscout/go/transport/http
NewServerFromContainable(cfg) ─────► NewServer(ctx, settings, handler)
(reads config keys, builds settings) (framework-free construction)
That seam is why the module can forbid go-tool-base, Viper, Cobra and Charm from its
graph while GTB tools keep their one-call …FromContainable ergonomics. A tool that does
not use the GTB config container builds its ServerSettings by hand and calls this
module directly.
The lifecycle seam: controls¶
The servers expose their lifecycle as go/controls primitives rather than running
themselves. Start/StartWithTLSPair return a controls.StartFunc, Stop a
controls.StopFunc, Status a controls.StatusFunc, and Register wires all three (plus
health) into a supervised service. Health endpoints are mounted outside the auth chain,
so a liveness probe never depends on a valid credential.
controls supervisor
├─ Start → bind listener, serve
├─ Status → health / readiness
└─ Stop → GracefulStop, then force after the deadline
This means the same server construction works whether you run it standalone
(srv.ListenAndServe()) or under a controls supervisor coordinating several services'
startup order and shutdown.
Relationship to go/transit¶
The middleware a server composes — request logging, OpenTelemetry, circuit breaking, rate
limiting — is not here; it lives in go/transit and
is consumed through a Chain (HTTP) or an InterceptorChain (gRPC). transport owns the
server shell and its lifecycle; transit owns everything that wraps a request or an RPC on
the way through.