Consul — Interfaces#

Sampling note#

Consul has 373 non-test, non-vendor interface definitions across ~500 files. This analysis focuses on the ~20 most architecturally significant interfaces, sampled from the core packages identified in the architecture analysis: acl/, agent/, agent/consul/, agent/proxycfg/, internal/controller/, internal/storage/, and agent/consul/stream/. Trivial single-file-scope interfaces (e.g. windowsSystem, partitionUnsetter, ticker) are omitted.


Interface catalog#

Authorizer#

  • Package: acl
  • File: acl/authorizer.go:59
  • Methods: ~35, e.g. ACLRead(*AuthorizerContext) EnforcementDecision, ServiceRead(string, *AuthorizerContext) EnforcementDecision, NodeWrite(string, *AuthorizerContext) EnforcementDecision, Snapshot(*AuthorizerContext) EnforcementDecision, ToAllowAuthorizer() AllowAuthorizer, plus enterpriseAuthorizer embedding
  • Purpose: The central ACL policy enforcement contract. Every protected operation calls an Authorizer method before executing. Returns a tri-state EnforcementDecision (Allow/Deny/Default) rather than bool to support policy chaining.
  • Implementations: PolicyAuthorizer (rule-based from HCL policy), AllowAuthorizer (wrapper that panics on Deny for cleaner call sites), DenyAll, AllowAll, and several enterprise CE shims.
  • Design quality: Intentionally broad — this is a central contract surface that must cover every resource type in Consul. The tri-state return (not bool) is a deliberate design choice enabling default-deny vs default-allow policy combinations. The enterpriseAuthorizer embedding extends it invisibly in enterprise builds — an honest acknowledgement that OSS and enterprise share one interface.

delegate (unexported)#

  • Package: agent
  • File: agent/agent.go:153
  • Methods: Leave() error, AgentLocalMember() serf.Member, LANMembersInAgentPartition() []serf.Member, LANMembers(f LANMemberFilter) ([]serf.Member, error), GetLANCoordinate() (CoordinateSet, error), JoinLAN(addrs []string, meta *EnterpriseMeta) (int, error), RemoveFailedNode(node string, prune bool, meta *EnterpriseMeta) error, ResolveTokenAndDefaultMeta(...) (resolver.Result, error), RPC(ctx, method, args, reply) error, ResourceServiceClient() pbresource.ResourceServiceClient, SnapshotRPC(...) error, Shutdown() error, Stats() map[string]map[string]string, ReloadConfig(ReloadableConfig) error, plus enterpriseDelegate
  • Purpose: The single abstraction that separates Agent (the runtime coordinator) from its two operational personalities: consul.Server (with Raft, full gossip, state store) and consul.Client (with LAN gossip only, RPC forwarding). Agent only calls delegate; it never directly references consul.Server or consul.Client.
  • Implementations: consul.Server, consul.Client
  • Design quality: This is the most architecturally significant interface in Consul. It enables all health check management, HTTP serving, and proxy config to be written once regardless of server/client role. The unexported name is intentional — it’s a package-internal seam, not a public plugin point.

cache.Type#

  • Package: agent/cache
  • File: agent/cache/type.go:14
  • Methods: Fetch(ctx context.Context, opts FetchOptions, req Request) (FetchResult, error), RegisterOptions() RegisterOptions
  • Purpose: The plugin interface for the client-side blocking-query cache. Any type of data that can be watched via blocking queries implements Type and registers itself with the Cache at startup. FetchOptions.MinIndex drives long-poll behavior; the cache deduplicates concurrent waiters per request key.
  • Implementations: ~30+, including ServiceHealthRequest, CARoots, CompiledDiscoveryChain, ConfigEntry, PreparedQuery, Intentions, etc. in agent/cache-types/
  • Design quality: Elegant two-method interface. The RegisterOptions method lets each type declare its own TTL, staleness limits, and refresh policy. The separation of State (opaque per-entry bookkeeping) from Value (returned to callers) in FetchResult is subtle but important for types that need cross-fetch state without leaking it.

