As a Go tech lead, would you require every service package to export an interface so consumers can mock it?
answer
- what does the mandate actually buy?
- implicit satisfaction makes it free later
- adding it later is cheap, removing it is not
- write the exceptions down, then count them
basics
~20 sNo, not as a blanket rule. Go satisfies interfaces implicitly, so a consumer can declare its own seam whenever it needs one; the mandate buys uniformity at the price of a second permanent API surface per package.
solid answer
~50 sI would set the default the other way: packages return concrete types, and the package that consumes behaviour declares the narrowest interface it needs. Implicit satisfaction means that costs nothing to adopt later, so the mandate buys only uniformity for double-generation tooling, while every mandated interface is a second exported surface importers couple to, cannot be widened without breaking them, and gives fakes a large area to drift over. I would write the exceptions down rather than leave them to taste: two implementations shipping today, a boundary another team plugs into, or an external dependency that must be stood in for. Then I would make it measurable -- count exported interfaces with one implementation -- and hear the test-infrastructure owner's case, because the real tension is their tooling uniformity against per-package coupling, and that tradeoff is mine to own, not to dismiss.
go deeper
Know the default this policy encodes: return concrete types, and let the package that consumes behaviour declare the interface it needs.
Be able to explain why the mandate is not free -- a second exported surface per package, an interface that cannot be widened without breaking implementers, and wide stand-ins that are easy to get wrong.
Argue the case with evidence: name the failure modes you have seen, propose the narrow-consumer-interface default, and describe how you would migrate an existing codebase without a big-bang change.
Own the decision end to end -- the written exceptions, who reviews an exported interface, what you measure, what you offer the team whose tooling the mandate was serving, and what reversing the call would cost once importers depend on it.
## What is actually being decided The question is not "are interfaces good". It is whether the *producer* of a type owes every consumer a pre-declared abstraction. In Go that is a genuine choice, because the language does not force it: satisfaction is structural and checked at the point of use, so any package can create the abstraction it needs, whenever it needs it, without the producer's cooperation. The mandate therefore buys convenience and consistency, not capability -- and it is a decision a tech lead owns and an API reviewer can overrule, which is why it is worth deciding deliberately rather than by habit. ## The case for the mandate, stated fairly The strongest advocate is usually the engineer who owns test infrastructure. Their argument is real: - **Uniformity.** If every service package exports `Fooer`, doubles can be produced the same way everywhere and nobody has to think. - **Onboarding.** Engineers arriving from ecosystems where interfaces are declared up front find the codebase familiar. - **A visible seam.** The interface documents, in one place, what a service does. - **No arguments in review.** A mechanical rule ends a recurring debate, and ending recurring debates has value on its own. Dismissing this as cargo-culting is the wrong move. It is a bid for lower variance, and variance genuinely costs teams. ## The costs, which compound quietly - **A second permanent surface.** Every package now exports an interface plus a concrete type. Importers name the interface in their own signatures and struct fields, so it is load-bearing whether or not it earns its keep. - **It cannot be widened.** Adding a method to an interface that outside code implements breaks that code at compile time; adding a method to a struct breaks nobody. The mandate converts a safe change into an unsafe one in every package it touches. - **The abstraction is the wrong shape.** A mirror of one struct abstracts nothing, and the fourteen-method version gives every stand-in fourteen behaviours to get wrong. Drift then produces the worst failure mode there is: a fully green suite over a broken integration. - **Callers lose the type.** Constructors that return the interface hide fields, hide later methods and degrade the documentation consumers read. - **It is unremovable at scale.** Once dozens of packages export interfaces and hundreds of call sites name them, walking it back is a large mechanical change nobody schedules. ## The policy I would set **Default:** return concrete types; declare interfaces where they are consumed, as narrow as the consumer's actual use, and unexported when only that package needs the name. **Written exceptions, so the default is not litigated weekly:** 1. Two or more implementations ship in the same release. 2. The interface is the contract another team implements -- a plugin point, an extension seam. 3. The dependency is outside the process and outside your control (a vendor API, a payments backend), where the seam has to exist regardless. 4. A constructor genuinely returns different concrete types depending on its input. **Owner and appeal path:** the owning team decides inside its package; anything exported goes past API review, and the lead breaks ties. Say out loud that the reviewer may overrule the author here, because an exported interface is a promise the whole organisation keeps. **Make it observable.** Count exported interfaces with exactly one implementation in the repository and watch the number over time. A policy with no measurement is a preference. **Give the advocate a real answer, not a veto.** If their pain is that doubles are inconsistent, solve that: put the stand-in and one exported behaviour suite next to the real implementation, so consumers get a supported fake without every package exporting an interface. That satisfies the uniformity argument without the coupling. ## What it costs to change your mind later If the organisation is pre-production or single-repo, removal is a mechanical change: delete the interface, return the concrete type, fix call sites, one commit. If external consumers already import the interface, removal is a breaking release, and the honest options are to keep the interface as a published contract you promise never to widen, or to version the module. That asymmetry is the reason to make the default choice early: adopting an interface later is free in Go, and removing one later is not.
- The test-infrastructure owner says inconsistent doubles are costing the team real time. What do you offer instead of the mandate?A supported stand-in and one exported behaviour suite shipped beside each real implementation, so consumers get a uniform, maintained double without every package exporting an interface. That answers the uniformity complaint at its source and keeps the coupling out of the API surface.
- How would you migrate a codebase that already exports one interface per service?Stop the growth first with a review rule, then convert opportunistically: when a package is touched, return the concrete type and let its consumers declare narrow interfaces. Count the remaining one-implementation exported interfaces so progress is visible. Inside a single repository each conversion is a mechanical, same-commit change.
- Which exception is most often abused, and how do you police it?"There will be a second implementation soon." Require the second implementation to exist in the same release, not on a roadmap. A speculative second implementation almost never arrives, and if it does, implicit satisfaction means introducing the interface then costs a single declaration.
- Does this policy change for a library published outside the organisation?It gets stricter. Whatever you export, you must live with: an exported interface can never be widened without breaking implementers, so publish one only when its method set is finished and you intend others to implement it. Otherwise export the concrete type and let consumers abstract locally.
saying these in an interview costs you the question
- Interfaces everywhere is just better design, no tradeoff
- The mandate is free because interfaces cost nothing at run time
- Consumers cannot write tests unless we export an interface
- We can always remove the interfaces later if they hurt
- The test infrastructure owner's uniformity argument is cargo cult