Skip to main content
  1. etcd Internals/

Chapter 9: Multi-Module Go Repository #

Welcome back! In the previous chapter, Feature Gates and Release Stages, we saw how etcd introduces new features carefully, hiding them behind gates until they are ready. But where does the code for those features actually live? If you open the etcd repository, you won’t find one giant Go module. Instead, you’ll find a monorepo with several Go modules — and each one has a special job.

Imagine a shopping mall. Each store is independent, with its own entrance, inventory, and staff. But all the stores share the same building, the same parking lot, and the same opening hours. If one store changes its products, neighboring stores might need to update what they sell.

That’s how etcd is organized: many modules living in one repository, released together, but each with its own go.mod file and its own responsibility.

By the end of this chapter, you’ll know why etcd is structured this way, what each module does, and how to decide where your next contribution belongs.


Why This Abstraction Matters #

Imagine you are a new contributor. You need to add a small utility function that converts a number into bytes, and you know both the etcd server and the etcdutl command-line tool need it.

Where do you put it?

  • If you put it inside server/v3, then etcdutl/v3 would have to import the entire server package, including Raft, storage, and networking code. That would be very heavy.
  • If you put it inside client/v3, you would make the official Go client heavier for every developer who uses it.
  • If you create a brand-new shared module, you’d need to manage its version separately, which makes releases harder.

The answer in etcd is usually pkg/v3, a dedicated module for small, shared utilities. But to know that answer, you first need to understand the module layout.


Our Central Use Case #

Let’s keep that example in mind for the whole chapter:

You need to add NumberToBytes, a helper that turns a uint64 into a byte slice. Both server/v3 and etcdutl/v3 want to use it.

With the multi-module layout, you’ll make a small change in pkg/v3, then update the modules that depend on it. At the end, all modules will use the same version of pkg/v3, and everything will still work together.


Key Concepts #

1. What Is a Go Module? #

A Go module is a collection of Go packages with a go.mod file. That file defines the module’s import path, its Go version, and its dependencies. For example, the client/v3 module has a go.mod that looks something like this:

module go.etcd.io/etcd/client/v3

go 1.21

require go.etcd.io/etcd/api/v3 v3.5.10

Each module is an independent unit. It can be versioned, tagged, and imported separately.

2. The Modules in etcd #

The etcd repository contains several modules. Here are the main ones you’ll encounter:

ModuleWhat it contains
go.etcd.io/etcd/api/v3API definitions: protobuf messages and gRPC service definitions.
go.etcd.io/etcd/client/v3The official Go client library.
go.etcd.io/etcd/server/v3The etcd server implementation.
go.etcd.io/etcd/etcdctl/v3The etcdctl command-line tool.
go.etcd.io/etcd/etcdutl/v3The etcdutl offline maintenance tool.
go.etcd.io/etcd/pkg/v3Reusable utility packages.
go.etcd.io/etcd/tests/v3Integration tests and test helpers.
go.etcd.io/raft/v3The Raft consensus implementation, hosted in a separate repository.
go.etcd.io/bboltThe persistent B-tree storage engine used by etcd’s backend, also hosted separately.

Notice how raft/v3 and bbolt are externalized: they live in their own repositories, but etcd depends on them. The rest of the modules live in the etcd repository itself.

3. How the Modules Depend on Each Other #

Modules are not all equal. Some are low-level building blocks, and some are higher-level applications. Here is a simplified dependency graph:

flowchart LR A[api/v3] --> C[client/v3] P[pkg/v3] --> C A --> S[server/v3] P --> S C --> S S --> E[etcdctl/v3] S --> U[etcdutl/v3] R[raft/v3] --> S B[bbolt] --> S

If module A depends on module B, then B must change before A can use the new version. That’s the dependency bumping order.

4. All Modules Are Released Together #

Even though each module is independent, etcd versions and releases them together. For example, go.etcd.io/etcd/client/[email protected] depends on go.etcd.io/etcd/api/[email protected].

This is like the shopping mall example: all stores share the same opening hours and the same grand opening date. You never have one module at version v3.5.10 and another at v3.8.2 in the same release.

5. pkg/v3 Has a Special Rule #

The pkg/v3 module is full of small, reusable utilities. But there is an important rule: a package belongs there only if it could someday be moved out of etcd into its own repository.

Why? Because pkg/v3 automatically becomes a dependency of the client library. If you add a package with lots of heavy dependencies, every client of etcd will also need those dependencies. That makes the client heavier than it should be.

So before putting something in pkg/v3, ask yourself:

  • Is this a generic utility, not tied to etcd concepts?
  • Does it have very few dependencies?
  • Could another project use this code?

If all answers are yes, pkg/v3 is probably the right home.


Solving the Use Case Step by Step #

Let’s add NumberToBytes the right way.

Step 1: Create the function in pkg/v3 #