connect/ca.Provider#

  • Package: agent/connect/ca
  • File: agent/connect/ca/provider.go:65
  • Methods: Configure(cfg ProviderConfig) error, State() (map[string]string, error), ActiveLeafSigningCert() (string, error), Sign(*x509.CertificateRequest) (string, error), Cleanup(providerTypeChange bool, otherConfig map[string]interface{}) error, plus embeds PrimaryProvider and SecondaryProvider
  • Purpose: The Certificate Authority plugin interface. Provider is the full interface for any CA backend; it embeds role-specific sub-interfaces. Implementations can be swapped at runtime via the CA config entry without restarting.
  • Sub-interfaces:
    • PrimaryProvider: GenerateCAChain() (string, error), SignIntermediate(*x509.CertificateRequest) (string, error), CrossSignCA(*x509.Certificate) (string, error), SupportsCrossSigning() (bool, error)
    • SecondaryProvider: GenerateIntermediateCSR() (string, string, error), SetIntermediate(intermediatePEM, rootPEM, opaque string) error
    • PrimaryUsesIntermediate (optional): GenerateLeafSigningCert() (string, error)
    • NeedsStop (optional): Stop()
  • Implementations: Built-in Consul CA, Vault PKI secrets engine, AWS ACM Private CA; external implementations via the plugin API.
  • Design quality: Excellent use of interface segregation — Primary and Secondary operations are cleanly separated because only one applies to a given datacenter. The optional NeedsStop and PrimaryUsesIntermediate interfaces use Go’s implicit satisfaction for additive capabilities without cluttering the core interface. ErrRateLimited sentinel allows providers to signal backpressure uniformly.

storage.Backend#

  • Package: internal/storage
  • File: internal/storage/storage.go:122
  • Methods: Read(ctx, consistency, id *pbresource.ID) (*pbresource.Resource, error), WriteCAS(ctx, res *pbresource.Resource) (*pbresource.Resource, error), DeleteCAS(ctx, id *pbresource.ID, version string) error, List(ctx, consistency, resType, tenancy, namePrefix) ([]*pbresource.Resource, error), WatchList(ctx, resType, tenancy, namePrefix) (Watch, error), ListByOwner(ctx, id *pbresource.ID) ([]*pbresource.Resource, error)
  • Purpose: The v2 resource system’s storage abstraction. Operates on generic pbresource.Resource proto messages, making it type-agnostic. All writes are CAS (compare-and-swap) operations; non-CAS writes are implemented at a higher layer by read-modify-write loops.
  • Sub-interface Watch: Next(context.Context) (*pbresource.Event, error), Close()
  • Implementations: internal/storage/raft (production, Raft-backed via go-memdb), internal/storage/inmem (tests)
  • Design quality: Well-designed with strong consistency guarantees documented per method. The ReadConsistency enum (Eventual/Strong) allows explicit tradeoffs. Wildcard tenancy ("*") for cross-namespace queries is built into the interface contract, not bolted on. The conformance test suite (internal/storage/conformance) is generated to verify all Backend implementations.

internal/controller.Reconciler#

  • Package: internal/controller
  • File: internal/controller/controller.go:305
  • Methods: Reconcile(ctx context.Context, rt Runtime, req Request) error
  • Purpose: The v2 controller reconciliation interface. Each controller registers a Reconciler that is called whenever a watched resource changes. The Runtime provides access to the resource client, a logger, and a typed cache. The controller framework handles retry with exponential backoff; returning RequeueAfterError overrides the backoff.
  • Companion interface Initializer: Initialize(ctx context.Context, rt Runtime) error — called once on controller start to pre-populate caches.
  • Implementations: All v2 feature controllers: TrafficPermissionsController, EndpointsController, mesh gateway controllers, etc.
  • Design quality: Admirably minimal — one method. The RequeueAfterError sentinel type (a time.Duration alias) is an idiomatic Go pattern that avoids adding configuration to the interface itself. Mirrors Kubernetes’ controller-runtime Reconciler interface (Reconcile(ctx, Request) (Result, error)) with minor adaptations for Consul’s Runtime type.

