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 including Read, 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 Watchable extension; everything else only needs KV. This is the type stored in EtcdServer.kv.
  • Implementations: watchableStore (internal, wraps store)
  • Design quality: Excellent layering. The embedding chain (ReadViewTxnRead, WriteViewTxnWrite, 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 []byte keys, returns revision integers, exposes explicit transaction objects.
  • Implementations: store struct (in server/storage/mvcc/kvstore.go)
  • Design quality: Well-segregated. ReadView and WriteView let narrow consumers avoid the full interface. The TxnRead/TxnWrite transaction 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, in watchable_store.go)
  • Design quality: Clean. The channel-return pattern (Chan() <-chan WatchResponse) is idiomatic Go; callers select on 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 ReadTx for correctness, ConcurrentReadTx for performance), batched writes, snapshot capability for Raft log truncation. The hook (SetTxPostLockInsideApplyHook) enables ConsistentIndexer to atomically persist the applied Raft index alongside each bbolt commit.
  • Implementations: backend struct (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 + UnsafeWriter
    UnsafeWriter: UnsafeCreateBucket, UnsafeDeleteBucket, UnsafePut, UnsafeSeqPut, UnsafeDelete UnsafeReader: 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 a txPostLockInsideApplyHook for 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. UnsafeReader methods require the caller to hold the read lock. This is the same Unsafe* 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. raftNode calls Save before advancing committed entries to guarantee durability ordering (WAL before bbolt).
  • Implementations: storage struct (production; wraps wal.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 / SetCheckpointer are setter methods injected post-construction to break circular dependencies (lessor depends on mvcc, mvcc depends on lessor).
  • Implementations: lessor struct (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) and AuthChecker (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) vs jwtTokenProvider (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: uberApplier holds a reference to an applierV3 chain that it swaps dynamically when alarms change state.
  • Implementations: uberApplier (production)
  • Design quality: Excellent. Tiny public surface hides the internal decorator chain (applierV3). EtcdServer only sees UberApplier.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 calls Send) from HTTP details. The Handler() method returns an HTTP handler for incoming Raft messages, enabling the same struct to own both directions.
  • Implementations: Transport struct
  • 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. IsIDRemoved validates 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 clusterServer gRPC 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 kvServer gRPC 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.Lessor which is the internal implementation). Note that LeaseRenew does 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.processInternalRaftRequestOnce calls Register(id) before proposing to Raft, then blocks on the returned channel. When the apply goroutine finishes applying the entry, it calls Trigger(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. SetBackend exists 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 a CORRUPT alarm 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 string keys and functional options (OpOption) rather than proto types. Do(op) allows deferred execution of any operation. Returns a Txn builder for optimistic transactions.
  • Implementations: kv struct (wraps pb.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 Txn for chaining. Commit() finalizes and sends the request.
  • Implementations: txn struct
  • Design quality: Excellent ergonomics. The fluent API directly maps to etcd’s transaction semantics. Panics on misuse (calling If after Then) — 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. quotaApplierV3 calls Available() before each write; if exceeded, it raises a NOSPACE alarm. The any-typed parameter allows checking different request types polymorphically.
  • Implementations: passthroughQuota (disabled), BackendQuota (checks bbolt size)
  • Design quality: Small and clean (3 methods). The any argument 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: bucket struct (in server/storage/schema/)
  • Design quality: Lightweight. The IsSafeRangeBucket method 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:

SizeCountExamples
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 + Watchable
  • TxnWrite = TxnRead + WriteView
  • TxnRead = ReadView + End()
  • BatchTx = UnsafeReadWriter + locking methods
  • UnsafeReadWriter = UnsafeReader + UnsafeWriter
  • ServerV3 = Server + RaftStatusGetter
  • ServerV2 = 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 in rafthttp package, satisfied by EtcdServer. The transport package owns the contract.
  • apply.RaftStatusGetter: defined in apply, satisfied by EtcdServer.
  • lease.TxnDelete: defined in lease, satisfied by mvcc’s TxnWrite.
  • cindex.Backend: a narrow ReadTx() slice of the full backend.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 — in Snapshot.WriteTo(w io.Writer)
  • http.Handler — in Transporter.Handler(), ServerPeer.RaftHandler()
  • error — universally (sentinel errors, wrapped errors, custom types)
  • fmt.Stringer — via Bucket.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 (ReadViewTxnReadKVWatchableKV) 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.