We’ll create a tiny package called numutil under pkg/.

// pkg/numutil/bytes.go (very simplified)
package numutil

import "fmt"

func NumberToBytes(n uint64) []byte {
    return []byte(fmt.Sprintf("%d", n))
}

This is a small, generic function. It has no etcd-specific code, so pkg/v3 is a good fit.

Step 2: Use it from the server module #

Inside server/v3, we can import the new package:

import (
    "go.etcd.io/etcd/pkg/v3/numutil"
)

data := numutil.NumberToBytes(10)

Because server/v3 already depends on pkg/v3, this works immediately. No dependency update is needed for the server side.

Step 3: Use it from etcdutl/v3 #

Now let’s use it in etcdutl/v3 too. etcdutl/v3 also depends on pkg/v3, so you can import the same package there.

If etcdutl/v3 did not yet depend on pkg/v3, you would add the dependency using go get inside the etcdutl directory.

Step 4: Run the checks #

After making your change, you should run the verification commands:

make verify
make test-unit

make verify checks formatting, imports, go.mod consistency, and other rules. make test-unit runs unit tests.

If everything is green, your change is ready for review.

What If You Picked the Wrong Module? #

Suppose you put NumberToBytes in server/v3 and then etcdutl/v3 wanted to use it. That would force etcdutl/v3 to import all of server/v3, which brings in Raft, storage, and networking code. That’s a sign you chose the wrong module.

That’s why the first step is always: locate your change in the correct module before editing. Think about which modules need the code, and which module is the lowest-level one that can safely contain it.


Under the Hood: How Releases Work #

Now let’s look at what happens when etcd maintainers release a new version.

Code-Light Walkthrough #

The release process follows the dependency graph from bottom to top. Lower-level modules are updated first, then higher-level modules that depend on them.

Here is a simplified sequence for releasing version v3.5.10:

sequenceDiagram participant Script participant A as api/v3 participant P as pkg/v3 participant C as client/v3 participant S as server/v3 Script->>A: update version to v3.5.10 Script->>P: update version to v3.5.10 Script->>C: use api/v3 and pkg/v3 v3.5.10 Script->>S: use client/v3 v3.5.10 Note over Script,S: Tags are pushed together

First, api/v3 and pkg/v3 are updated. Then client/v3 and server/v3 are updated to reference those new versions. Finally, the command-line tools are updated. All release tags are pushed at the same time.

A Closer Look at the Scripts #

The etcd repository contains documentation and scripts that automate this process.

For example, to update a dependency like github.com/spf13/cobra to a new version, you would run:

cd ${ETCD_ROOT_DIR}/etcdctl
go get github.com/spf13/[email protected]
go mod tidy
cd ..
make fix

This updates the module’s go.mod file and then runs make fix, which resets the generated files and workspace files.

There is also a helper script that updates a dependency across all modules:

./scripts/update_dep.sh github.com/spf13/cobra v1.7.0

This script follows the correct bumping order automatically.

Why Order Matters #

The dependency bumping order is important. If server/v3 depends on pkg/v3, then pkg/v3 must be released first. Otherwise, server/v3 might try to use a new version of pkg/v3 that doesn’t exist yet.

This is like the shopping mall analogy: the stores on the lower level need to restock their inventory before the upper-level stores can update their shelves.


How New Contributors Should Think #

Before you write any code, ask yourself a few questions:

  1. Is this an API protocol definition?
    Put it in api/v3. That’s where protobuf messages live, as you saw in gRPC Key-Value API and clientv3.

  2. Is this network client code?
    Put it in client/v3. This is the official Go client used by applications.

  3. Is this etcd server internal logic?
    Put it in server/v3. This is the heart of etcd, including Raft integration, storage, and membership.

  4. Is this a command-line tool?
    Put it in etcdctl/v3 for live-cluster administration, or etcdutl/v3 for offline data operations. See etcdctl / etcdutl Command-Line Tools.

  5. Is this a shared, generic utility?
    Put it in pkg/v3, but keep it lightweight.

  6. Is this an integration test?
    Put it in tests/v3 or in the same module as the code it tests.

If you’re still unsure, ask in the issue or pull request. Maintainers will help you find the right home.


Conclusion #

In this chapter, you learned:

  • etcd is a monorepo containing several Go modules.
  • Each module has a specific role, from low-level APIs to command-line tools.
  • pkg/v3 is for generic utilities, but it must stay lightweight because it becomes part of the client dependency tree.
  • Modules are versioned and released together, with lower-level modules updated first.
  • The dependency bumping order is defined in dependency_management.md.
  • New contributors should locate their change in the correct module before editing.

Now that you know where etcd’s code lives, are you ready to see how the project proves that code really works? The next chapter is all about deliberately crashing clusters to find hidden bugs.

Continue to Robustness Testing Framework.


Generated by AI Codebase Knowledge Builder