agent/consul/controller.Reconciler (v1, config-entry-based)#

  • Package: agent/consul/controller
  • File: agent/consul/controller/reconciler.go:61
  • Methods: Reconcile(context.Context, Request) error
  • Purpose: The v1 controller reconciliation interface, operating on config entries (not generic resources). Same pattern as v2 but Request carries Kind, Name, and EnterpriseMeta (config entry coordinates) instead of a pbresource.ID.
  • Implementations: API gateway, ingress gateway, and other config-entry-driven controllers.
  • Design quality: Nearly identical to the v2 Reconciler. The existence of two parallel Reconciler interfaces (v1 and v2) reflects Consul’s migration strategy — v1 controllers operate on the old config-entry system; v2 controllers operate on the new resource system. Both will coexist for the foreseeable future.

stream.Payload#

  • Package: agent/consul/stream
  • File: agent/consul/stream/event.go:51
  • Methods: HasReadPermission(authz acl.Authorizer) bool, Subject() Subject, ToSubscriptionEvent(idx uint64) *pbsubscribe.Event
  • Purpose: The event streaming payload interface. Every change event published to EventPublisher carries a Payload. The HasReadPermission method allows the publisher to filter events per-subscriber without knowing the payload type, applying ACL checks inline at delivery time.
  • Implementations: One per catalog/config event type: EventPayloadCheckServiceNode (health), ConfigEntryEvent, NodeEvent, ServiceEvent, etc.
  • Design quality: Clean ISP. Three methods, each serving a distinct concern: ACL filtering, routing to subscribers, and protocol serialization. The Subject method drives a topic-partitioned pub/sub model, allowing subscribers to watch only relevant events (e.g. health events for service “web” in partition “default”).

state.ReadTxn / WriteTxn#

  • Package: agent/consul/state
  • File: agent/consul/state/memdb.go:16
  • ReadTxn methods: Get(table, index string, args ...interface{}) (ResultIterator, error), First(table, index string, args ...interface{}) (interface{}, error), FirstWatch(table, index string, args ...interface{}) (<-chan struct{}, interface{}, error)
  • WriteTxn methods: Embeds ReadTxn, plus Defer(func()), Delete(table, obj) error, DeleteAll(table, index, args) (int, error), DeletePrefix(table, index, prefix) (bool, error), Insert(table, obj) error
  • Purpose: Thin wrappers over go-memdb that add type safety and enable the change-tracking machinery that feeds the EventPublisher. WriteTxn is the only way to write; ReadTxn is the only way to read — neither exposes the underlying memdb.Txn directly.
  • Design quality: The wrapping approach is necessary for two reasons: (1) the txn.Commit() override publishes change events to subscribers before finalizing, and (2) using interfaces prevents direct memdb access from state-store callers, reducing coupling. The AbortTxn embedding on ReadTxn ensures that read transactions are always cleaned up.

state.EventPublisher#

  • Package: agent/consul/state
  • File: agent/consul/state/memdb.go:61
  • Methods: Publish([]stream.Event), RegisterHandler(stream.Topic, stream.SnapshotFunc, bool) error, Subscribe(*stream.SubscribeRequest) (*stream.Subscription, error)
  • Purpose: The pub/sub broker for state store change events. After a WriteTxn.Commit(), the processed change events are published here. Subscribers (e.g. streaming clients, proxycfg watchers) receive these events in real time.
  • Implementations: stream.EventPublisher (the production implementation with TTL window), plus test mocks.
  • Design quality: Minimal surface. RegisterHandler allows subsystems to declare how they produce snapshot events (for new subscribers who need the full current state before receiving deltas). The SnapshotFunc callback pattern keeps the publisher generic.

