Transport stack & foundations — extraction sequence plan¶
- Authors
- Matt Cockayne, Claude Opus 4.8 (AI drafting assistant)
- Date
- 2026-07-13
- Status
- IMPLEMENTED (2026-07-18) — shipped to main; originally APPROVED (open questions resolved in review 2026-07-13; design points D1–D4 deferred to their phases)
Summary¶
With controls extracted, the next natural target is the transport stack
(pkg/http, pkg/grpc, pkg/gateway). But the transport packages import several
other not-yet-extracted GTB packages, and a framework-free module cannot
import go-tool-base (the depfootprint guard forbids it). So the transport stack
cannot go first: its dependencies must extract ahead of it. This plan establishes
the dependency-forced order, the shape of each extraction (pure repoint vs
facade, per playbook §11.1),
and the client/server module split for the transport layer.
This is a sequence/planning spec. Each package still gets its own extraction (a short spec or a direct §5 application) when its turn comes; this document fixes the order and the design decisions that span packages.
The dependency graph (measured 2026-07-13)¶
gateway → http(server), grpc, tls, config, logger
http → { client: tls, circuitbreaker, ratelimit }
{ server: authn, redact, tls, controls*, config, logger }
grpc → authn, redact, tls, circuitbreaker, ratelimit, config, logger (server-only)
config and logger stay in GTB — their coupling is already adapter-confined
(each transport package has a config_adapter.go; logging is the *slog.Logger
seam). controls is already external (go/controls). Everything else
(redact, tls, authn, circuitbreaker, ratelimit) must be resolved before
the transport stack can be framework-free.
Per-package shape¶
| Package | LOC | Consumers | Shape | Notes |
|---|---|---|---|---|
redact |
252 | 8 | pure repoint | 0 GTB imports; transport dep and widely used → extract first |
regexutil |
145 | 2 | pure repoint | independent of transport; ease-10 warm-up |
browser |
206 | 3 | pure repoint | independent of transport; ease-10 warm-up |
tls |
215 | 8 | facade | imports config+logger; config_adapter.go stays in GTB |
authn |
872 | 4 | pure repoint | 0 GTB imports |
circuitbreaker (internal) |
202 | 2 | pure | client-side RoundTripper; promote with the client module |
ratelimit (internal) |
84 | 2 | pure | server-side middleware; promote with the server module |
http |
2611 | 17 | facade | split client/server — see below |
grpc |
1715 | 2 | facade | server-only |
gateway |
309 | 0 | facade | composes http+grpc |
The client/server split & the module map (decided 2026-07-13)¶
Measured dependency footprints differ sharply between the client and server halves of both transport packages:
- Client (
http:client.go,client_middleware.go,retry.go, client-side circuit-breaker round-tripper;grpc:CircuitBreakerInterceptor/…Stream…client interceptors,OTelClientHandlerdial option): depends only ontls(client config) + the resilience/middleware primitives. Noauthn,redact, orcontrols. - Server (
http:server.go,auth.go,security_headers.go; all ofgrpcserver;gateway): depends onauthn,redact,tls,controls,config.
Both http and grpc therefore split cleanly into a light client half and a
heavy server half. The resulting module map (client extracted separately per your
call; shared middleware standalone per O1; grpc client included per O5):
go/transportmw(standalone shared middleware — O1) — the transport-neutral resilience primitives (circuitbreaker,ratelimit) plus the shared middleware used by both client and server: request/response chaining, logging (withredact), and OTel instrumentation. One module withhttpmw(RoundTripperhttp.Handlermiddleware) andgrpcmw(unary/stream interceptors + dial-option/handler) sub-packages. Deps:go/redact,go/tls, OTel contrib SDK,*slog. Consumed by every client and server module below.go/httpclient(client — O5) — hardenedhttp.Clientfactory wiring thehttpmwround-trippers. Light:go/transportmw+go/tls+ stdlib. Exactly whatgo/chatneeded; a client consumer never inheritscontrols/authn/gateway.go/grpcclient(client — O5, not deferred) — grpc client interceptors and dial options (circuit-breaker/retry/OTel client instrumentation). We have in-scope consumers that dial grpc without the server stack. Light:go/transportmwgo/tls.go/transport(server stack) — thehttpserver,grpcserver, andgateway, with server auth, security headers, server rate-limiting, and thecontrolshealth integration. One module, sub-packages (transport/http,transport/grpc,transport/gateway) sogateway → http/grpcstays an internal import with no three-way version lockstep. Requiresgo/transportmw,go/httpclient/go/grpcclient(where it reuses client bits),go/controls,go/authn,go/redact,go/tls,go/observability.
Why the client/mw split pays off: a single heavy module would force
controls/authn/gateway into every client-only consumer's go.mod even under
dead-code elimination. Splitting keeps the client + middleware graphs genuinely
light, which is the common reuse case.
Observability brought forward (O2). Transport instruments via the OTel
contrib SDK directly (not pkg/telemetry), so observability is not a hard
dependency — but per your call it extracts before the transport layer, so
go/transportmw and go/transport can consume go/observability for
provider/exporter setup rather than re-inventing it, and observability lands as an
independently useful module sooner.
Recommended sequence¶
Phase 1 — leaf warm-ups (pure repoints, exercise playbook §11):
redact→go/redact. First: it is a transport dependency and has the most consumers (8). Highest leverage; near-mechanical.regexutil→go/regexutil,browser→go/browser. Independent of the transport stack; low-risk warm-ups that can slot in anytime (good parallel or filler work).
Phase 2 — foundations:
tls→go/tls(facade;config_adapter.gostays in GTB).authn→go/authn(pure repoint). Independent oftls; either order.observability→go/observability(thetelemetry/otelcore+logs/metrics/tracinggroup; brought forward, O2). Independent of the analytics/telemetry split; extract the observability half now so the transport layer can consume it.
Phase 3 — shared transport middleware (O1):
go/transportmw— resilience primitives (circuitbreaker,ratelimit) + sharedhttpmw/grpcmwmiddleware. The clients and servers all sit on this.
Phase 4 — clients (O5, not deferred):
go/httpclientandgo/grpcclient— the light client factories / interceptors. Either order; both consumego/transportmw+go/tls.
Phase 5 — the server stack:
go/transport—httpserver +grpcserver +gateway, promoted together; config adapters stay in GTB; consumes every module above +go/controls.
Each phase updates the extraction-report readiness checklist and applies the
per-component §5 procedure. The order is dependency-sound at every step: a module
never imports a still-in-tree go-tool-base/pkg/* its depfootprint guard forbids.
Resolved decisions (review 2026-07-13)¶
- O1 — Shared middleware = its own standalone module.
go/transportmwholds the resilience primitives + the sharedhttpmw/grpcmwmiddleware; clients and servers depend on it (not folded intohttpclient). - O2 — Observability brought forward. Extract
go/observabilityin Phase 2, before the transport layer, so the middleware/servers can consume it. (Not a hard dependency — transport uses the OTel contrib SDK directly — but sequenced early by choice and independently valuable.) - O3 — Naming.
go/httpclient+go/grpcclient+go/transportmw+go/transport. - O5 — grpc client not deferred.
go/grpcclientships in Phase 4 alongsidego/httpclient; there are in-scope consumers that dial grpc without the server.
Remaining design points (resolve within the relevant phase)¶
- D1 —
transportmwinternal shape. One module withhttpmw/grpcmwsub-packages (proposed, mirrors thego/transportsub-package choice) vs. two modules. Confirm when reading the actual middleware files. - D2 — Resilience-primitive home. Keep
circuitbreaker/ratelimitinsidego/transportmw(proposed — their only consumers are transport middleware) vs. a separatego/resilience. Split only if a non-transport consumer appears. - D3 —
observabilityscope vs. the telemetry split. Confirm theotelcore+logs/metrics/tracinggroup extracts cleanly without the rootpkg/telemetryanalytics/observability split — the report marks it "ready now", but verify config confinement at extraction time. - D4 — Client factory vs. interceptor boundary. Decide whether the client
interceptors/round-trippers live in
go/transportmw(proposed) with the client modules holding only the hardened factory + wiring, or whether the client modules own their interceptors.
Acceptance criteria (for the plan)¶
- The sequence is dependency-sound: no module, at the point it is cut, imports a
still-in-tree
go-tool-base/pkg/*that its depfootprint guard forbids. - Each phase leaves GTB green (
just ci) consuming the newly-published modules. - The client/server split is honoured:
go/httpclient's dependency graph contains nocontrols/authn/gateway.