Traefik — Interfaces#
Sampling note#
Traefik is a size-L project (~500–2000 .go files). The generated Kubernetes CRD clientset/informer/lister code under pkg/provider/kubernetes/crd/generated/ accounts for ~80+ interface definitions that are boilerplate code-gen output. These are excluded from analysis. Focus is on the ~25 hand-authored interfaces that define Traefik’s own architectural contracts.
Interface catalog#
Provider#
- Package:
pkg/provider - File:
pkg/provider/provider.go:9 - Methods:
Init() errorProvide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error
- Purpose: The central extensibility seam of the entire system. Any integration (Docker, Kubernetes, Consul, etcd, file, HTTP, ACME, plugin, etc.) that can produce dynamic routing configuration implements this two-method contract.
Init()is called once for validation/setup;Provide()runs for the lifetime of the process emittingdynamic.Messageevents onto the channel whenever the underlying source changes. - Implementations: ~15 in the codebase —
pkg/provider/docker,pkg/provider/kubernetes/crd,pkg/provider/kubernetes/ingress,pkg/provider/consul,pkg/provider/etcd,pkg/provider/file,pkg/provider/http,pkg/provider/nomad,pkg/provider/ecs,pkg/provider/rest,pkg/provider/acme,pkg/provider/tailscale, and plugin providers loaded at runtime. - Design quality: Exceptionally well-segregated. Two methods: one for lifecycle, one for the event stream. The push-model (
chan<-) is the right primitive — it decouples providers from their consumer (ConfigurationWatcher) entirely. The*safe.Poolargument gives providers a managed goroutine launcher without needing to know about the shutdown sequence. Follows ISP perfectly.
NamespacedProvider#
- Package:
pkg/provider - File:
pkg/provider/provider.go:20 - Methods: Embeds
Provider, addsNamespace() string - Purpose: Optional refinement of Provider for integrations that operate in a specific namespace context (e.g., Kubernetes namespace-scoped providers). Enables logging and diagnostics to identify which namespace a provider instance serves.
- Implementations: Kubernetes CRD and Ingress providers.
- Design quality: Clean interface embedding — extends without breaking the base contract.
tcp.Handler#
- Package:
pkg/tcp - File:
pkg/tcp/handler.go:8 - Methods:
ServeTCP(conn WriteCloser) - Purpose: The TCP-layer equivalent of
http.Handler. Any component that processes a raw TCP connection implements this. Used by the TCP router to dispatch connections to TLS terminators, TCP passthrough proxies, or the HTTP forwarder. - Implementations: TCP router manager, TLS handler, HTTP forwarder, TCP proxy (passthrough mode).
- Design quality: Minimal, mirrors
http.Handler. The companionHandlerFuncadapter follows the stdlib pattern precisely. The single-method design is ideal — a TCP handler does exactly one thing.
tcp.WriteCloser#
- Package:
pkg/tcp - File:
pkg/tcp/handler.go:22 - Methods: Embeds
net.Conn, addsCloseWrite() error - Purpose: Extends the standard
net.Connwith half-close semantics (FIN on write side only). This is necessary for TCP passthrough proxying where the proxy must signal end-of-write without closing the read side — essential for correct HTTP/1.x pipelining and proxying protocols that use half-close. - Implementations: Wrappers around
*net.TCPConnand TLS connections. - Design quality: Precise extension of a stdlib interface. The method is included specifically to support a protocol-level requirement, not for convenience — good interface hygiene.
udp.Handler#
- Package:
pkg/udp - File:
pkg/udp/handler.go:4 - Methods:
ServeUDP(conn *Conn) - Purpose: UDP-layer counterpart to
tcp.Handlerandhttp.Handler. Dispatches UDP “connections” (Traefik’s virtual UDP session abstraction) to the appropriate service. - Implementations: UDP router manager, UDP proxy.
- Design quality: Single-method, consistent with
tcp.Handler. TheHandlerFuncadapter is provided. Note that UDP is connectionless;*udp.Connhere is a Traefik-defined abstraction for tracking a UDP session by source address.
ProxyBuilder#
- Package:
pkg/server/service - File:
pkg/server/service/service.go:41 - Methods:
Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)Update(configs map[string]*dynamic.ServersTransport)
- Purpose: Abstracts the construction of reverse proxy handlers. Allows the service manager to be independent of whether the standard
net/http/httputil.ReverseProxyor the fast-path proxy (fasthttp-based) is in use. Also handles transport configuration updates on config reload. - Implementations:
pkg/proxy/httputil.ProxyBuilder(standard),pkg/proxy/smart_builder.SmartBuilder(fast-path with automatic selection),pkg/proxy/fast.ProxyBuilder(fasthttp). - Design quality: Two responsibilities in one interface (build + update) is a minor ISP tension, but acceptable given that both operations operate on the same transport configuration. The
Updatemethod exists because transport configs must be refreshed on dynamic config changes without rebuilding all proxies.
ServiceBuilder#
- Package:
pkg/server/service - File:
pkg/server/service/service.go:47 - Methods:
BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) - Purpose: Allows additional service types (weighted round-robin, mirror, failover) to be injected into the service manager as builders without the manager needing to know their implementation. The manager holds a slice of
ServiceBuilderinstances and tries each in turn when resolving a service name. - Implementations: Weighted RR (
wrr), mirror, failover, P2C, HRW, leasttime load balancers. - Design quality: Single-method — a textbook function-object interface. The variadic
serviceBuilders ...ServiceBuilderinNewManageris a nice open/closed design — new service types can be added without changing the manager’s constructor signature.
middleware.PluginsBuilder#
- Package:
pkg/server/middleware - File:
pkg/server/middleware/plugins.go:15 - Methods:
Build(pName string, config map[string]any, middlewareName string) (plugins.Constructor, error) - Purpose: Abstracts the plugin execution backend (yaegi/interpreted Go vs WASM/wazero) from the middleware builder. The middleware builder calls this to obtain a
plugins.Constructorfor any middleware type marked as a plugin, without needing to know whether the plugin runs as interpreted Go or WASM. - Implementations:
pkg/plugins.Builder(the real builder, wraps both yaegi and wazero execution environments). A no-op implementation is used when plugins are disabled. - Design quality: Single-method, clean isolation. The
plugins.Constructortype (func(context.Context, http.Handler) (http.Handler, error)) is the middleware factory signature, which is the correct level of abstraction to return from a builder.
metrics.Registry#
- Package:
pkg/observability/metrics - File:
pkg/observability/metrics/metrics.go:14 - Methods: ~20 methods —
IsEpEnabled(),IsRouterEnabled(),IsSvcEnabled(), counter/gauge/histogram accessors for entry-point, router, and service dimensions. - Purpose: The unified metrics abstraction across all instrumentation backends (Prometheus, InfluxDB, StatsD, Datadog, OpenTelemetry). Components instrument themselves against this interface rather than any specific backend. A multi-registry implementation fans out to all enabled backends simultaneously.
- Implementations:
pkg/observability/metrics/prometheus.go,pkg/observability/metrics/influxdb.go,pkg/observability/metrics/statsd.go,pkg/observability/metrics/datadog.go,pkg/observability/metrics/opentelemetry.go,VoidRegistry(no-op, avoids nil checks). - Design quality: Broad interface — ~20 methods — which could be a concern for ISP. However it is a service-boundary interface (one implementation per backend), not a consumer-facing interface, so breadth here is acceptable. The
VoidRegistrypattern (no-op implementation) is idiomatic Go for optional observability.
healthcheck.StatusSetter / StatusUpdater#
- Package:
pkg/healthcheck - File:
pkg/healthcheck/healthcheck.go:33,40 - Methods:
StatusSetter:SetStatus(ctx context.Context, childName string, up bool)StatusUpdater:RegisterStatusUpdater(fn func(up bool)) error
- Purpose: Bidirectional health propagation protocol. Load balancers implement
StatusSetterto receive up/down notifications about their backends. Load balancers that can aggregate status (e.g., weighted RR that goes “all down”) implementStatusUpdaterto notify their own parent load balancer. - Implementations:
wrr.Balancer,failover.Handler,mirror.Handler(StatusSetter);wrr.Balancer(StatusUpdater). - Design quality: Two small single-method-ish interfaces that together form a composable health propagation tree. This is a good example of interface segregation — a component only needs to implement the side of the protocol that applies to its role.
Interface patterns#
Size distribution#
Traefik has a strong preference for small interfaces. Most hand-authored interfaces have 1–3 methods:
Provider: 2 methodstcp.Handler,udp.Handler: 1 method eachProxyBuilder: 2 methodsServiceBuilder: 1 methodPluginsBuilder: 1 methodStatusSetter,StatusUpdater: 1 method each
The outlier is metrics.Registry (~20 methods), which is intentionally broad as a service-boundary abstraction. The serviceManager internal interface (2 methods) and middlewareChainBuilder (1 method) follow the same small-interface discipline.
Embedding#
Interface embedding is used purposefully:
NamespacedProviderembedsProvider— a clean refinement that preserves Liskov substitutiontcp.WriteCloserembedsnet.Conn— extends a stdlib interface without copying its method setudp.Handlerandtcp.Handlerare standalone (no embedding), consistent withhttp.Handler
Implicit satisfaction#
All interfaces are consumed-defined (defined where they are used), not provider-defined. For example:
serviceManagerinpkg/server/router/router.gois a local interface satisfied by*service.Managerwithout the service package knowing about itmiddlewareChainBuilderis defined independently in bothpkg/server/routerandpkg/server/service, satisfied by the samemiddleware.Buildertype- This pattern allows packages to depend on abstractions without circular imports
stdlib interfaces used#
net.Connembedded intcp.WriteCloserhttp.Handler— the root abstraction for the entire HTTP processing pipeline; every layer (router, middleware, service, proxy) produces and consumeshttp.Handlerhttp.ResponseWriter— extended by middleware for response capture (e.g.,recoveryResponseWriter)context.Context— pervasive throughout all interfaces as the first parameter
Key abstractions#
1. provider.Provider — the extensibility seam#
The most architecturally significant interface in the codebase. Its two-method contract is the reason Traefik can integrate with ~15 different infrastructure platforms without core changes. The channel-based Provide() signature is the right abstraction: it encodes the push model that separates provider lifecycles from the watcher.
2. tcp.Handler + tcp.WriteCloser — the protocol-layer handler hierarchy#
These two interfaces define the TCP processing model. Together with http.Handler (stdlib) and udp.Handler, they form a three-protocol handler hierarchy that is consistent in design. The WriteCloser extension is a precise, protocol-motivated interface extension — not a kitchen-sink addition.
3. middleware.PluginsBuilder — the plugin execution abstraction#
A single-method interface that hides whether a plugin runs as interpreted Go (yaegi) or WASM (wazero). This is what makes Traefik’s plugin ecosystem work without requiring native Go compilation — the abstraction boundary is at the plugins.Constructor function type, not at a plugin object boundary.
4. metrics.Registry — the observability fan-out abstraction#
A broad interface that lets all core components instrument themselves without knowing which metrics backend(s) are active. The multi-registry and void-registry implementations show how Go interfaces enable clean observability injection: components never check if metrics are enabled, they just call methods on the registry.
5. healthcheck.StatusSetter / StatusUpdater — bidirectional health propagation#
An elegant two-interface protocol for hierarchical health state propagation through nested load balancers. Each interface is single-method and captures exactly one role in the protocol — a textbook example of ISP applied to a non-trivial domain problem.
Interface-driven extensibility#
Traefik’s plugin model is built on three interface abstractions working together:
provider.Provider— plugin providers generate dynamic configuration the same way built-in providers do. No special plugin API.plugins.Constructor(func(context.Context, http.Handler) (http.Handler, error)) — plugin middleware is just a function that wraps anhttp.Handler. This matches thecontainous/alicemiddleware chaining model precisely.middleware.PluginsBuilder— the bridge between the plugin manager (yaegi/wazero) and the middleware builder. Swap the implementation to support a new execution environment.
The ProxyBuilder interface provides a similar seam for swapping the HTTP proxy implementation — the service manager builds backends without knowing whether fasthttp or net/http/httputil is under the hood. This has been used to implement a “smart builder” that automatically selects the proxy implementation per backend.
The result is a system where extension points are genuinely open: new providers, middleware, and proxy backends can be introduced by implementing a small, well-defined interface — and the plugin system makes this possible at runtime without recompilation.