proxycfg DataSources (set of 20 single-method interfaces)#

  • Package: agent/proxycfg
  • File: agent/proxycfg/data_sources.go:139+
  • Pattern: Each interface has exactly one method: Notify(ctx context.Context, req *structs.XxxRequest, correlationID string, ch chan<- UpdateEvent) error
  • Interfaces (20+): CARoots, CompiledDiscoveryChain, ConfigEntry, ConfigEntryList, Datacenters, Health, HTTPChecks, Intentions, IntentionUpstreams, LeafCertificate, GatewayServices, ServiceGateways, PeeringList, PreparedQuery, ResolvedServiceConfig, ServiceList, TrustBundle, TrustBundleList, ExportedPeeredServices, and more.
  • Purpose: The complete set of data dependencies for proxy config generation. proxycfg.Manager holds a DataSources struct (not an interface — a struct of interfaces), so it can independently substitute each data source.
  • Implementations: Two parallel implementations in agent/proxycfg-glue/ — one backed by the cache layer (for client agents), one backed by direct state store reads (for server agents). These are wired up via the proxycfg-sources/ packages.
  • Design quality: Exemplary ISP. Defining 20 separate single-method interfaces rather than one large “data layer” interface allows each data dependency to be replaced, tested, or stubbed independently. The correlationID + ch chan<- UpdateEvent pattern is consistent across all — a small domain-specific protocol for multiplexed subscriptions over a single goroutine. The cost: significant boilerplate and visual noise in data_sources.go.

structs.ConfigEntry#

  • Package: agent/structs
  • File: agent/structs/config_entry.go:84
  • Methods: GetKind() string, GetName() string, Normalize() error, Validate() error, CanRead(acl.Authorizer) error, CanWrite(acl.Authorizer) error, GetMeta() map[string]string, GetEnterpriseMeta() *acl.EnterpriseMeta, GetRaftIndex() *RaftIndex, GetHash() uint64, SetHash(h uint64)
  • Purpose: The base interface for all centralized configuration entries stored in Raft. Implementations include ServiceDefaults, ProxyDefaults, ServiceRouter, ServiceSplitter, ServiceResolver, IngressGateway, TerminatingGateway, ServiceIntentions, MeshConfig, APIGateway, HTTPRoute, TCPRoute, and more (19 kinds).
  • Extended by:
    • ControlledConfigEntry — adds DefaultStatus(), GetStatus(), SetStatus() for v1 controller-managed entries
    • UpdatableConfigEntry — adds UpdateOver(prev ConfigEntry) error for merge-not-replace semantics on upsert
    • WarningConfigEntry — adds Warnings() []string for non-fatal validation messages
  • Design quality: Good use of interface embedding for optional capabilities. The CanRead/CanWrite methods on the entry itself (rather than on a separate authorizer) means each entry type owns its own ACL logic — a localization tradeoff that reduces the Authorizer’s surface area at the cost of distribution across 19 types.

resource.Registry#

  • Package: internal/resource
  • File: internal/resource/registry.go:33
  • Methods: Register(reg Registration), Resolve(typ *pbresource.Type) (reg Registration, ok bool), Types() []Registration
  • Purpose: The v2 resource type registry. Teams register their resource types (with proto message, scope, validation hooks, mutation hooks, and ACL hooks) via Register. The ResourceService resolves type metadata via Resolve for every CRUD operation.
  • Implementations: TypeRegistry (production, with mutex-protected map), plus mocks.
  • Design quality: Simple registry pattern. Registration is a struct (not an interface) containing function hooks — a more flexible approach than requiring each type to implement an interface. The panic-on-invalid-registration approach (checked at startup) catches configuration errors early.

ACLResolverBackend#

  • Package: agent/consul
  • File: agent/consul/acl.go:139
  • Methods: ACLDatacenter() string, ResolveIdentityFromToken(token string) (bool, ACLIdentity, error), ResolvePolicyFromID(policyID string) (bool, *ACLPolicy, error), ResolveRoleFromID(roleID string) (bool, *ACLRole, error), IsServerManagementToken(token string) bool, RPC(ctx, method, args, reply) error, plus EnterpriseACLResolverDelegate
  • Purpose: The data-access interface for ACLResolver. By defining this interface, ACLResolver can resolve tokens from either the local state store (on server) or via RPC to the authoritative datacenter (on client), without the resolver knowing which path it’s on.
  • Implementations: consul.Server (directly, satisfies interface with its state store methods)
  • Design quality: Classic backend pattern — consumer-defined interface (ACLResolver defines what it needs; Server provides it). The EnterpriseACLResolverDelegate embedding follows the same pattern as Authorizer’s enterprise extension.

