Vault — Patterns#

Concurrency patterns#

Goroutine-per-task with go func#

  • Usage: 348 occurrences — the most common concurrency primitive used throughout the codebase for background tasks, event dispatching, and request handling.
  • Example: vault/expiration.go — background revocation workers; vault/eventbus/bus.go — async event delivery to subscribers.
  • Assessment: Idiomatic but not always bounded. The fairshare subsystem (see below) was introduced specifically to bound the goroutines used by the expiration manager.

Channel-based shutdown (ShutdownCh chan struct{})#

  • Usage: Every command struct (command/proxy.go:73, command/agent.go, command/server.go) carries a ShutdownCh chan struct{} field. Closing the channel signals all listening goroutines to stop.
  • Example: command/proxy.go:742case <-c.ShutdownCh: in the main select loop. MakeShutdownCh() in command/commands.go wires OS signals (SIGINT, SIGTERM) to close the channel.
  • Assessment: Classic Go shutdown idiom. Closing a channel broadcasts to all receivers simultaneously, which is the right primitive when N goroutines must all stop. Used consistently across all long-running command binaries.

Fair-share worker pool (helper/fairshare.JobManager)#

  • Usage: Custom-built fair-share scheduler in helper/fairshare/. Used by the ExpirationManager to process lease revocations without letting one tenant’s burst of expirations starve others.
  • Example: helper/fairshare/jobmanager.goJobManager maintains per-queue list.List instances and assigns work to dispatcher workers using round-robin across queues. Implements onceStart/onceStop sync.Once guards for lifecycle safety.
  • Assessment: Sophisticated and domain-appropriate. The fairshare design directly solves the “noisy neighbor” problem for lease revocations — a problem that naive worker pools cannot handle. Worth studying as an example of purpose-built concurrency infrastructure.

External worker pool (gammazero/workerpool)#

  • Usage: command/agentproxyshared/cache/static_secret_capability_manager.go:45 — Vault Agent’s static secret capability manager uses an external worker pool library for capability refresh workers.
  • Assessment: Pragmatic delegation to a library for a non-critical workload, contrasting with the in-house fairshare for the critical expiration path.

