An exported 12-method interface in your Go proxy package is used by every downstream service. How do you shrink it?
answer
- count what each consumer actually calls
- the fan-in is the real problem
- a wide interface cannot grow or shrink
- re-declare it as an embedding of the small ones
- delete the name only in a major version
basics
~20 sMeasure which methods each consumer actually calls, then add small one- and two-method interfaces beside the big one and move parameters onto them. Redeclare the wide interface as an embedding of those, so existing code still compiles, then delete it.
solid answer
~50 sFirst measure rather than guess: `go list -deps ./...` and a grep for the interface name show who depends on the package, and reading each call site shows which of the twelve methods that consumer really uses -- usually one or two. Then add narrow interfaces, `HeaderRewriter` and the like, and change every function signature to take the narrow one. Keep the wide interface alive during the migration by redeclaring it as an interface that embeds the small ones; that is a source-compatible change, so implementers and callers keep compiling. Then move consumers over release by release and delete the wide name when the last reference goes. The lasting fix is to stop exporting a mirror of your struct at all: export the concrete type, and let each function ask for the capability it calls.
code
go · 15 linestype HeaderRewriter interface {
SetRequestHeader(key, value string)
StripRequestHeader(key string)
}
type Forwarder interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
// Same method set as before, so nothing downstream breaks yet.
type Proxy interface {
HeaderRewriter
Forwarder
// plus the remaining methods, grouped the same way
}go deeper
Know the symptom: a wide interface forces anyone writing a stand-in to implement every method, even the ones their code never calls.
Be able to explain that embedding the small interfaces back into the wide name preserves its method set exactly, so the intermediate step compiles for all existing implementers.
Show the whole sequence under production constraints: measure real call sites, add narrow interfaces, keep the old name compiling, migrate consumers, and remove it only when nothing references it.
Own the release consequence. Deciding when the removal lands, which major version carries it, and who absorbs the migration cost across teams is the part of this a reviewer will push back on.
## What actually goes wrong with a wide exported interface A twelve-method exported interface hurts in four distinct ways, and it helps to name them separately because each suggests a different part of the fix. 1. **Every implementer carries all twelve.** Any consumer that wants a fake for a test has to write twelve methods, eleven of which panic or return zero values. That is friction on every test in every downstream repository. 2. **It cannot grow.** Adding a thirteenth method breaks every outside implementer at compile time. In a versioned module that is a major-version bump, which means your consumers must edit import paths. 3. **It cannot be narrowed either.** Removing a method breaks anyone who calls it. The shape is frozen in both directions the moment it is exported. 4. **It drags the import graph.** Because the interface mentions your types in its signatures, every consumer importing it imports those too, and the dependency fan-in makes the package hard to change at all. ## Step one: measure the real usage Do not redesign from intuition. `go list -deps ./...` prints the transitive package dependencies of your own build, and `go list -f '{{.ImportPath}} {{.Imports}}' ./...` shows, package by package, who imports the proxy package. Across repositories, a search for the interface name plus its method names gives the same picture. What you are looking for is a table: consumer, methods called. Almost always it is heavily skewed -- most consumers call one or two methods, and only your own package uses all twelve. That table is the design. Each cluster of methods that consumers use together is a candidate interface. ## Step two: introduce the narrow interfaces Declare the small interfaces -- one or two methods each, named for the capability rather than the implementation: `HeaderRewriter`, `Forwarder`. Change your own function signatures so each parameter takes only the capability that function calls. Adding new interface types breaks nothing, so this step is safe to land on its own. ## Step three: keep the old name compiling The trick that makes the migration bearable is that an interface can be redeclared as the embedding of others without changing its method set at all. If the wide `Proxy` interface becomes `interface { HeaderRewriter; Forwarder; ... }`, every existing implementer and every existing caller still compiles, because the requirements are byte-for-byte the same set. Now consumers can move to the narrow interfaces at their own pace, and you have a mechanical way to see who is left: anyone still naming the wide type. ## Step four: delete it Once no consumer names the wide interface, remove it. If the package is versioned and published, that removal is the breaking change, so it belongs in the next major version -- which is exactly why steps two and three exist: they let the disruptive part happen once, on your schedule, rather than every time you want to add a method. ## The design change that stops it recurring The root cause is usually mechanical: someone generated an interface mirroring every method of a struct, exported it, and made it the package's public face. The durable fix is the standard Go shape -- export the concrete type from the constructor, and take the narrow interface as a *parameter* wherever your own code needs to dispatch. A struct can gain methods forever without breaking anyone; an interface cannot. When you do need to export an interface, size it by what a *caller* calls, never by what the implementation offers. Two methods is a normal ceiling. If you find yourself writing a fifth, ask whether you are describing a capability or simply re-typing an object. ## What to say in the interview Name the measurement step, the compatible intermediate (embedding the small interfaces to preserve the method set), the staged consumer migration, and the removal in a major version. Then name the prevention: interfaces sized by the call site, concrete types returned from constructors.
- Which go command shows you who depends on the package carrying that interface?`go list -deps ./...` prints the transitive package dependencies of your build, and `go list -f '{{.ImportPath}} {{.Imports}}' ./...` shows the direct imports package by package, so you can see the fan-in. `go mod graph` answers a different question -- the module requirement graph, not package-level imports.
- Why is redeclaring the wide interface as an embedding of the small ones safe for existing code?Because embedding only unions method sets. If the union is exactly the original list of methods, the interface's requirements are unchanged, so every existing implementer still satisfies it and every existing call still type-checks. It is a pure refactor of the declaration, not of the contract.
- Why is adding a thirteenth method to an exported interface worse than adding one to an exported struct?Nothing outside your package implements a struct, so a new method there is additive and breaks nobody. An interface is implemented by consumers, so a new requirement fails their compile. In a published module that forces a major version, and every consumer has to edit import paths to take it.
saying these in an interview costs you the question
- Proposes deleting methods immediately and letting consumers fix builds
- Says adding a method to an exported interface is backwards compatible
- Sizes the interface by the struct's methods rather than call sites
- Generates an interface mirroring every method of one implementation
- Assumes only the package's own tests implement it