xds.ProxyWatcher#

  • Package: agent/xds
  • File: agent/xds/server.go:86
  • Methods: Watch(proxyID ServiceID, nodeName string, token string) (<-chan *proxycfg.ConfigSnapshot, limiter.SessionTerminatedChan, proxycfg.SrcTerminatedChan, context.CancelFunc, error)
  • Purpose: The interface between the xDS gRPC server and the proxycfg manager. The xDS server calls Watch once per Envoy stream; it receives a channel of ConfigSnapshot updates and two session-termination signals (one for overload shedding, one for source termination). The CancelFunc tears down the watch.
  • Implementations: proxycfg.Manager
  • Design quality: Narrow, single-method. The multi-return signature is unconventional but reflects the three distinct lifecycle signals needed: snapshot updates, overload shedding, and source-side disconnect. Using channels here (rather than callbacks) keeps the xDS server’s event loop explicit and easy to reason about with select.

submatview.View#

  • Package: agent/submatview
  • File: agent/submatview/materializer.go:22
  • Methods: Update(events []*pbsubscribe.Event) error, Result(index uint64) interface{}, Reset()
  • Purpose: The interface for materialized views backed by the streaming subscription system. Each View accumulates incremental events into a query result that is then cached in the Store. Used for cache types that need real-time streaming rather than blocking-query polling.
  • Implementations: Health service view, service list view, etc. in agent/rpcclient/health/view.go and similar files.
  • Design quality: Clean three-method design. Reset() is called when the stream reconnects and a new snapshot is being received. The Result(index) pattern (index passed in, not stored by the view) is a deliberate separation of concerns — the cache manages the Raft index, the view manages the data.

Interface patterns#

Size distribution#

  • Majority are narrow: The modal size is 1-3 methods. Reconciler (1 method), View (3 methods), cache.Type (2 methods), all 20 proxycfg data source interfaces (1 method each), Watch (2 methods), storage.Backend (6 methods).
  • Legitimately broad: Authorizer (~35 methods) and delegate (~14 methods) are intentionally wide. Both are justified by their role as complete behavioral contracts covering an entire domain.
  • Mid-size: ConfigEntry (~11 methods), connect/ca.Provider (~9 methods), ACLResolverBackend (~7 methods), storage.Backend (6 methods).

Embedding#

  • Interface extension: ConfigEntry is embedded by ControlledConfigEntry, UpdatableConfigEntry, WarningConfigEntry — classic capability extension without modifying the base.
  • Enterprise extension: Authorizer embeds enterpriseAuthorizer; delegate embeds enterpriseDelegate; ACLResolverBackend embeds EnterpriseACLResolverDelegate. Consul’s build-tag system uses this to add enterprise methods invisibly to OSS consumers.
  • Transaction extension: WriteTxn embeds ReadTxn, which embeds read operations; AbortTxn extends ReadTxn with cleanup.

Implicit satisfaction (consumer vs. provider defined)#

Nearly all significant interfaces in Consul are consumer-defined (defined in the package that uses them, not the package that implements them). Examples:

  • agent/proxycfg/data_sources.go defines the 20 Notify interfaces — consumed by proxycfg, implemented by cache/state glue layers.
  • agent/xds/server.go:ProxyWatcher — consumed by xDS, implemented by proxycfg.
  • agent/consul/acl.go:ACLResolverBackend — consumed by ACLResolver, implemented by consul.Server.
  • agent/consul/fsm/fsm.go:StorageBackend — consumed by FSM, implemented by raftstorage.

This pervasive consumer-definition pattern prevents circular imports and ensures interfaces are sized to actual consumer needs (not provider capabilities).

stdlib interfaces used#

  • fmt.Stringerstream.Topic and stream.Subject are declared as type Topic fmt.Stringer, making them typed aliases of the stringer interface.
  • io.Reader, io.Writer — used in SnapshotRPC on the delegate interface.
  • context.Context — used everywhere; Consul’s async operations universally accept context for cancellation.

