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() error
    • Provide(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 emitting dynamic.Message events 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.Pool argument 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, adds Namespace() 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 companion HandlerFunc adapter 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, adds CloseWrite() error
  • Purpose: Extends the standard net.Conn with 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.TCPConn and 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.Handler and http.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. The HandlerFunc adapter is provided. Note that UDP is connectionless; *udp.Conn here 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.ReverseProxy or 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 Update method 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 ServiceBuilder instances 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 ...ServiceBuilder in NewManager is 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.Constructor for 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.Constructor type (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 VoidRegistry pattern (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 StatusSetter to receive up/down notifications about their backends. Load balancers that can aggregate status (e.g., weighted RR that goes “all down”) implement StatusUpdater to 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 methods
  • tcp.Handler, udp.Handler: 1 method each
  • ProxyBuilder: 2 methods
  • ServiceBuilder: 1 method
  • PluginsBuilder: 1 method
  • StatusSetter, 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:

  • NamespacedProvider embeds Provider — a clean refinement that preserves Liskov substitution
  • tcp.WriteCloser embeds net.Conn — extends a stdlib interface without copying its method set
  • udp.Handler and tcp.Handler are standalone (no embedding), consistent with http.Handler

Implicit satisfaction#

All interfaces are consumed-defined (defined where they are used), not provider-defined. For example:

  • serviceManager in pkg/server/router/router.go is a local interface satisfied by *service.Manager without the service package knowing about it
  • middlewareChainBuilder is defined independently in both pkg/server/router and pkg/server/service, satisfied by the same middleware.Builder type
  • This pattern allows packages to depend on abstractions without circular imports

stdlib interfaces used#

  • net.Conn embedded in tcp.WriteCloser
  • http.Handler — the root abstraction for the entire HTTP processing pipeline; every layer (router, middleware, service, proxy) produces and consumes http.Handler
  • http.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:

  1. provider.Provider — plugin providers generate dynamic configuration the same way built-in providers do. No special plugin API.
  2. plugins.Constructor (func(context.Context, http.Handler) (http.Handler, error)) — plugin middleware is just a function that wraps an http.Handler. This matches the containous/alice middleware chaining model precisely.
  3. 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.