As a Go library owner, how do you decide between cutting a /v2 module path and keeping a change inside v1?
answer
- the cost lands on other people
- no routine update crosses a major
- ask whether it can be additive
- a silent behaviour change argues loudest
- state the v1 support window first
basics
~20 sWeigh what a /v2 costs everyone else. Because consumers must rewrite import paths by hand and no upgrade command crosses a major, a v2 reaches nobody automatically and you maintain two trees. Cut one only when the break cannot be made additive.
solid answer
~50 sIn Go the cost of a major is unusually visible: `/v2` is a new module path, so every consumer edits import lines, `go get -u` never carries them across, and you support two trees for as long as v1 has users. That pushes the default toward additive change — a new function, a new option type, a new package beside the old one, with the old surface deprecated but working. I cut a v2 only when the break cannot be expressed additively: a method added to an interface consumers implement, or a silent behaviour change that still compiles, where a new path is the only way to force a deliberate decision. Then it turns organisational — who pays the migration, how long v1 keeps security fixes, whether the biggest consumer can absorb it this quarter. If not, ship it additively and expect to be overruled.
go deeper
Understand the basic asymmetry: releasing v2 does not move anyone, because the import path changes and consumers have to edit their code. That alone explains why Go libraries prefer adding new functions.
Be able to list the additive alternatives — a new function, an option type, a new package, deprecation comments — and say what kind of break cannot be expressed that way, such as adding a method to an interface consumers implement.
Show you have run one: the migration guide, the first consumer migrated by you, the seam where v1 and v2 values meet, and the decision about whether the library's package-level state makes gradual migration unsafe.
Own the whole call — who pays the migration, the stated support window for each kind of fix, whether your types propagate the break through consumers' own APIs, and what you do when the biggest consumer says not this quarter.
## What you are actually deciding The technical question — is this change breaking? — is usually easy. The decision that belongs to a library owner is whether the break is worth a new module path, given that in Go a major version costs *other people* work that they did not ask for and cannot automate away. ## The specific costs of a /v2 in Go These are worth naming precisely, because they are what makes the Go answer different from bumping a major in most other ecosystems. **Every consumer edits source.** `example.com/lib/v2` is a different import path, so adopting v2 is a mechanical rewrite of import lines in every file that touches the library, in every repository that depends on you, plus a `go.mod` change. **No routine update ever crosses.** `go get -u` upgrades within a module path. A team that never deliberately adopts v2 will sit on the newest v1.x forever and will not be nagged. That is a feature for stability and a problem for adoption: your v2 has zero users by default. **You maintain two trees.** For the length of the v1 support window, every security fix and every serious bug fix lands twice, in two layouts, with two release processes and two sets of CI. That cost is yours, is ongoing, and is routinely underestimated at the point of deciding. **Coexistence is real but sharp.** A program may require both majors, which is what makes gradual migration possible. But their types are unrelated, and each major carries its own package-level state, so registries, defaults and sentinel errors exist twice. If your library holds process-wide state, gradual migration is much more dangerous than it looks. ## The test that decides it Ask whether the change can be made **additive**. Most breaks can: - New behaviour behind a new function or a new option type, leaving the old signature intact. - A new package inside the same module when the shape of an API changes substantially. - A widened return type or an extra field, where the old call sites still compile. - Deprecation comments on the old surface, so tooling steers new code without breaking old code. Recent Go toolchains make this cheaper: since Go 1.26 `go fix` is the home of the modernizers and understands `//go:fix inline`, which can rewrite call sites of a deprecated wrapper — useful *within* a major, and no help at all across a path change. A v2 is genuinely warranted when additive is impossible or dishonest: - **The break is in an interface consumers implement.** Adding a method to an exported interface breaks every implementation, and there is no additive form. - **The change is silent.** The same call now returns different results, or the same configuration means something else. This is the strongest argument of all for a path change, because a silent behaviour change gives consumers no compiler help at all — the new path is the only mechanism that forces a deliberate decision. - **The API's shape is wrong** and each additive patch has been making it worse. Accumulated deprecations have a cost of their own, and a clean break can be the honest move. ## The organisational half Once you believe a v2 is technically justified, the decision moves to constraints you do not fully control, and this is where a library owner gets overruled. **Who pays.** Count the consumers and who owns them. If your biggest internal consumer is mid-migration on something else, the calendar decides, not you. Landing the change means landing it in their quarter, or not at all. **The support window.** State it before you release, not after: how long v1 receives security fixes, and how long it receives bug fixes, which are different answers. Without a stated window, v1 becomes permanent by default and you have quietly signed up for two trees forever. **Whether values must cross.** If your types appear in *your consumers'* exported APIs, then their consumers face the same seam, and the migration is not one team's work but a wave through a dependency graph. That single question often flips the decision on its own. **What you give them.** A migration guide with a mechanical before-and-after, a rewrite where it can be automated, and a first migration you perform yourself in the largest consumer. A v2 that ships without those is a v2 that does not get adopted. **Whether v1 can become a shim.** Reimplementing v1 as a thin wrapper over v2 is often the highest-leverage move available: it collapses two implementations into one, makes coexistence safe by giving a mixed build one set of package-level state, and means bug fixes land once. It costs you a release and it is usually worth it. ## What being overruled looks like The platform team says no new majors this quarter because a toolchain migration is already in flight. The largest consumer says the rewrite is three weeks it does not have. Both are legitimate, and the right response is not to ship v2 anyway to an empty audience — it is to land the capability additively behind a new function now and hold the cleanup for a window when someone can pay for it. A major version nobody adopts is worse than no major version, because you carry the maintenance without getting the benefit.
- Which kind of break argues most strongly for a new module path rather than an additive change?A silent behaviour change — code that still compiles but now does something different. Every other break gives consumers a compiler error to act on, so it can often be staged additively with deprecations. A silent change offers no such signal, and a new import path is the only mechanism that forces each consumer to make a deliberate decision.
- How does the support window for v1 change the decision?It sets the size of the bill you are signing. Every fix lands in two trees for as long as v1 is supported, so the window has to be stated up front — separately for security fixes and bug fixes — and enforced. Without a stated end, v1 becomes permanent by default and the two-tree cost never expires.
- Why is reimplementing v1 as a wrapper over v2 often the best move?It collapses two implementations into one, so bug fixes land once and any program that requires both majors links a single set of package-level variables. That removes the worst coexistence failure — duplicated registries and defaults — and it costs you one release rather than costing every consumer a rushed migration.
- What would make you refuse a v2 even when the change is genuinely breaking?When your types appear in consumers' own exported APIs, so the migration propagates through a whole dependency graph rather than stopping at one team, or when the teams who would pay cannot absorb it in any near quarter. In both cases the capability ships additively now and the cleanup waits for a window someone will fund.
saying these in an interview costs you the question
- Cuts a major for any breaking change on principle
- Assumes consumers get v2 through a routine upgrade
- Ignores the cost of maintaining two trees
- States no support window for the old major
- Treats the decision as the library owner's alone