ADR 0003 - Implementation language

Decisionstable
On this page

Status

Accepted. This closes the stack question that initial discovery was opened to answer.

Context

Relay is a connection-holding server. Its job is to keep many long-lived WebSockets open, correlate requests and responses across them under deadlines, run timers per connection, and serve an HTTP API generated from a published OpenAPI document. The workload is concurrency and I/O, with almost no computation.

Five things constrained the choice:

  • Connection density. The design assumes a single instance holds many long-lived executor sockets. The ownership model in components exists partly so this can be scaled horizontally, but per-instance density still sets the operational floor.
  • Deployment shape. One of the v1 goals is a single binary plus a Postgres URL, with multi-architecture container images.
  • Server-stub generation. The HTTP surface is generated from the vendored OpenAPI document. Generation quality decides how much hand-written glue sits between the specification and the handlers, and hand-written glue is where drift starts.
  • Ecosystem proximity. One of the SDKs and the upstream command-line client are both Go. Reading them is part of the work, and the client is the conformance target.
  • Maintainer fluency. A solo maintainer with agent assistance. Velocity in a familiar language is worth more than a marginal runtime advantage.

Python with an async web framework was the alternative worth taking seriously, and it has a proven precedent in this ecosystem: dbos-argus implements the read path on that stack. It loses on the deployment shape and on connection density, and it wins on nothing that this project needs.

Decision

Build Relay in Go.

The supporting choices that follow from it, recorded here so they are not re-litigated per package:

  • WebSockets: github.com/coder/websocket. This is the library the design notes called nhooyr.io/websocket, which was renamed after its author handed it over. The old import path is not the one to use.
  • HTTP: the standard library, using the method-and-path routing patterns added in Go 1.22. The generated server binds to it, so there is no router dependency.
  • OpenAPI: oapi-codegen, generating models and a standard-library server interface from the vendored document.
  • Postgres: pgx with its connection pool, queries generated by sqlc, migrations by golang-migrate driven from an embedded filesystem so relay migrate needs no files on disk.
  • CLI: cobra, matching the maintainer's other tools.
  • Logging: log/slog from the standard library.
  • Generators are pinned as Go tool dependencies in go.mod, so a contributor needs Go and nothing else, and generated output does not shift under a differently versioned local install.

Consequences

  • The single-binary goal is met by the toolchain rather than by packaging work. Cross-compilation for the release matrix is a build flag.
  • The Go SDK becomes the first-choice reading for the protocol specification, because its types are directly legible to this project. The specification must still cover all four SDKs and accept their union, so this is a convenience and not a shortcut.
  • sqlc and oapi-codegen output is committed and checked for drift by the quality gate, so a stale generated file fails CI instead of being noticed later.
  • Go's error handling makes the deadline-and-failure paths that the HTTP contract cares about explicit at every call site. That is verbose, and it is the reason the error mapping can be trusted.
  • The module path currently uses the working project name. Renaming it before the first public release is a go mod edit and a search and replace, which is cheap while the repository is private and expensive afterwards. See ADR 0002.