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, plusenterpriseAuthorizerembedding - Purpose: The central ACL policy enforcement contract. Every protected operation calls an
Authorizermethod before executing. Returns a tri-stateEnforcementDecision(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
enterpriseAuthorizerembedding 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, plusenterpriseDelegate - Purpose: The single abstraction that separates
Agent(the runtime coordinator) from its two operational personalities:consul.Server(with Raft, full gossip, state store) andconsul.Client(with LAN gossip only, RPC forwarding).Agentonly callsdelegate; it never directly referencesconsul.Serverorconsul.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
Typeand registers itself with theCacheat startup.FetchOptions.MinIndexdrives long-poll behavior; the cache deduplicates concurrent waiters per request key. - Implementations: ~30+, including
ServiceHealthRequest,CARoots,CompiledDiscoveryChain,ConfigEntry,PreparedQuery,Intentions, etc. inagent/cache-types/ - Design quality: Elegant two-method interface. The
RegisterOptionsmethod lets each type declare its own TTL, staleness limits, and refresh policy. The separation ofState(opaque per-entry bookkeeping) fromValue(returned to callers) inFetchResultis 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 embedsPrimaryProviderandSecondaryProvider - Purpose: The Certificate Authority plugin interface.
Provideris 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) errorPrimaryUsesIntermediate(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 —
PrimaryandSecondaryoperations are cleanly separated because only one applies to a given datacenter. The optionalNeedsStopandPrimaryUsesIntermediateinterfaces use Go’s implicit satisfaction for additive capabilities without cluttering the core interface.ErrRateLimitedsentinel 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.Resourceproto 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
ReadConsistencyenum (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
Reconcilerthat is called whenever a watched resource changes. TheRuntimeprovides access to the resource client, a logger, and a typed cache. The controller framework handles retry with exponential backoff; returningRequeueAfterErroroverrides 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
RequeueAfterErrorsentinel type (atime.Durationalias) is an idiomatic Go pattern that avoids adding configuration to the interface itself. Mirrors Kubernetes’ controller-runtimeReconcilerinterface (Reconcile(ctx, Request) (Result, error)) with minor adaptations for Consul’sRuntimetype.
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
RequestcarriesKind,Name, andEnterpriseMeta(config entry coordinates) instead of apbresource.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
EventPublishercarries aPayload. TheHasReadPermissionmethod 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
Subjectmethod 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, plusDefer(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-memdbthat add type safety and enable the change-tracking machinery that feeds theEventPublisher.WriteTxnis the only way to write;ReadTxnis the only way to read — neither exposes the underlyingmemdb.Txndirectly. - 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. TheAbortTxnembedding onReadTxnensures 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.
RegisterHandlerallows subsystems to declare how they produce snapshot events (for new subscribers who need the full current state before receiving deltas). TheSnapshotFunccallback 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.Managerholds aDataSourcesstruct (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 theproxycfg-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<- UpdateEventpattern is consistent across all — a small domain-specific protocol for multiplexed subscriptions over a single goroutine. The cost: significant boilerplate and visual noise indata_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— addsDefaultStatus(),GetStatus(),SetStatus()for v1 controller-managed entriesUpdatableConfigEntry— addsUpdateOver(prev ConfigEntry) errorfor merge-not-replace semantics on upsertWarningConfigEntry— addsWarnings() []stringfor non-fatal validation messages
- Design quality: Good use of interface embedding for optional capabilities. The
CanRead/CanWritemethods 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. TheResourceServiceresolves type metadata viaResolvefor every CRUD operation. - Implementations:
TypeRegistry(production, with mutex-protected map), plus mocks. - Design quality: Simple registry pattern.
Registrationis 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, plusEnterpriseACLResolverDelegate - Purpose: The data-access interface for
ACLResolver. By defining this interface,ACLResolvercan 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 (
ACLResolverdefines what it needs;Serverprovides it). TheEnterpriseACLResolverDelegateembedding follows the same pattern asAuthorizer’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
Watchonce per Envoy stream; it receives a channel ofConfigSnapshotupdates and two session-termination signals (one for overload shedding, one for source termination). TheCancelFunctears 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
Viewaccumulates incremental events into a query result that is then cached in theStore. 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.goand similar files. - Design quality: Clean three-method design.
Reset()is called when the stream reconnects and a new snapshot is being received. TheResult(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) anddelegate(~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:
ConfigEntryis embedded byControlledConfigEntry,UpdatableConfigEntry,WarningConfigEntry— classic capability extension without modifying the base. - Enterprise extension:
AuthorizerembedsenterpriseAuthorizer;delegateembedsenterpriseDelegate;ACLResolverBackendembedsEnterpriseACLResolverDelegate. Consul’s build-tag system uses this to add enterprise methods invisibly to OSS consumers. - Transaction extension:
WriteTxnembedsReadTxn, which embeds read operations;AbortTxnextendsReadTxnwith 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.godefines the 20Notifyinterfaces — 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.Stringer—stream.Topicandstream.Subjectare declared astype Topic fmt.Stringer, making them typed aliases of the stringer interface.io.Reader,io.Writer— used inSnapshotRPCon 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.