Traefik — Patterns#
Concurrency patterns#
Managed goroutine pool with panic recovery (safe.Pool)#
- Usage: All long-lived goroutines in the system — TLS manager, config watcher, provider aggregator, entrypoint listeners — are launched via
safe.Pool.GoCtx(), never rawgo func. - Example:
pkg/safe/routine.go:32—GoCtxwraps the goroutine inGoWithRecover, which installs adefer recover()that logs the stack trace and continues.Stop()cancels the pool context and blocks onwaitGroup.Wait(). - Assessment: Excellent. This single abstraction solves three concerns at once: panic isolation (a panicking goroutine doesn’t take down the process), context propagation (pool context threads through every goroutine), and coordinated shutdown (WaitGroup ensures all goroutines drain before exit). It is idiomatic and effectively used throughout.
Channel-based event bus (provider → watcher)#
- Usage: Every provider communicates configuration changes by sending
dynamic.Messagevalues into achan dynamic.Messagechannel. The watcher owns two internal channels:allProvidersConfigs(buffered, capacity 100) andnewConfigs(unbuffered pipeline signal). - Example:
pkg/server/configurationwatcher.go—receiveConfigurationsgoroutine consumesallProvidersConfigs, deduplicates viareflect.DeepEqual, and signalsapplyConfigurationsvianewConfigs. - Assessment: Clean fan-in design. The buffered capacity-100 input channel absorbs burst updates from fast providers. The two-goroutine split (receive vs apply) keeps the hot path lock-free at the cost of one extra channel hop.
RingChannel (lossy non-blocking producer)#
- Usage: The provider aggregator wraps each provider’s output in a
RingChannel— a custom bounded single-item buffer that drops the oldest message when full, ensuring writers never block. - Example:
pkg/provider/aggregator/ring_channel.go—ringBuffer()goroutine implements a clever two-selecttrick: it prefers writing (to avoid unnecessary drops) and only reads when the output is not ready, effectively preferring the most recent update over older ones. - Assessment: Sophisticated and well-commented. The “prefer-write” bias is non-obvious but correct — it avoids the 50% drop rate that a naive random-select would produce. This pattern is directly motivated by the system’s guarantee that only the latest config state matters.
Context cancellation (pervasive)#
- Usage:
context.Contextappears in 639 places. Every goroutine, middleware, and provider method receives a context. Cancellation propagates from thesafe.Poolroot context down through the call tree. - Example:
pkg/server/configurationwatcher.go— providers receive the pool’s context and must respect cancellation to stop their watch loops. - Assessment: Consistent and idiomatic. Context is used both for cancellation and for carrying request-scoped values (logger, observability handles). Occasionally overloaded (contexts carry middleware names and route info), but this is conventional in the Go ecosystem.
Atomic hot-swap for routing table#
- Usage: Each
TCPEntryPointholds an atomic reference to the current TCP router (switcher). When config updates arrive,RouterFactory.CreateRouters()builds a fresh router tree, thenSwitch(rt)atomically swaps the reference. In-flight requests complete on the old router. - Example:
pkg/server/server_entrypoint_tcp.go:380—e.switcher.Switch(rt)is called inside the config update listener; no lock is held during the swap, so request serving never stalls during a config reload. - Assessment: Excellent zero-downtime reload pattern. The “build a new tree, swap atomically” approach trades memory (a brief double-buffering) for simplicity — there is no incremental diff, just full rebuild and atomic replace.
Backoff retry with panic-safe wrapper#
- Usage: Providers that poll external APIs (Docker, Consul, ECS, Nomad) wrap their poll operation with
safe.OperationWithRecoverand retry it withbackoff.RetryNotifyusing exponential backoff. - Example:
pkg/provider/consulcatalog/consul_catalog.go:242—backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(...), ctxLog), notify). - Assessment: Consistent retry pattern across all polling providers. The
OperationWithRecoverwrapper converts a panic into an error that the backoff sees as a retryable failure — a robust defense against unexpected provider panics.
Worker pool via select {} (153 occurrences)#
- Usage:
selectis used extensively for coordinating goroutine lifecycles: waiting for either work or context cancellation, choosing between send and receive on optional channels (e.g., inRingChannel), and implementing timeouts on ACME/TLS challenge flows. - Assessment: Standard Go idiom; usage is appropriate and not over-engineered.
Error handling#
- Style: Mixed —
fmt.Errorfwith%wwrapping dominates (1,273 total error-related usages);errors.Newfor leaf sentinels;errors.Is/errors.Asfor inspection. No third-party error library. - Error types defined: Mostly sentinel
var err... = errors.New(...)rather than typed structs. Notable custom struct type:pkg/api/handler.go:17—apiErrorfor JSON API error responses. Package-level sentinels:errBodyTooLarge,errClosedListener,mirror.ErrBodyTooLarge. - Wrapping approach:
fmt.Errorf("context message: %w", err)with a consistentverb: nounstyle throughout. Example fromcmd/traefik/traefik.go:250:fmt.Errorf("plugin: failed to create plugin builder: %w", err). The wrapping messages read as a breadcrumb trail. - Examples:
cmd/traefik/traefik.go:312:return nil, fmt.Errorf("creating router factory: %w", err)— single-level wrap in the bootstrap chain.pkg/middlewares/auth/forward.go:171:errors.Is(err, errBodyTooLarge)— sentinel inspection after wrapped propagation.pkg/safe/routine.go:74:err = fmt.Errorf("panic in operation: %w", err)— panic-to-error conversion preserving the chain.
Configuration pattern#
- Approach: Two large configuration structs (
static.Configurationanddynamic.Configuration) loaded via reflection-driven loaders inpaerser/cli. No functional options for the core server; config is value-passed to constructors. - Example:
pkg/server/configurationwatcher.go:38—NewConfigurationWatchertakes scalar values (pool, provider, entrypoints, requiredProvider) as constructor arguments, not an options struct. This is the dominant pattern: explicit constructor arguments. - Functional options appear narrowly: In the Kubernetes shared informer factory (generated code) and in
pkg/testhelpers/config.gofor building test fixtures (WithRouters,WithService, etc.). The test helper use is notable — functional options are considered ergonomic enough for test builders but not for production constructors. - Reflection-driven config loading:
pkg/cli/deprecation.gowalks thestatic.Configurationstruct viareflect.Typeto filter deprecated and unknown fields — the config schema is the single source of truth.
Dependency injection#
- Approach: Pure manual constructor injection. No DI framework (no wire, dig, or fx).
- Evidence:
cmd/traefik/traefik.go:setupServer()— approximately 220 lines of explicit construction and wiring. Each dependency is constructed in topological order and passed explicitly to the next constructor. The function is essentially the composition root for the entire application. - Rationale (visible in code): Comments in
setupServer()note ordering constraints between components (TLS manager before ACME, metrics before observability manager). These constraints are enforced by code order, not a framework. This trades framework magic for explicit, auditable startup sequencing.
Other notable patterns#
Middleware chain via alice#
The pkg/server/middleware/Builder.BuildMiddlewareChain() uses github.com/containous/alice to compose HTTP middleware. Each middleware is a func(http.Handler) (http.Handler, error) (alice.Constructor). The builder resolves middleware names from config, constructs each middleware via a switch dispatch, and appends it to the chain. The final chain wraps the service handler.
This is a clean pipeline-of-constructors pattern. Alice is a minimal library (~100 lines); Traefik uses it because it handles the error-propagating wrapping (func(http.Handler) (http.Handler, error)) that stdlib does not.
Registry pattern (metrics)#
pkg/observability/metrics defines a Registry interface that every metrics backend (Prometheus, Datadog, StatsD, OpenTelemetry, Semconv) satisfies. At startup, registerMetricClients() builds a []metrics.Registry slice and wraps it in a MultiRegistry that fan-outs calls. This is a textbook registry/multi-backend pattern: adding a new metrics backend requires only implementing the interface and adding a case in registerMetricClients.
Factory pattern (RouterFactory, ManagerFactory)#
RouterFactory and ManagerFactory separate the construction of routers/services from their use. Both are created once at startup and called on every config update. The factory holds shared dependencies (TLS manager, transport manager, observability) that are constant across config reloads; the config-specific state is passed per-call. This keeps the hot rebuild path stateless.
Table-driven tests#
Table-driven tests are the dominant test style (1,137 occurrences of t.Run, testCases, tt.name, etc.). Both anonymous struct slices and named struct types are used. The pattern is consistent and idiomatic throughout the codebase.
Generics (minimal, utility only)#
Generics are present but narrowly scoped to utility functions:
shuffle[T any]— shuffles a slice (appears in three service packages)pointer[T any]— wraps a value in a pointer (test helpers and server aggregator)pool[T any]— a typed sync.Pool wrapper inpkg/proxy/fast/proxy.goconvert[T any]— JSON unmarshal helper in Kubernetes providers
No generic data structures or algorithm abstractions. Generics are used to eliminate type-specific boilerplate, not to express architectural abstractions.
sync.Once / sync.OnceValue for lazy initialization#
Used in a few specific places: pkg/provider/kubernetes/crd/generated/... for the lazy Kubernetes internal type parser, and pkg/provider/kubernetes/gateway/features.go:10 using the newer sync.OnceValue wrapper introduced in Go 1.21. Usage is focused and correct.
Type switches for config dispatch#
The middleware builder (pkg/server/middleware/middlewares.go) uses a large switch on middleware type to dispatch to the correct constructor. This is a deliberate trade-off: it centralizes all middleware construction in one place, making it easy to audit which middlewares exist and how they are built, at the cost of touching one file to add a new middleware type.
Interface embedding (narrow use)#
NamespacedProvider in pkg/provider/provider.go extends the base Provider interface with a Namespaces() []string method. This is used by the aggregator to handle multi-namespace providers differently from single-namespace ones. Interface extension is not heavily used elsewhere — Traefik generally prefers composition via struct embedding over interface extension.