Skip to content

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.