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 aShutdownCh chan struct{}field. Closing the channel signals all listening goroutines to stop. - Example:
command/proxy.go:742—case <-c.ShutdownCh:in the main select loop.MakeShutdownCh()incommand/commands.gowires 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 theExpirationManagerto process lease revocations without letting one tenant’s burst of expirations starve others. - Example:
helper/fairshare/jobmanager.go—JobManagermaintains per-queuelist.Listinstances and assigns work todispatcherworkers using round-robin across queues. ImplementsonceStart/onceStop sync.Onceguards 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.go—selectonquit,newWorkchannels drives the job dispatch loop. - Assessment: Idiomatic. Vault uses
selectcorrectly: always with adefaultcase or explicit timeout where appropriate to avoid blocking.
Context cancellation and timeouts#
- Usage:
context.Contextappears 4,392 times.context.WithCancel/WithTimeout/WithDeadlineandctx.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, andatomic.*— 877 occurrences total. - Example:
helper/fairshare/jobmanager.go:l sync.RWMutexprotects thequeuesmap;vault/eventbus/bus.go:subscriptions atomic.Int64tracks subscription count without a lock;vault/core.gocontains dozens of named mutexes protecting individual subsystems. - Assessment:
Corenotably uses fine-grained named mutexes (e.g.,mountsLock,auditLock,credLock) rather than one big lock — correct for a heavily concurrent server.atomicis 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
errgroupfor 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.goimplements a broker usinghashicorp/eventlogger. Subscribers callsubscribeInternalwhich creates a namedeventlogger.Pipelineof filter/formatter nodes; the broker routes all events througheventTypeAlland each pipeline applies its own predicate (usinggo-bexprboolean expressions). - Example:
vault/eventbus/bus.go:323—pipeline := 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.Int64subscription counter andatomic.Boolstarted 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:
fmt.Errorf("%w", err)— 2,924 occurrences, dominant in all code written after Go 1.13. This is the primary approach invault/,sdk/, and recentcommand/code.hashicorp/errwrap— 212 occurrences, concentrated inapi/andvault/core. Uses the{{err}}template format:errwrap.Wrapf("context: {{err}}", err). Pre-Go 1.13 legacy.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.ErrSecretNotFound—api/kv.go:10vault.ErrInRestoreMode—vault/expiration.go:2717vault.errReadOnly—vault/barrier_access.go:76sdk/database.ErrNotInitialized—sdk/database/helper/connutil/connutil.go:12eventbus.ErrNotStarted—vault/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.NonFatalError—vault/core.go:179— wraps errors that should not halt Vault startup (NewCorecan 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 subsystemsapi/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:
- Constructing the physical backend
- Building factory maps (string →
logical.Factoryfunction) - Passing the
CoreConfigstruct tovault.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.gofiles — 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 inlogical.Backend’sStorageViewapi.LogicalRequestembedsapi.BaseLogicalRequestto 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 lookupsvault/logical_system_custom_messages.go:323—parameterValidateAndUse[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 poolvault/core.go— multiplesync.Onceguards 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.Corecallsmlock.LockMemory()during initialization on Linux to prevent secrets from being swapped to disk. BarrierViewchroot pattern: Every plugin gets aBarrierView— 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.
NonFatalErroron startup:vault.NewCorecan return both a valid*Coreand aNonFatalError, allowing the server to start in a degraded state (e.g., some backends failed to register) rather than refusing to start.