Moby — Patterns#
Sampling strategy#
Moby is an XL project (~5,000+ Go files). Pattern detection used grep-based surveys across the entire codebase followed by deep reads of the most illustrative files. Key files examined: errdefs/defs.go, errdefs/helpers.go, daemon/events/events.go, daemon/cluster/cluster.go, client/client_options.go, daemon/list.go, daemon/daemon.go, daemon/server/middleware/middleware.go, daemon/command/daemon.go, daemon/stats/collector.go, pkg/plugins/client.go, daemon/command/trap/trap.go. Architecture and interfaces results were read first.
Concurrency patterns#
Worker pool via errgroup.SetLimit#
- Usage: At least 4 sites in the daemon:
daemon/list.go,daemon/server/router/system/system_routes.go,daemon/internal/builder-next/builder.go,daemon/internal/builder-next/adapters/snapshot/layer.go. - Example:
daemon/list.go:135–160— containers listing useslog2(numContainers)workers:numWorkers := max(int(math.Log2(float64(numContainers))), 1) g, ctx := errgroup.WithContext(ctx) g.SetLimit(numWorkers) for i := range containerList { g.Go(func() error { ... }) } return g.Wait() - Assessment: Excellent pattern — bounded concurrency prevents goroutine explosions under high container counts. The
log2(N)heuristic is self-documenting in the comment.errgroupprovides automatic error collection and context cancellation on first failure. This is idiomatic Go 1.21+ usage.
Pub/sub channel-based event bus#
- Usage:
daemon/events/events.go— the primary daemon event stream. Alsodaemon/stats/collector.gofor per-container stats fan-out. Uses the externalgithub.com/moby/pubsublibrary. - Example:
type Events struct { mu sync.Mutex events []eventtypes.Message // circular ring buffer of last 256 pub *pubsub.Publisher } func (e *Events) Subscribe() ([]eventtypes.Message, chan any, func()) { l := e.pub.Subscribe() cancel := func() { e.Evict(l) } return buffered, l, cancel } - Assessment: Clean pattern — returns buffered history + live channel + cancel func in a single call, preventing race between “give me past events” and “subscribe to future events” with a single mutex hold. The
chan anytype requires type assertion by consumers; this predates Go generics but is a reasonable choice given the age of the codebase.
Two-mutex pattern for long-running reconfiguration#
- Usage:
daemon/cluster/cluster.go— exclusively for Swarm cluster state management. Uniquely, the code has a detailed comment block explaining the locking discipline (lines 1–37 ofcluster.go). - Pattern:
type Cluster struct { controlMutex sync.Mutex // held for full lifecycle of init/join/leave mu sync.RWMutex // held for state reads during reconfiguration nr *NodeRunner ... }controlMutex: prevents concurrent reconfiguration operations (join + leave simultaneously). Held for the entire duration of slow operations.mu(RWMutex): allows reads (e.g., network stack asking for current state) to proceed even whilecontrolMutexis held by a long-running operation.
- Assessment: Sophisticated and well-documented. The two-mutex pattern is uncommon in typical Go code but appropriate here: it separates “I am doing a big operation” from “I am reading current state.” The code comment explicitly teaches the pattern to future maintainers. Worth highlighting as a book example of mutex design for operational reliability.
Graceful shutdown via signal channel + WaitGroup#
- Usage:
daemon/command/daemon.go:363–398anddaemon/command/trap/trap.go. - Pattern:Second signal (while already shutting down) triggers
c := make(chan os.Signal, forceQuitCount) signal.Notify(c, os.Interrupt, syscall.SIGTERM) // ... var apiWG, apiStartWG sync.WaitGroup apiStartWG.Wait() // block until all listeners are ready // ... on signal ... apiWG.Wait() // drain in-flight requestsos.Exit(128 + signum).SIGHUPwires into a separate goroutine that callsdaemon.Reload(). - Assessment: Standard Go shutdown idiom, well-executed. The two-WaitGroup approach (
apiStartWGfor readiness,apiWGfor drain) is a clean separation that prevents the daemon from advertising readiness before listeners are open.
sync.Cond for stats polling#
- Usage:
daemon/stats/collector.go— the stats collector usessync.Condto wake workers when new containers are added or removed. - Pattern:
type Collector struct { m sync.Mutex cond *sync.Cond publishers map[*container.Container]*pubsub.Publisher } s.cond = sync.NewCond(&s.m) - Assessment: One of the few legitimate
sync.Condusages in modern Go code. Here it is appropriate: the stats polling loop needs to sleep until there are containers to poll, andsync.Cond.Broadcast()wakes it when the container set changes. A channel-based approach would have been equally valid but the Cond is idiomatic for this “wake on set change” pattern.
Context cancellation — pervasive#
- Usage: 2,267
context.Contextfunction parameter occurrences across the non-vendor codebase. 193context.WithCancel/WithTimeout/WithDeadlinecall sites. - Pattern: Context is threaded through virtually every public function, from HTTP handlers down to containerd gRPC calls. Context keys use unexported struct types to avoid collisions:
type APIVersionKey struct{} // daemon/server/httputils type UAStringKey struct{} // dockerversion - Assessment: Exemplary context discipline. No string keys for context values (uses struct types instead). Context cancellation from the HTTP request is propagated all the way to the containerd gRPC call, enabling proper cleanup if a client disconnects mid-operation.
Rate limiting via golang.org/x/time/rate#
- Usage: 4 sites —
client/pkg/progress/progressreader.go,daemon/internal/progress/progressreader.go,daemon/internal/builder-next/adapters/containerimage/pull.go,daemon/logger/logger_error.go,daemon/libnetwork/resolver.go. - Example:
daemon/logger/logger_error.go— rate-limits error log spam from failing logging drivers;daemon/libnetwork/resolver.go— rate-limits DNS responses to prevent amplification attacks. - Assessment: Precise, appropriate use of token bucket rate limiting. The DNS rate limiting is a security-aware choice, not just performance.
reexec — process re-execution for privilege operations#
- Usage:
cmd/dockerd/main.go:17(reexec.Init()), with registrations indaemon/graphdriver/windows/windows.go,daemon/libnetwork/sandbox_externalkey_unix.go,daemon/builder/dockerfile/copy_windows.go. - Pattern: The daemon binary registers handlers with
reexec.Register(name, fn)at init. When a privileged operation is needed (e.g., setting a network namespace key, writing a Windows layer), the daemon re-executes itself with a specific argument.reexec.Init()at startup checks if the binary was invoked this way and dispatches to the handler, then exits. This avoidsfork(2)withoutexec(2), which is unsafe with goroutines. - Assessment: A Moby-specific, Go-idiomatic solution to the “fork without exec is unsafe” problem. It is elegant but requires registering handlers before
reexec.Init()is called. Worth understanding for anyone building daemons that manage Linux namespaces.
Error handling#
- Style: Mixed — legacy
github.com/pkg/errors(240 import sites) coexists with stdliberrors.Is/errors.As(438 call sites). Active migration from the former to the latter is visible in code comments and recent commits. - Dominant approach: Custom marker interface taxonomy via the
errdefspackage — unique to Moby and architecturally significant.
errdefs marker interface pattern#
// errdefs/defs.go — public interfaces (zero methods each, except one marker method)
type ErrNotFound interface { NotFound() }
type ErrConflict interface { Conflict() }
type ErrInvalidParameter interface { InvalidParameter() }
type ErrUnauthorized interface { Unauthorized() }
// ... 12 total categories
// errdefs/helpers.go — private wrapper types
type errNotFound struct{ error }
func (errNotFound) NotFound() {}
func (e errNotFound) Unwrap() error { return e.error }
// Constructor — idempotent (returns as-is if already the right type)
func NotFound(err error) error {
if err == nil || cerrdefs.IsNotFound(err) { return err }
return errNotFound{err}
}
// Classification — used in HTTP handler to map to status codes
func IsNotFound(err error) bool { var e ErrNotFound; return errors.As(err, &e) }- Error types defined: 12 semantic categories:
ErrNotFound,ErrConflict,ErrInvalidParameter,ErrUnauthorized,ErrForbidden,ErrUnavailable,ErrSystem,ErrNotModified,ErrNotImplemented,ErrUnknown,ErrCancelled,ErrDeadline. - HTTP mapping:
daemon/server/httputils/errors.gouseserrdefs.IsNotFound()→ 404,errdefs.IsConflict()→ 409, etc. The API layer never inspects error strings. - Wrapping approach:
fmt.Errorf("%w", ...)for new code;errors.WithStack()fromgithub.com/pkg/errorsin legacy code. TheerrNotFoundwrapper implements bothCause()(pkg/errors compatibility) andUnwrap()(stdlib compatibility). - Assessment: Sophisticated and well-designed. The marker interface approach is more flexible than sentinel errors (wrapping preserves the original message) and more composable than custom types (any error can be classified). The dual
Cause()/Unwrap()implementation shows careful backward-compatibility thinking during the migration. Book-worthy pattern.
errConnectionFailed — struct-wrapping for typed errors#
- File:
client/request.go— wraps errors with connection failure context:type errConnectionFailed struct{ error } func (e errConnectionFailed) Error() string { return ... } - Assessment: Simpler variant of the errdefs pattern for a specific error class in the client library.
Configuration pattern#
- Approach: Functional options in
client/package; flat config struct + mergo in the daemon.
Functional options in client/#
type clientConfig struct { /* ~12 fields */ }
type Opt func(*clientConfig) error // the option type
// 15 named constructors:
func WithHost(host string) Opt { ... }
func WithTLSClientConfig(ca, cert, key string) Opt { ... }
func WithDialContext(fn func(ctx, network, addr string) (net.Conn, error)) Opt { ... }
func WithAPIVersion(version string) Opt { ... }
// ...
// Applied in constructor:
func NewClientWithOpts(ops ...Opt) (*Client, error) {
for _, op := range ops {
if err := op(&c.clientConfig); err != nil { return nil, err }
}
}- Naming: Uses
Opt(notOption) as the type name — a minor stylistic choice. - Error-returning options: Options return
error, allowing validation during construction. This is stricter than the typicalfunc(*T)pattern but appropriate for a public library where misconfiguration should fail fast. - Assessment: Textbook functional options, well-executed. The error-returning variant is appropriate for library code where client misconfiguration is a programming error.
Daemon config — flag struct + mergo merge + atomic.Pointer hot reload#
// daemon.Daemon
configStore atomic.Pointer[configStore] // daemon/daemon.go:106
// Hot reload on SIGHUP:
func (daemon *Daemon) Reload(conf *config.Config) error {
newStore := *daemon.configStore.Load()
newStore.config = *conf
daemon.configStore.Store(&newStore) // atomic swap
}- Assessment:
atomic.Pointer[T]is the canonical Go 1.19+ pattern for a hot-swappable config. Readers take a snapshot withLoad()and operate on the snapshot; writers atomically replace the pointer. No lock required for readers. The one-field-per-flag pattern inconfig.Config(~70 fields) is verbose but straightforward.
Dependency injection#
- Approach: Manual constructor wiring — no DI framework.
- Evidence:
daemon/command/daemon.go(daemonCLI.start()) — ~400-line initialization sequence that explicitly constructs each subsystem and passes dependencies as parameters. Thedaemon.Daemongod-struct acts as the ambient context; subsystems that need access to other subsystems receive them as constructor parameters or interface arguments. - Pattern for subsystem isolation: Rather than passing
*daemon.Daemonto HTTP route handlers, each handler package defines aBackendinterface containing only the methods it needs:// daemon/server/router/container/backend.go type Backend interface { ContainerCreate(ctx, config, hostCfg, networkCfg, platform, name) (string, error) ContainerStart(ctx, name, checkpoint, checkpointDir string) error // ~30 methods specific to container operations }*daemon.Daemonsatisfies this interface via its method set. The injection is done inbuildRouters(). - Assessment: The Backend-per-router pattern is the most interesting DI detail here — it enforces ISP without a DI framework. The broad
Daemongod-struct is the pragmatic trade-off for a single-process system that needs all subsystems simultaneously.
Other notable patterns#
Functional options#
As described above, used pervasively in client/. Also used in BuildKit integration (daemon/internal/builder-next) where the buildkit.Opt struct configures the builder worker.
Table-driven patterns#
- Prevalence: Very heavy — 594 occurrences of
testCases/testcases/tc.name/tt.namepatterns in test files. - Style: Anonymous struct slices with named fields. Standard Go idiom.
- Example:
client/*_test.go— virtually every client method has a table-driven test.
Interface embedding#
- Prevalence: Moderate. Key examples:
client.APIClientis a composite of ~20 sub-interfaces embedded together for testability.libcontainerdtypes embed nothing but theBackendinterface hierarchy uses composition. - Notable:
daemon/container/store.go—Storeembedsmemdb.MemDB-backed lookup, presented as an interface.
Type assertions / type switches#
- Count: 53 type switch statements.
- Primary use: Error type classification before the
errdefstaxonomy was fully adopted (legacy code), and in the event system wherechan anycarries heterogeneous event types.
Generics usage (Go 1.18+)#
- Limited but targeted:
client/utils.go:117—decodeWithRaw[T any]eliminates repetitive decode + return-raw-JSON boilerplate across the 60+ client API methods. - Assessment: Judicious use — not pervasive, applied where it eliminates real duplication. Suggests the project adopts generics conservatively, which is appropriate for a widely-deployed library.
Registry pattern#
- Usage: Layer store (
layerStore.Register()), volume drivers (store.Register(d, name)), BuildKit source manager (sm.Register(source)), plugin store (reexec.Register()). - Pattern: Named registration at init time; lookup by name or ID at runtime. The plugin system uses both
reexec.Register()(for in-process worker handlers) and a runtime plugin store (for external v2 plugins).
Plugin HTTP-RPC pattern#
- Usage:
pkg/plugins/client.go— external plugins (authorization, logging, network drivers, volume drivers) communicate over a Unix socket using a JSON-over-HTTP protocol. - Pattern: The daemon acts as an HTTP client calling
POST /Plugin.Methodon the plugin’s socket. Plugin lifecycle is managed bypkg/plugins/plugins.gowith retry/backoff logic. - Assessment: Simple and debuggable. Plugin developers can implement the protocol in any language. The trade-off is higher overhead than gRPC and no schema enforcement, but for plugins that are rarely called (authz, volume operations), this is acceptable.
sync.OnceValue / sync.OnceValues (Go 1.21)#
- Usage: 8 sites —
client/client.go:120(default user agent string),daemon/daemon.go:826(system check),daemon/pkg/oci/defaults.go:193(masked paths list),daemon/internal/platform/platform_linux.go:18(CPU list),daemon/libnetwork/ns/init_linux.go:21(namespace handles). - Assessment: Good adoption of Go 1.21’s
sync.OnceValuefor lazy initialization of expensive/fallible computations. The project is actively modernizing; these usages replacedsync.Once+ manual error handling.
Backoff/retry#
- Usage:
pkg/plugins/client.go:171(callWithRetry) andpkg/plugins/plugins.go:200(loadWithRetry) — exponential backoff for plugin socket connection. - Pattern:
backoff(retries int) time.Durationreturnstime.Duration(math.Pow(2, float64(retries))) * 100ms, capped at 2 seconds. - Assessment: Simple exponential backoff without jitter — adequate for the plugin use case (local Unix sockets) but would be problematic for distributed systems. The
moby/swarmkitside uses more sophisticated backoff.
stdcopy — custom stream multiplexing#
- Usage:
api/pkg/stdcopy/stdcopy.go— a custom binary protocol for multiplexing stdout/stderr over a single TCP/Unix connection without a TTY. - Protocol: 8-byte header per frame: 1 byte stream type (stdin=0, stdout=1, stderr=2), 3 bytes padding, 4 bytes big-endian payload size.
- Assessment: Pre-dates widespread adoption of gRPC stream multiplexing. It solves a real problem (TTY mode vs raw mode) but is Moby-specific protocol knowledge that consumers must handle. The
StdCopy()function handles the demultiplexing.
Pattern summary for the book#
| Pattern | Prevalence | Book Interest |
|---|---|---|
errdefs marker interface error taxonomy | Core/unique to Moby | ★★★ — unique, reusable design |
| Two-mutex pattern (controlMutex + mu) | 1 site, documented | ★★★ — rare, explained in comments |
reexec for privilege operations | 4 sites | ★★★ — solves Go+namespace problem |
| Functional options with error return | client/ package | ★★ — good public API pattern |
| Bounded worker pool with errgroup.SetLimit | 4 sites | ★★ — modern idiomatic Go |
| pubsub channel event bus | events, stats | ★★ — fan-out with backpressure |
| Backend-per-router ISP enforcement | 12 routers | ★★ — ISP without DI framework |
| atomic.Pointer config hot reload | 1 site | ★★ — SIGHUP-safe config swap |
| sync.OnceValue for lazy init | 8 sites | ★ — simple modernization |
| Plugin HTTP-RPC over Unix socket | pkg/plugins | ★★ — language-agnostic extension |
| stdcopy stream multiplexing | api/pkg | ★ — protocol detail |