select for multiplexing#

  • Usage: 500 select { occurrences — the dominant pattern for multiplexing timer ticks, shutdown signals, new-work notifications, and error channels.
  • Example: helper/fairshare/jobmanager.goselect on quit, newWork channels drives the job dispatch loop.
  • Assessment: Idiomatic. Vault uses select correctly: always with a default case or explicit timeout where appropriate to avoid blocking.

Context cancellation and timeouts#

  • Usage: context.Context appears 4,392 times. context.WithCancel/WithTimeout/WithDeadline and ctx.Done() account for 566 occurrences. Every backend call, storage operation, and plugin invocation is context-aware.
  • Example: vault/eventbus/bus.go:defaultTimeout — a 60-second deadline is applied to all event sends; subscribers that block are forcibly dropped.
  • Assessment: Exemplary context discipline. Context flows through every layer from HTTP handler down to physical storage — even the AES-GCM barrier operations accept a context. This enables robust request cancellation and leak prevention.

Sync primitives#

  • Usage: sync.Mutex, sync.RWMutex, sync.Once, sync.WaitGroup, sync.Map, and atomic.* — 877 occurrences total.
  • Example: helper/fairshare/jobmanager.go:l sync.RWMutex protects the queues map; vault/eventbus/bus.go:subscriptions atomic.Int64 tracks subscription count without a lock; vault/core.go contains dozens of named mutexes protecting individual subsystems.
  • Assessment: Core notably uses fine-grained named mutexes (e.g., mountsLock, auditLock, credLock) rather than one big lock — correct for a heavily concurrent server. atomic is used for simple counters and boolean flags.

errgroup (limited use)#

  • Usage: Only 1 file — command/operator_migrate.go:394. Explicitly scoped to the migration operator which reads from a source and writes to a destination concurrently.
  • Assessment: Intentionally minimal. Vault avoids errgroup for the core request path, preferring explicit channel patterns that expose more control over cancellation and shutdown ordering.

EventBus pipeline (publish/subscribe)#

  • Usage: vault/eventbus/bus.go implements a broker using hashicorp/eventlogger. Subscribers call subscribeInternal which creates a named eventlogger.Pipeline of filter/formatter nodes; the broker routes all events through eventTypeAll and each pipeline applies its own predicate (using go-bexpr boolean expressions).
  • Example: vault/eventbus/bus.go:323pipeline := eventlogger.Pipeline{...} with a filter node, formatter node (CloudEvents), and async sink node per subscriber.
  • Assessment: Powerful but complex. The per-subscriber pipeline approach allows fine-grained filtering without broadcasting raw events. The atomic.Int64 subscription counter and atomic.Bool started flag are clean synchronization choices for the broker lifecycle.

Error handling#

Style: mixed legacy + modern#

Vault shows a clear generational progression. Three distinct wrapping strategies coexist:

  1. fmt.Errorf("%w", err) — 2,924 occurrences, dominant in all code written after Go 1.13. This is the primary approach in vault/, sdk/, and recent command/ code.
  2. hashicorp/errwrap — 212 occurrences, concentrated in api/ and vault/ core. Uses the {{err}} template format: errwrap.Wrapf("context: {{err}}", err). Pre-Go 1.13 legacy.
  3. pkg/errors — 7 files only (command/base.go, command/ssh.go, command/operator_migrate.go, physical/spanner/, physical/gcs/). Minimal and confined to older subpackages.

Sentinel errors#

Many package-level var Err* = errors.New(...) sentinels are defined:

  • api.ErrSecretNotFoundapi/kv.go:10
  • vault.ErrInRestoreModevault/expiration.go:2717
  • vault.errReadOnlyvault/barrier_access.go:76
  • sdk/database.ErrNotInitializedsdk/database/helper/connutil/connutil.go:12
  • eventbus.ErrNotStartedvault/eventbus/bus.go:47

Custom error types#

  • sdk/helper/errutil.UserError / InternalError — lightweight type distinction between user-input errors (4xx) and server errors (5xx). Used by backends to set HTTP response status.
  • vault.NonFatalErrorvault/core.go:179 — wraps errors that should not halt Vault startup (NewCore can return one alongside a valid *Core).
  • vault/seal.PartialSealWrapError — signals that only some seal wrappers succeeded during a multi-seal write.
  • internalshared/configutil.ConfigError — config file parse errors with location context.

Multi-error aggregation#

hashicorp/go-multierror is used when multiple independent operations can each fail:

  • command/proxy.go:1082 — collecting shutdown errors from multiple subsystems
  • api/cliconfig/config.go:88 — collecting all config validation errors rather than stopping at the first

errors.Is / errors.As#

164 usages — used for error type inspection at boundaries (HTTP handler checking for UserError to set 400 vs 500, unseal checking for NonFatalError).


Configuration pattern#

CoreConfig struct injection#

The primary configuration mechanism is a large config struct passed to vault.NewCore(CoreConfig{...}). It carries all factory maps (not instances) for backends:

vault.NewCore(vault.CoreConfig{
    Physical:           physicalBackend,
    LogicalBackends:    map[string]logical.Factory{ "kv": kv.Factory, ... },
    CredentialBackends: map[string]logical.Factory{ "approle": approle.Factory, ... },
    AuditBackends:      map[string]audit.Factory{ "file": file.Factory, ... },
    Logger:             logger,
    ...
})

Backends are instantiated lazily when first mounted. This avoids constructing hundreds of unused backend instances at startup.

Functional options in api/auth/*#

All auth method clients in api/auth/ use the functional options pattern:

type LoginOption func(a *AppRoleAuth) error

func WithMountPath(mountPath string) LoginOption { ... }
func WithWrappingToken() LoginOption { ... }

auth, err := approle.NewAppRoleAuth(roleID, &secretID, approle.WithMountPath("custom/approle"))

This pattern appears consistently across every auth method package (approle, azure, cert, userpass, jwt, kubernetes). It enables optional parameters without breaking the API when new options are added.


Dependency injection#

Approach: manual wiring via factory maps#

Vault uses no DI framework. The injection is performed manually in command/server.go by:

  1. Constructing the physical backend
  2. Building factory maps (string → logical.Factory function)
  3. Passing the CoreConfig struct to vault.NewCore

Plugins are never imported directly into CoreConfig; only their factory functions are registered. This means Core is decoupled from all plugin implementations at compile time — a factory is just func(context.Context, *BackendConfig) (Backend, error).

Plugin registry#

helper/builtinplugins.Registry is an immutable, package-level registry (var Registry = newRegistry()) that maps plugin names to logical.Factory and deprecation status. It is thread-safe by design (no mutation after construction). External plugins registered at runtime are stored in the PluginCatalog which IS mutable (protected by a mutex).


Other notable patterns#

Table-driven tests#

  • Prevalence: 964 testCases/tests := occurrences in *_test.go files — extremely heavy use across the entire codebase.
  • Style: Almost universally anonymous []struct{} with inline test case definitions, following stdlib convention.
  • Example: vault/core_test.go — test cases for unseal sequences, token validation, and mount operations are all table-driven.

Interface embedding#

Core interfaces are kept minimal and composed through embedding:

  • logical.Storage (4 methods: List/Get/Put/Delete) is embedded in logical.Backend’s StorageView
  • api.LogicalRequest embeds api.BaseLogicalRequest to layer optional fields

Type switches (142 uses)#

Used heavily in the framework layer (sdk/framework/) for dispatching on logical.Operation values and in command dispatch for determining the concrete backend type.

Generics (limited, Go 1.18+)#

  • helper/syncmap.SyncMap[K comparable, V IDer] — generic synchronized map used for identity store lookups
  • vault/logical_system_custom_messages.go:323parameterValidateAndUse[T bool | string] and similar helpers for HCL parameter handling
  • Overall: Generics are used sparingly and pragmatically, not retrofitted into existing code.

Builder pattern (domain-specific)#

vault/identity_store_entities_update.go:17 defines EntityBuilder for constructing identity entities during update operations. Protobuf-generated code uses .Build() extensively but that is generated, not handwritten.

Registry pattern#

helper/builtinplugins.Registry is a read-only plugin registry initialized once at package init time. The pattern “var X = newX()” with an unexported constructor returning an immutable value is used in several places (event subscribers, quota managers) to provide global singletons safely.

sync.Once guards (72 uses)#

Used consistently for one-time lifecycle operations:

  • helper/fairshare.JobManager.onceStart / onceStop — prevent double-start and double-stop of the worker pool
  • vault/core.go — multiple sync.Once guards for initializing subsystems that must run exactly once during unseal

Security-oriented patterns#

Several patterns appear specifically because Vault is a secrets management system:

  • Memory locking (mlock): vault.Core calls mlock.LockMemory() during initialization on Linux to prevent secrets from being swapped to disk.
  • BarrierView chroot pattern: Every plugin gets a BarrierView — a namespaced, read-only-enforcing wrapper around storage that prevents path traversal. The name “chroot” appears in the code comment (vault/barrier_view.go:16).
  • Audit-before-action: Requests are written to the audit log before the backend handles them. If audit logging fails, the request is rejected. This is an unusual but intentional ordering that prioritizes audit correctness over availability.
  • NonFatalError on startup: vault.NewCore can return both a valid *Core and a NonFatalError, allowing the server to start in a degraded state (e.g., some backends failed to register) rather than refusing to start.