skip to content

You maintain a Go client SDK dozens of teams pin, and a design flaw needs a breaking change. How do you decide what ships?

level: principalimportance: nice to knowfreq 28%

answer

  1. count the importers, not the elegance
  2. wrong answers versus ugly answers
  3. additive first, parallel second, major last
  4. who pays for the migration
  5. a deprecation with no date is forever

basics

~20 s

Weigh what the flaw costs callers against what a break costs them. Silent wrong results justify a break; ugliness does not. Prefer an additive path plus a deprecation notice naming the replacement and its removal release.

solid answer

~50 s

I rank the options by cost to importers, not by cleanliness. First choice is additive: a new function, a new interface for the new behaviour, a new option, with the old path reimplemented on top of it and marked `// Deprecated:` naming the replacement and the release it disappears in. Second is a parallel surface in the same major version — a new package or type living beside the old one — which doubles the documentation but breaks no build. A major version is last, because it is a maintenance fork: two branches, two sets of security fixes, and every importer paying migration time I cannot pay for them. What tips me toward breaking is whether the flaw makes callers silently wrong — data loss, a dropped error, an auth bug — rather than merely awkward. And whatever I choose, I state the deprecation window with a date and an owner, pre-announce it, and offer a migration I have automated as far as I can.

go deeper

for a junior

Recall the safe moves first: adding a function, a type or an option is additive, while renaming or removing an exported symbol breaks every importer that upgrades.

for a middle

Be ready to explain the alternatives to breaking — a second interface, a variadic option, a delegating twin marked deprecated — and what each costs in extra surface.

for a senior

Show that you would carry both paths for a stated window, reimplement the old on the new so behaviour cannot drift, and measure remaining usage before deleting anything.

for a principal

Own the call and its funding: who pays for dozens of migrations, what evidence justifies a break, how long the old line is supported, and who is accountable for the removal date.

## Frame the decision as somebody else's budget The question is never "what is the right API". It is "who pays, and for what benefit". Dozens of internal teams pinning your SDK means a break costs dozens of unplanned migrations, each landing in a sprint somebody else already committed. Your improved design is a benefit to you and to future readers; the migration is a cost to people who did not ask for it. Any argument that skips that asymmetry is not a maintainer's argument. So the first thing to establish is the *size* of both sides: how many importers, how deep the usage (one call site or a type implementing your interface everywhere), and how bad the flaw actually is for them today. ## The ladder, cheapest first **1. Additive, in place.** Most "we must break this" turns out not to. A new method goes on a *new* interface, detected by type assertion. A new parameter becomes a variadic option, so the constructor signature never changes again. A misnamed function gets a correctly named twin, with the old one delegating to it. The old symbol stays, marked `// Deprecated:` with the replacement named and the removal release stated. Nothing downstream stops compiling; the surface grows. **2. Parallel surface in the same major version.** When the shape is wrong rather than the name — the whole client type is unsalvageable — a second package or a second type beside the old one lets both compile at once. Consumers migrate on their own schedule; you carry two implementations for a stated window, ideally with the old one built on the new one so behaviour cannot diverge. The cost is real: two sets of docs, two sets of examples, and a support surface where people ask which to use. **3. A new major version.** This is a maintenance fork. You will backport fixes to the old line for as long as anyone is on it, and you will discover that "anyone" includes a service nobody staffs. Take it when the flaw is pervasive and the additive path would leave the API permanently confusing — and only when the organisation has actually agreed to fund the migration and the double maintenance, not when you have merely convinced yourself the new design is better. ## What justifies a break A short list, and I hold it strictly: - **Silent incorrectness.** Callers get wrong answers, lose data, or skip an authorization check without any error being returned. This is the strongest case, because the status quo already costs them more than migrating will. - **Unfixable safety.** The type cannot be made safe for concurrent use, or the API forces a leak that no additive change can plug. - **A contract that blocks everything.** A shape so wrong that every future feature has to be bolted on sideways; the accumulating cost of the workaround exceeds the one-time migration. And the list of things that do not justify it: naming, symmetry, a nicer parameter order, wanting a cleaner surface for new readers, and the fact that the current design embarrasses you. Those go in the deprecation notice and wait for the next major version, or wait forever. ## The parts people forget **Nobody is dragged onto a new version — until they are.** Publishing a tag does not upgrade anyone directly, since version selection resolves to what modules actually require. But a shared dependency that bumps its requirement on you moves everyone behind it, so "they can just pin" is only true until a third party makes it false. Plan for the transitive path, and retract a tag that shipped a break you did not intend. **Deprecation without a deadline is permanent.** Every deprecated symbol you ship without a removal release and a named owner is surface you maintain forever. State the release in the notice, publish a table of what goes when, and check the usage before the date rather than the week of it. **Automate what you can.** If you can ship a mechanical rewrite for the common call sites, the migration becomes an afternoon rather than a quarter, and the political argument disappears with the cost. Even a precise migration guide with before/after snippets per pattern changes the answer you get from teams. **Announce before you tag, not with the tag.** Give the affected teams the deprecation, the reason, the window and the migration path in advance, and ask the two largest importers to try it. They will find the case you did not model, and their agreement is what makes the deadline real. ## How I would answer in one line Break when leaving it alone keeps handing callers wrong results; otherwise grow the surface additively, mark the old path deprecated with a named replacement and a removal release, and spend the major version only when the organisation has agreed to pay for it.

  • A team refuses to migrate before your stated removal release. What do you do?
    Find out why: usually the cost is real, or the migration path misses their case. If it is capacity, the deadline moves once, publicly, with a new date — a deadline nobody enforces teaches everyone to ignore the next one. If it is a gap in the replacement, that is my bug and the window pauses until it is fixed.
  • How do you decide when a deprecated symbol can actually be deleted?
    By evidence, not by the calendar alone: the stated release has arrived, the surface diff shows the replacement covers every case, and a search across the organisation's code shows no remaining importers, or only ones that have agreed. Then the deletion ships in a release where breaks are allowed, announced separately from the tag that contains it.
  • What do you owe importers when you do ship a breaking change?
    Advance notice with a date, a written reason, a migration guide with before-and-after code for each affected pattern, automation where the rewrite is mechanical, a maintained older line for a stated window with security fixes, and a named person who answers questions during the migration. Without those, the break is offloading your work onto other teams.

saying these in an interview costs you the question

  • Breaks the API because the current design is inelegant
  • Ships a major version with no plan to maintain the old line
  • Deprecates with no removal release and no owner
  • Assumes importers can simply pin and stay safe forever
  • Announces the break in the release notes of the breaking tag