Generics usage#

The agent/consul/controller/queue package uses Go generics (1.18+): WorkQueue[T ItemType], DeferQueue[T ItemType], Limiter[T ItemType]. This is isolated to the queue infrastructure and does not yet appear in higher-level interfaces.

Mock generation#

Systematic use of //go:generate mockery --name InterfaceName --inpackage across most significant interfaces: cache.Type, ca.Provider, ACLResolver, StateStore (multiple local variants), Backend (resource service), Registry, etc. This generates mock_*.go files alongside source files, ensuring mocks stay in sync.


Key abstractions#

1. delegate — The Server/Client Bifurcation Seam#

The most impactful interface in Consul. Without it, Agent would have to fork on ServerMode in dozens of places. With it, all per-agent logic (health checks, HTTP API, proxy config, DNS) is written once and works regardless of whether the node runs Raft or not. The interface is exactly as wide as Agent needs — not the full API of consul.Server.

2. Authorizer — Universal Policy Enforcement#

Consul’s security model flows through this single interface. Every protected operation — RPC handler, HTTP handler, DNS response, event delivery — calls Authorizer methods. The tri-state EnforcementDecision enables layered policy evaluation (token policy → role policy → default policy). It’s the only legitimately wide interface in the codebase, and it earns its width.

3. connect/ca.Provider — The CA Plugin System#

This is Consul’s primary plugin interface for external systems. The PrimaryProvider/SecondaryProvider split is architecturally elegant: it reflects the real operational boundary (primary DC generates roots; secondary DCs get signed intermediates). The optional NeedsStop interface demonstrates Go’s additive capability pattern in production.

4. storage.Backend — The V2 Storage Abstraction#

This interface makes the v2 resource system backend-agnostic. The strong CAS semantics and well-defined consistency levels (EventualConsistency / StrongConsistency) are rare in internal Go interfaces — most projects use opaque error returns. The companion Watch interface and ErrWatchClosed sentinel make the streaming contract explicit. The conformance test suite is the gold standard for interface contract testing.

5. The proxycfg Notify family — Maximum ISP#

Twenty single-method interfaces, each representing one data dependency for Envoy config generation. This is the most extreme application of the Interface Segregation Principle in the codebase. Each interface can be implemented by either the cache layer or the state store directly, allowing the same proxycfg logic to run on both client agents and server agents without a conditional in sight.


Interface-driven extensibility#

CA Provider plugins (connect/ca.Provider)#

The most explicit plugin system. Consul ships with three built-in providers (Consul built-in, Vault, AWS ACM PCA). External providers can implement Provider and be registered via configuration. The CA rotation system calls CrossSignCA on the old provider to issue a cross-signed cert from the new root, enabling zero-downtime transitions.

Config entry extensibility (structs.ConfigEntry)#

New networking features (routing, rate limiting, JWT auth, API gateways) are added by implementing ConfigEntry and registering the kind in AllConfigEntryKinds. The state store, Raft FSM, HTTP API, and CLI all operate generically on the ConfigEntry interface — adding a new type requires only implementing the interface and a few registration lines.

V2 resource type registration (resource.Registry)#

Teams register resource types via Registry.Register(Registration{...}) with hooks for validation, mutation, and ACL. The ResourceService gRPC server then handles CRUD for the type generically. This shifts new feature development from full-stack (HTTP → RPC → state store → Raft → CLI) to interface-level (define proto + register + write controller).

Cache type plugins (cache.Type)#

The client-side cache is fully pluggable. Any data type that supports blocking queries can implement cache.Type and be registered at startup with Cache.RegisterType(). The cache then handles deduplication, TTL, and refresh automatically.

Event streaming payloads (stream.Payload)#

New event topics are added by implementing stream.Payload and writing a SnapshotFunc for initial state delivery. The EventPublisher and all subscriber infrastructure are generic over Payload, so no changes are needed to the pub/sub core when adding new event types.