etcd — Interfaces#
Interface catalog#
WatchableKV#
- Package:
go.etcd.io/etcd/server/v3/storage/mvcc - File:
server/storage/mvcc/kv.go - Methods:
KV(embedded) +Watchable(embedded) — effectively ~12 methods includingRead,Write,Compact,Commit,Restore,Close,NewWatchStream - Purpose: The central storage contract in the server. Anything that needs to observe key changes (Watch gRPC) requires the
Watchableextension; everything else only needsKV. This is the type stored inEtcdServer.kv. - Implementations:
watchableStore(internal, wrapsstore) - Design quality: Excellent layering. The embedding chain (
ReadView→TxnRead,WriteView→TxnWrite,KV,WatchableKV) is a textbook incremental interface hierarchy. Each interface adds exactly one conceptual concern.
KV (mvcc)#
- Package:
go.etcd.io/etcd/server/v3/storage/mvcc - File:
server/storage/mvcc/kv.go - Methods:
ReadView // FirstRev() int64; Rev() int64; Range(ctx, key, end, opts) WriteView // DeleteRange(key, end) (n, rev int64); Put(key, value, leaseID) (rev int64) Read(mode ReadTxMode, trace) TxnRead Write(trace) TxnWrite HashStorage() HashStorage Compact(trace, rev int64) (<-chan struct{}, error) Commit() Restore(b backend.Backend) error Close() error - Purpose: Server-internal multi-version key-value store contract. Distinct from
client/v3.KV— uses raw[]bytekeys, returns revision integers, exposes explicit transaction objects. - Implementations:
storestruct (inserver/storage/mvcc/kvstore.go) - Design quality: Well-segregated.
ReadViewandWriteViewlet narrow consumers avoid the full interface. TheTxnRead/TxnWritetransaction types are returned (not accepted), modeling ownership explicitly.
WatchStream#
- Package:
go.etcd.io/etcd/server/v3/storage/mvcc - File:
server/storage/mvcc/watcher.go - Methods:
Watch(ctx, id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) Chan() <-chan WatchResponse RequestProgress(id WatchID) RequestProgressAll() bool Cancel(id WatchID) error Close() Rev() int64 - Purpose: Streaming contract for key-change observation. A server-side stream created per Watch gRPC call. Abstracts the watch multiplexing from the gRPC layer.
- Implementations:
watchStream(internal, inwatchable_store.go) - Design quality: Clean. The channel-return pattern (
Chan() <-chan WatchResponse) is idiomatic Go; callersselecton it without polling.
Backend#
- Package:
go.etcd.io/etcd/server/v3/storage/backend - File:
server/storage/backend/backend.go - Methods:
ReadTx() ReadTx BatchTx() BatchTx ConcurrentReadTx() ReadTx Snapshot() Snapshot Hash(ignores func(bucketName, keyName []byte) bool) (uint32, error) Size() int64 SizeInUse() int64 OpenReadTxN() int64 Defrag() error ForceCommit() Close() error SetTxPostLockInsideApplyHook(func()) - Purpose: Encapsulates bbolt. Provides two read-transaction modes (blocking
ReadTxfor correctness,ConcurrentReadTxfor performance), batched writes, snapshot capability for Raft log truncation. The hook (SetTxPostLockInsideApplyHook) enablesConsistentIndexerto atomically persist the applied Raft index alongside each bbolt commit. - Implementations:
backendstruct (production),MockBackend(tests) - Design quality: Moderately large (12 methods) but each is necessary. The two read-tx modes expose bbolt’s concurrency model deliberately — it is a conscious performance design, not API bloat.
BatchTx#
- Package:
go.etcd.io/etcd/server/v3/storage/backend - File:
server/storage/backend/batch_tx.go - Methods:
Lock() Unlock() Commit() CommitAndStop() LockInsideApply() LockOutsideApply() UnsafeReadWriter // embeds UnsafeReader + UnsafeWriterUnsafeWriter:UnsafeCreateBucket,UnsafeDeleteBucket,UnsafePut,UnsafeSeqPut,UnsafeDeleteUnsafeReader:UnsafeRange(bucket, key, endKey, limit),UnsafeForEach(bucket, visitor) - Purpose: Write interface to bbolt with explicit locking. Two lock variants —
LockInsideApply/LockOutsideApply— signal whether the call happens within the apply loop, enabling atxPostLockInsideApplyHookfor atomic consistent-index persistence. - Implementations:
batchTxBuffered(production),batchTx(base) - Design quality: The
Unsafe*naming convention is strong — it communicates “you must hold the lock” at the call site. Two distinct lock methods for the same mutex is unusual but purposeful: the apply hook logic depends on knowing whether the call is inside the apply path.
ReadTx / UnsafeReader#
- Package:
go.etcd.io/etcd/server/v3/storage/backend - File:
server/storage/backend/read_tx.go - Methods:
// ReadTx: RLock() RUnlock() UnsafeReader // embedded // UnsafeReader: UnsafeRange(bucket Bucket, key, endKey []byte, limit int64) (keys [][]byte, vals [][]byte) UnsafeForEach(bucket Bucket, visitor func(k, v []byte) error) error - Purpose: Read interface to bbolt.
UnsafeReadermethods require the caller to hold the read lock. This is the sameUnsafe*contract enforced by naming. - Implementations:
readTx,concurrentReadTx
Storage (WAL+Snap)#
- Package:
go.etcd.io/etcd/server/v3/storage - File:
server/storage/storage.go - Methods:
Save(st raftpb.HardState, ents []raftpb.Entry) error SaveSnap(snap raftpb.Snapshot) error Close() error Release(snap raftpb.Snapshot) error Sync() error MinimalEtcdVersion() *semver.Version - Purpose: Durability contract used by
raftNode. Combines WAL and snapshot management.raftNodecallsSavebefore advancing committed entries to guarantee durability ordering (WAL before bbolt). - Implementations:
storagestruct (production; wrapswal.WAL+snap.Snapshotter) - Design quality: Small and focused (6 methods). Good example of the adapter pattern: wraps two independent components behind one interface consumed by raftNode.
Lessor (server/lease)#
- Package:
go.etcd.io/etcd/server/v3/lease - File:
server/lease/lessor.go - Methods:
SetRangeDeleter(rd RangeDeleter) SetCheckpointer(cp Checkpointer) Grant(id LeaseID, ttl int64) (*Lease, error) Revoke(id LeaseID) error Checkpoint(id LeaseID, remainingTTL int64) error Attach(id LeaseID, items []LeaseItem) error GetLease(item LeaseItem) LeaseID Detach(id LeaseID, items []LeaseItem) error Promote(extend time.Duration) Demote() Renew(id LeaseID) (int64, error) Lookup(id LeaseID) *Lease Leases() []*Lease ExpiredLeasesC() <-chan []*Lease Recover(b backend.Backend, rd RangeDeleter) Stop() - Purpose: Core lease lifecycle — grant TTL-bound token, attach keys, renew, revoke. Includes leadership state (
Promote/Demote) because lease expiry is only enforced by the leader.SetRangeDeleter/SetCheckpointerare setter methods injected post-construction to break circular dependencies (lessor depends on mvcc, mvcc depends on lessor). - Implementations:
lessorstruct (production),FakeLessor(tests) - Design quality: Moderately large (16 methods) but cohesive — everything relates to lease lifecycle. The setter methods for circular-dependency breaking are a pragmatic workaround; ideally these would be constructor parameters.
AuthStore#
- Package:
go.etcd.io/etcd/server/v3/auth - File:
server/auth/store.go - Methods: ~30 methods covering:
AuthEnable() / AuthDisable() / IsAuthEnabled() / AuthStatus() Authenticate(ctx, username, password) (*AuthenticateResponse, error) UserAdd / UserDelete / UserChangePassword / UserGrantRole / UserGet / UserRevokeRole RoleAdd / RoleGrantPermission / RoleGet / RoleRevokePermission / RoleDelete UserList / RoleList IsPutPermitted / IsRangePermitted / IsDeleteRangePermitted / IsAdminPermitted GenTokenPrefix() (string, error) Revision() uint64 CheckPassword(username, password) (uint64, error) Close() error AuthInfoFromCtx(ctx) / AuthInfoFromTLS(ctx) WithRoot(ctx) context.Context HasRole(user, role) bool BcryptCost() int Recover(be AuthBackend) - Purpose: Unified RBAC contract. The interface is large because it mirrors the auth gRPC service surface plus internal permission-check methods used by
authApplierV3. - Implementations:
authStore(production) - Design quality: Large but justified — the interface must serve both the gRPC handler (user/role CRUD) and the apply-path decorator (permission checks). Could potentially be split into
AuthAdmin(CRUD) andAuthChecker(permission checks) per ISP, but the unified interface simplifies wiring.
TokenProvider#
- Package:
go.etcd.io/etcd/server/v3/auth - File:
server/auth/store.go - Methods:
info(ctx, token string, revision uint64) (*AuthInfo, bool) assign(ctx, username string, revision uint64) (string, error) enable() disable() closeNotifier() <-chan struct{} invalidateUser(string) - Purpose: Token backend abstraction. Allows swapping
simpleTokenProvider(random tokens with in-memory TTL) vsjwtTokenProvider(stateless JWT). All methods are unexported — this is a purely internal extension point. - Implementations:
simpleTokenProvider,jwtTokenProvider - Design quality: Clean ISP example. The swappable token backend is invisible to users of
AuthStore.
UberApplier#
- Package:
go.etcd.io/etcd/server/v3/etcdserver/apply - File:
server/etcdserver/apply/uber_applier.go - Methods:
Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *Result - Purpose: The single entry point for applying a committed Raft log entry to state. The narrow interface belies significant complexity beneath:
uberApplierholds a reference to anapplierV3chain that it swaps dynamically when alarms change state. - Implementations:
uberApplier(production) - Design quality: Excellent. Tiny public surface hides the internal decorator chain (
applierV3).EtcdServeronly seesUberApplier.Apply().
applierV3 (internal)#
- Package:
go.etcd.io/etcd/server/v3/etcdserver/apply - File:
server/etcdserver/apply/interface.go - Methods: ~30 methods (unexported interface):
Apply(r, shouldApplyV3, applyFunc) *Result Put / Range / DeleteRange / Txn / Compaction LeaseGrant / LeaseRevoke / LeaseCheckpoint Alarm Authenticate / AuthEnable / AuthDisable / AuthStatus UserAdd / UserDelete / UserChangePassword / UserGrantRole / UserGet / UserRevokeRole RoleAdd / RoleGrantPermission / RoleGet / RoleRevokePermission / RoleDelete / UserList / RoleList ClusterVersionSet / ClusterMemberAttrSet / DowngradeInfoSet - Purpose: Internal decorator chain interface. Every operation that can be applied from the Raft log has an entry here. Decorators (
authApplierV3,quotaApplierV3,applyV3Capped,applyV3Corrupt) intercept calls before/after delegating. - Implementations:
applierV3Backend(base),authApplierV3,quotaApplierV3,applyV3Capped,applyV3Corrupt - Design quality: Large by design — it matches the Raft operation space. Unexported, so it’s an implementation detail. The decorator pattern it enables is the key strength.
Transporter#
- Package:
go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp - File:
server/etcdserver/api/rafthttp/transport.go - Methods:
Start() error Handler() http.Handler Send(m []raftpb.Message) SendSnapshot(m snap.Message) AddRemote(id types.ID, urls []string) AddPeer(id types.ID, urls []string) RemovePeer(id types.ID) RemoveAllPeers() UpdatePeer(id types.ID, urls []string) ActiveSince(id types.ID) time.Time ActivePeers() int Stop() - Purpose: Peer-to-peer Raft message transport. The interface decouples
raftNode(which callsSend) from HTTP details. TheHandler()method returns an HTTP handler for incoming Raft messages, enabling the same struct to own both directions. - Implementations:
Transportstruct - Design quality: Well-designed. Bidirectional (send + receive handler) with a clean lifecycle (Start/Stop). Peer management methods (
AddPeer,RemovePeer) follow the dynamic cluster membership model.
Raft (rafthttp callback)#
- Package:
go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp - File:
server/etcdserver/api/rafthttp/transport.go - Methods:
Process(ctx context.Context, m raftpb.Message) error IsIDRemoved(id uint64) bool ReportUnreachable(id uint64) ReportSnapshot(id uint64, status raft.SnapshotStatus) - Purpose: Callback interface from the transport back into the server. When the transport receives an incoming Raft message from a peer, it calls
Process.IsIDRemovedvalidates that a sender isn’t a removed member. This is a consumer-defined interface — rafthttp defines what it needs from its host. - Implementations:
EtcdServer(satisfies all four methods) - Design quality: Excellent ISP example. 4 methods, all necessary for the transport’s use cases. Defined by the consumer (
rafthttp) not the provider (EtcdServer). Classic Go interface placement.
Server (etcdserver)#
- Package:
go.etcd.io/etcd/server/v3/etcdserver - File:
server/etcdserver/server.go - Methods:
AddMember / RemoveMember / UpdateMember / PromoteMember ClusterVersion() *semver.Version StorageVersion() *semver.Version Cluster() api.Cluster Alarms() []*pb.AlarmMember LeaderChangedNotify() <-chan struct{} - Purpose: Cluster management contract. Used by the
clusterServergRPC handler and the embedding API.LeaderChangedNotify()returns a channel that closes on leadership change — a neat notification pattern. - Implementations:
EtcdServer - Design quality: Well-focused. Membership operations + version query + alarm query. Not polluted with KV operations (those live in
RaftKV).
RaftKV#
- Package:
go.etcd.io/etcd/server/v3/etcdserver - File:
server/etcdserver/v3_server.go - Methods:
Range(ctx, r *pb.RangeRequest) (*pb.RangeResponse, error) Put(ctx, r *pb.PutRequest) (*pb.PutResponse, error) DeleteRange(ctx, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) Txn(ctx, r *pb.TxnRequest) (*pb.TxnResponse, error) Compact(ctx, r *pb.CompactionRequest) (*pb.CompactionResponse, error) - Purpose: KV operations at the RPC-protocol level (proto request/response objects). Used by
kvServergRPC handler. Each method serializes the operation into a Raft proposal and waits for commitment. - Implementations:
EtcdServer - Design quality: Appropriately sized (5 methods = the KV gRPC service). The proto-typed signatures are a deliberate choice — no translation needed between gRPC layer and server layer.
Lessor (etcdserver, the RPC wrapper)#
- Package:
go.etcd.io/etcd/server/v3/etcdserver - File:
server/etcdserver/v3_server.go - Methods:
LeaseGrant(ctx, *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) LeaseRevoke(ctx, *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) LeaseRenew(ctx, id lease.LeaseID) (int64, error) LeaseTimeToLive(ctx, *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error) LeaseLeases(ctx, *pb.LeaseLeasesRequest) (*pb.LeaseLeasesResponse, error) - Purpose: Lease operations at the RPC level (distinct from
server/lease.Lessorwhich is the internal implementation). Note thatLeaseRenewdoes NOT take proto types — renewal bypasses Raft on the leader for performance. - Implementations:
EtcdServer - Design quality: Clean separation from internal
lease.Lessor. Two interfaces with the same name (Lessor) in different packages serving different layers is occasionally confusing but architecturally correct.
Wait#
- Package:
go.etcd.io/etcd/pkg/v3/wait - File:
pkg/wait/wait.go - Methods:
Register(id uint64) <-chan any Trigger(id uint64, x any) IsRegistered(id uint64) bool - Purpose: ID-keyed channel rendezvous.
EtcdServer.processInternalRaftRequestOncecallsRegister(id)before proposing to Raft, then blocks on the returned channel. When the apply goroutine finishes applying the entry, it callsTrigger(id, result). This bridges the proposal and apply goroutines without knowing each other’s internals. - Implementations:
list(production, sharded),waitWithResponse(test stub) - Design quality: Small and elegant (3 methods). The sharded implementation (64 buckets by
id % 64) avoids lock contention on busy systems. A beautiful example of interface enabling two decoupled goroutines to coordinate.
ConsistentIndexer#
- Package:
go.etcd.io/etcd/server/v3/etcdserver/cindex - File:
server/etcdserver/cindex/cindex.go - Methods:
ConsistentIndex() uint64 ConsistentApplyingIndex() (uint64, uint64) UnsafeConsistentIndex() uint64 SetConsistentIndex(v uint64, term uint64) SetConsistentApplyingIndex(v uint64, term uint64) UnsafeSave(tx backend.UnsafeReadWriter) SetBackend(be Backend) - Purpose: Tracks the highest Raft log index that has been applied to the bbolt storage, persisting it atomically alongside each bbolt commit (via the
txPostLockInsideApplyHook). Ensures exactly-once application of Raft entries after a restart. - Implementations:
consistentIndex,fakeConsistentIndex(tests) - Design quality: Good. The
Unsafe*methods follow the naming convention.SetBackendexists to handle the circular bootstrap order (backend is created before cindex’s first use).
CorruptionChecker#
- Package:
go.etcd.io/etcd/server/v3/etcdserver - File:
server/etcdserver/corrupt.go - Methods:
InitialCheck() error PeriodicCheck() error CompactHashCheck() - Purpose: Detects data corruption by hashing the KV store and comparing across cluster members. Called on startup (
InitialCheck) and periodically. Triggers aCORRUPTalarm if hashes diverge. - Implementations:
corruptionChecker - Design quality: Tiny (3 methods), clearly focused. Well-separated from the checker’s implementation details.
Client KV (client/v3)#
- Package:
go.etcd.io/etcd/client/v3 - File:
client/v3/kv.go - Methods:
Put(ctx, key, val string, opts ...OpOption) (*PutResponse, error) Get(ctx, key string, opts ...OpOption) (*GetResponse, error) Delete(ctx, key string, opts ...OpOption) (*DeleteResponse, error) Compact(ctx, rev int64, opts ...CompactOption) (*CompactResponse, error) Do(ctx, op Op) (OpResponse, error) Txn(ctx) Txn - Purpose: User-facing KV API. Uses
stringkeys and functional options (OpOption) rather than proto types.Do(op)allows deferred execution of any operation. Returns aTxnbuilder for optimistic transactions. - Implementations:
kvstruct (wrapspb.KVClient) - Design quality: Clean user API. The functional-options pattern (
opts ...OpOption) handles the combinatorial request options (prefix, range, rev, sort, etc.) without proliferating method variants.
Client Txn#
- Package:
go.etcd.io/etcd/client/v3 - File:
client/v3/txn.go - Methods:
If(cs ...Cmp) Txn Then(ops ...Op) Txn Else(ops ...Op) Txn Commit() (*TxnResponse, error) - Purpose: Fluent mini-transaction builder. Mirrors the Compare-Then-Else structure of the etcd transaction protocol. Each method returns
Txnfor chaining.Commit()finalizes and sends the request. - Implementations:
txnstruct - Design quality: Excellent ergonomics. The fluent API directly maps to etcd’s transaction semantics. Panics on misuse (calling
IfafterThen) — fail-fast design.
Quota#
- Package:
go.etcd.io/etcd/server/v3/storage - File:
server/storage/quota.go - Methods:
Available(req any) bool Cost(req any) int Remaining() int64 - Purpose: Storage quota enforcement.
quotaApplierV3callsAvailable()before each write; if exceeded, it raises aNOSPACEalarm. Theany-typed parameter allows checking different request types polymorphically. - Implementations:
passthroughQuota(disabled),BackendQuota(checks bbolt size) - Design quality: Small and clean (3 methods). The
anyargument is a deliberate trade-off for generality — the interface predates generics.
Bucket#
- Package:
go.etcd.io/etcd/server/v3/storage/backend - File:
server/storage/backend/batch_tx.go - Methods:
ID() BucketID Name() []byte String() string IsSafeRangeBucket() bool - Purpose: bbolt bucket descriptor. Buckets in etcd’s bbolt schema (e.g.,
key,meta,lease,auth) are defined as static constants satisfying this interface.IsSafeRangeBucket()is a carve-out to distinguish key-value buckets (where range queries are safe) from others. - Implementations:
bucketstruct (inserver/storage/schema/) - Design quality: Lightweight. The
IsSafeRangeBucketmethod is a design smell — it encodes application logic in a storage primitive — but it avoids a bug class (inadvertent duplicate reads from non-KV buckets).
Interface patterns#
Size distribution#
The distribution is sharply bimodal:
| Size | Count | Examples |
|---|---|---|
| 1–3 methods | ~30% | UberApplier (1), Watchable (1), Wait (3), Quota (3), TxnDelete (2), Raft (4), SnapshotServer (1) |
| 4–8 methods | ~30% | ReadTx (3+2), Storage (6), WatchStream (7), KV client (6), Txn (4) |
| 9–16 methods | ~25% | Backend (12), Transporter (12), Lessor internal (16), BatchTx (6+5+2) |
| 17+ methods | ~15% | AuthStore (~30), applierV3 (~30), Authenticator (~17), KV server (~12) |
The large interfaces (AuthStore, applierV3, Authenticator) are not design failures — they mirror the proto service surface and the Raft operation space respectively. They are protocol-shaped interfaces, not domain interfaces.
Embedding#
Embedding is used extensively and well:
WatchableKV=KV+WatchableTxnWrite=TxnRead+WriteViewTxnRead=ReadView+End()BatchTx=UnsafeReadWriter+ locking methodsUnsafeReadWriter=UnsafeReader+UnsafeWriterServerV3=Server+RaftStatusGetterServerV2=Server+Leader()+ClientCertAuthEnabled()ServerPeer=ServerV2+ HTTP handlers
This embedding forms proper hierarchies rather than ad-hoc aggregation. Each embedded interface is independently useful and independently testable.
Consumer-defined interfaces (Go ISP in practice)#
Several interfaces are defined by their consumer, not their provider:
rafthttp.Raft: defined inrafthttppackage, satisfied byEtcdServer. The transport package owns the contract.apply.RaftStatusGetter: defined inapply, satisfied byEtcdServer.lease.TxnDelete: defined inlease, satisfied by mvcc’sTxnWrite.cindex.Backend: a narrowReadTx()slice of the fullbackend.Backend, defined where it’s consumed.
This pattern is consistently applied — packages define the minimal interface they need, then the full implementation elsewhere satisfies it naturally. Zero circular imports result.
Unsafe prefix convention#
The Unsafe* naming convention encodes a locking contract:
UnsafeReader,UnsafeWriter,UnsafeReadWriter— methods that require the caller to hold the lock.ConsistentIndexer.UnsafeConsistentIndex(),UnsafeSave()— same convention.
This is more expressive than comments alone: the name at every call site signals “you must be holding a lock here.” No other project in the 50-project set uses this convention as consistently.
Proto-driven interface symmetry#
A deliberate architectural pattern: the server-side RPC interfaces (RaftKV, Lessor in etcdserver, Authenticator) use proto request/response types directly. This means no translation layer between gRPC handlers and server internals. The gRPC handler receives a proto request, calls the interface method, and returns the proto response. The downside is that the internal server is coupled to the proto types, but etcd considers the proto format the canonical data model.
Implicit satisfaction#
All interfaces are satisfied implicitly (no explicit _ Interface = (*ConcreteType)(nil) compile-time checks in production code). Test files do use var _ Interface = (*FakeType)(nil) assertions. The fakeConsistentIndex and FakeLessor types are explicit test doubles that satisfy their interfaces.
stdlib interfaces used#
io.Writer— inSnapshot.WriteTo(w io.Writer)http.Handler— inTransporter.Handler(),ServerPeer.RaftHandler()error— universally (sentinel errors, wrapped errors, custom types)fmt.Stringer— viaBucket.String()context.Context— in nearly every interface method that touches gRPC or storage
Key abstractions#
The 5 most architecturally significant interfaces:
1. WatchableKV — the storage contract#
WatchableKV is the spine of etcd’s storage architecture. It unifies read-modify-watch in one interface, enabling Watch semantics without a separate pub-sub system. The embedding hierarchy (ReadView → TxnRead → KV → WatchableKV) demonstrates how to build layered interfaces where each layer adds exactly one concern.
2. UberApplier — the command applicator#
One method (Apply) hides the entire decorator chain for applying Raft commands. This interface is what allows the server to swap behavior when alarms fire (NOSPACE, CORRUPT) without touching the EtcdServer code. The contrast between the 1-method public interface and the 30-method applierV3 internal interface is a master class in encapsulation.
3. Wait — the proposal-apply bridge#
Wait.Register/Trigger is how EtcdServer connects the propose goroutine to the apply goroutine without shared state or direct coupling. This 3-method interface enables the entire linearizable write path. It’s the most minimal interface with the highest architectural leverage in the codebase.
4. Backend — the durable storage contract#
backend.Backend wraps bbolt with batching, concurrent reads, and snapshot capability. Every subsystem (mvcc, lease, auth, cindex) that needs persistence goes through this interface. The two-transaction-mode design (ReadTx vs ConcurrentReadTx) exposes a deliberate performance trade-off via the interface.
5. Transporter + Raft pair — the network boundary#
The Transporter/Raft interface pair forms the network boundary of the consensus engine. Together they define a clean membrane: the server calls Transporter.Send() to emit, and the transport calls Raft.Process() to deliver. Neither side knows the other’s implementation. This pattern enables mocking the entire network in integration tests with zero real socket operations.
Interface-driven extensibility#
Swappable token backends#
auth.TokenProvider (unexported) allows swapping between stateful (simple) and stateless (jwt) token implementations. This is the only auth extension point, and it’s intentionally not exported — etcd does not intend for users to provide custom token backends.
Swappable apply decorators#
applierV3 (unexported) is the decorator chain interface. Adding a new cross-cutting concern (e.g., rate limiting, audit logging) would mean adding a new decorator implementing all 30 methods and wrapping the existing chain. The pattern is clear but the cost is high (30 methods to delegate). A more focused interceptor interface would be more extensible.
Hooks for backend callbacks#
backend.Hooks (implemented by hooks.go) provides lifecycle callbacks on bbolt operations. The ConsistentIndexer uses the txPostLockInsideApplyHook hook on Backend for atomic index persistence. This is a narrow extension point for persistent side effects that must be co-atomic with bbolt commits.
Quota for pluggable limits#
The Quota interface is the most explicit extension point: passthroughQuota (disabled) and BackendQuota (enabled) swap in/out based on config. The interface is simple enough that a custom quota implementation would be trivial — though in practice this is not a public extension point.
No plugin system#
etcd has no dynamic plugin system. Extension is via embedding (embed.StartEtcd(cfg)) or wrapping (grpc proxy). The gRPC proxy is implemented entirely via the standard client/v3 library, demonstrating that the client interfaces (KV, Watcher, Lease) are rich enough to implement a full pass-through proxy without accessing internal server interfaces.