How do you decide whether an internal Go package should become a published module other teams depend on?
answer
- the tag is not the decision
- a second real consumer, not a hypothetical
- the import path is forever
- who does the migration work
- copying is often cheaper than depending
basics
~20 sPublishing is a support commitment, not a packaging step. Decide on who else genuinely needs it, whether you will honour a stable import path for years, and whether keeping it internal or copying the code is cheaper.
solid answer
~50 sThe technical act is a tag; the decision is about who carries the compatibility burden afterwards. I ask three things. Is there a second real consumer today, not a hypothetical one? A package with one user should stay internal. Can we promise a stable import path forever, since the module path is baked into every consumer's imports and a rename is effectively a new module. And who pays when the API needs to change — every breaking change becomes a coordinated migration we own, not a refactor we do on Friday. If the answers are yes, I publish deliberately: a path chosen for the long term, v0 while the API is still moving, and a rule that published tags are never moved. If not, the honest alternatives are keeping the package under `internal/` or letting the other team copy a few hundred lines - a copy is cheap, a module you must support for three years is not.
go deeper
Understand that publishing a module is a commitment to other teams, not just a tag. If you are asked, the safe instinct is to keep code internal until someone outside genuinely needs it.
Be able to argue the concrete costs: the import path becomes permanent, exported names become a contract, and every fix now needs a release before a consumer can use it. Know that internal/ keeps a package unimportable from outside.
Show that you would shape the release process — smallest possible exported surface, v0 while the API moves, cheap patch releases, tags that are never overwritten — and that you can name the failure each control prevents.
Own the call and the way it can be overruled. Weigh a second real consumer, the permanence of the path, who staffs migrations, and the honestly costed alternative of copying. Expect consuming teams to object on cadence, stability or ownership, and treat that as legitimate input.
## The tag is easy; the promise is the decision Publishing a Go module takes about thirty seconds. What you are actually deciding is whether your team will absorb the cost of other teams depending on your code, for as long as they do. That decision is yours to make and theirs to overrule, because they inherit the consequences. ### The questions that decide it **Is there a second consumer, right now?** "Someone might want this" is the most common reason a module is published and the worst one. A package with one user should stay where its user is; the abstraction has not been tested against a second set of requirements, and every early published API is a guess. Wait for the second consumer, then publish the shape both of them actually need. **Can we commit to the import path?** The module path is the module's identity and appears in every consumer's source. Move the repository, rename the organisation, reorganise into a monorepo, and you have created a different module and a migration for everyone. Choosing between a host path and a vanity domain is a decision to make before the first tag, not after — and a vanity domain is itself an operational commitment, because it must keep resolving for as long as anyone builds against the module. **Who pays for the next breaking change?** This is the question teams skip. Once published, an exported name is a contract. Every rename, every added parameter, every changed error value is somebody else's work. The default answer has to be that the publishing team pays: you do the additive change, you write the migration, you keep the old path working. If your team cannot staff that, do not publish. **What is the alternative, honestly costed?** Three real options usually exist. Keep it under `internal/` so it is unimportable outside your module and stays free to change. Let the other team copy the code — a few hundred lines duplicated is often cheaper over three years than a dependency with a release cycle. Or publish. Copying is under-rated by engineers and over-punished in review, and it is frequently the right answer for small, stable, rarely changing code. ### If you do publish, publish deliberately **Pick the surface first, then the path.** Export the smallest thing that satisfies the second consumer. Everything you export is permanent; everything unexported is free. It is far easier to add a function later than to remove one. **Use v0 while you mean v0.** A v0 line signals that the API may change, and it is honest during the period when it will. What is dishonest is sitting on v0 for years while a dozen teams treat it as stable — at that point the promise exists in practice and the version number is just avoiding the conversation. Cutting v1 is the moment you accept the compatibility burden explicitly, and it should be a decision with a date, not a drift. **Tag discipline is the operational half.** Releases come from reviewed commits. Patch versions carry fixes and are cheap — use them freely rather than accumulating changes. Published tags are never moved or deleted, and the release automation should be incapable of overwriting one. A changelog that says what changed per version is what lets consumers upgrade without reading diffs. **Decide who cuts releases.** If everyone can push a tag, someone eventually publishes from a branch, or force-pushes a fix over a released version, and consumers pay for it. A named owner or an automated release job is the control. ### The overrule you should expect Consuming teams get a vote, because they carry the risk of depending on you. Reasonable objections: your release cadence is too slow to fix their blocking bug; your API is still moving weekly; nobody on your team is on call for it; the module duplicates something they already depend on. Any of those is a legitimate reason to say the code should stay internal and be copied instead. The failure mode to avoid is publishing to look like a platform team and then treating consumers' upgrade pain as their problem. ### The lightweight middle ground Sometimes the right call is to publish while explicitly limiting the promise: a v0 line, a stated set of supported consumers, a written note that the API will change and that the publishing team will do the migrations. That is a real, defensible position — provided the note is honoured, and provided somebody revisits it rather than letting v0.13.7 quietly become the company's most depended-on library.
- What does staying on v0 actually buy you, and what does it cost?It buys honesty while the API is still moving: consumers are told breaking changes are allowed and can plan for them. It costs credibility once the module is genuinely depended on — cautious teams pin hard and avoid upgrading, and a long-lived v0 becomes a way of avoiding the compatibility conversation rather than having it.
- A consuming team needs a breaking change to your published API. How do you decide?The cost lands on every other consumer, so the burden of proof is on the change. I look first for an additive path — a new function, a new option type, a second entry point. If it genuinely must break, that is a major-version decision plus a migration, and I settle who does that work before agreeing, not after.
- How do you keep the option of not publishing open?Keep the package under internal/ until a second team has a concrete need, which makes it unimportable from outside and free to change. When that need arrives, compare publishing against letting them copy the code. Copying a few hundred stable lines is often cheaper for both teams than a dependency with a release cycle between every fix.
- What operational controls would you put around releases once you publish?A named owner or an automated job that cuts tags from reviewed commits, release automation that refuses to overwrite an existing tag, a changelog entry per version, and freely used patch releases so fixes reach consumers quickly. Most publishing incidents trace back to someone having the ability to force-push a tag.
saying these in an interview costs you the question
- Publishes because the code might be useful someday
- Treats the module path as easy to change later
- Expects consumers to absorb breaking-change migrations
- Never considers copying the code as an option
- Sits on v0 indefinitely while consumers rely on stability
- Lets anyone on the team push a release tag