A Go test's hand-written fake must implement six methods though the code calls two. What went wrong?
answer
- width is paid by every implementer
- count the methods the code actually calls
- the abstraction mirrors the store, not the need
- declare it again, smaller, where it is used
- the concrete type satisfies both without edits
basics
~20 sThe interface was taken from the implementation's whole surface instead of being declared by the code that uses it. Redeclare it in the consuming package with just the two methods called; the real type still satisfies it, and the fake shrinks to two methods.
solid answer
~50 sSomebody wrote the interface as a mirror of the database layer — every method the store can perform — and the consumer depends on all of it. Interface width is paid by every implementer, and in tests that means each fake must stub four methods nobody calls. The fix is to declare the interface in the package that uses it, with the two methods it actually calls: `type userReader interface { UserByID(ctx context.Context, id int64) (User, error) }`. The concrete store satisfies the narrow interface without any change, because satisfaction is by method set. The width also matters beyond typing effort: a wide interface imports the producer's types into the consumer's signatures, tells a reader nothing about what this code really needs, and forces a recompile-and-fix pass on every fake each time an unrelated method is added.
code
go · 9 lines// Declared beside the implementation, mirroring it
type Repository interface {
UserByID(ctx context.Context, id int64) (User, error)
SaveUser(ctx context.Context, u User) error
DeleteUser(ctx context.Context, id int64) error
ListUsers(ctx context.Context) ([]User, error)
Count(ctx context.Context) (int, error)
Ping(ctx context.Context) error
}go deeper
Remember that an interface should list only the methods the calling code uses, and that the real type does not need editing when you declare a smaller one for it.
Explain the mechanics: interface width is a cost paid by every implementer, satisfaction is structural so narrowing needs no change to the concrete type, and wide interfaces drag the producer's types into your signatures.
Show how you would spot this in review and unwind it safely across an existing codebase, including what breaks when a shared interface is split and how you sequence the change.
Own the standard: no shared central interfaces package, consumers declare their own views, and a widening of any exported interface is treated as a compatibility event rather than an ordinary edit.
## What the six-method fake is telling you Effort in a test double is a measurement. When a stand-in has to implement four methods the code under test never calls, the interface is describing the *implementation*, not the *dependency*. Someone enumerated everything the database layer can do and called that an abstraction. In Go the fix is unusually cheap, because interfaces are satisfied structurally and any package may declare one. ## Width is paid by implementers ```go // Too wide: declared to mirror the store type Repository interface { UserByID(ctx context.Context, id int64) (User, error) SaveUser(ctx context.Context, u User) error DeleteUser(ctx context.Context, id int64) error ListUsers(ctx context.Context) ([]User, error) Count(ctx context.Context) (int, error) Ping(ctx context.Context) error } ``` Every type that wants to stand in for this must supply six methods. There is exactly one real implementation and N test implementations, so the cost lands almost entirely on tests. Worse, each of those four unused methods is dead weight that a reader must check before concluding what the service touches. Narrow it to the need: ```go type userReader interface { UserByID(ctx context.Context, id int64) (User, error) } ``` The real store already has that method, so it satisfies the new interface with no edit. Nothing about the production wiring changes; only the declared dependency shrinks. ## Three costs a wide interface carries beyond typing **It leaks the producer's types.** A method like `Begin(ctx context.Context) (*sql.Tx, error)` in the interface forces the consuming package to import `database/sql` in order to name the type — so the abstraction that was supposed to decouple the packages has hard-wired one to the other. A narrow interface usually mentions only the consumer's own domain types. **It hides intent.** `Repository` says "anything a database can do". `userReader` says "this code reads one user by id". The second is documentation that the compiler enforces, and it tells a reviewer where to look for effects. **It multiplies churn.** Add a seventh method to the shared interface for one new caller, and every hand-written fake in every package stops compiling until somebody adds a stub. That is a real, recurring tax, and it is why teams that share one god interface end up embedding it into their fakes just to stop the breakage — trading a compile error for a runtime nil panic later. ## But isn't duplicating the signature bad? This is the objection worth answering directly, because engineers arriving from languages where interfaces are declared once and implemented explicitly find consumer-side declaration wasteful. In Go, repeating a method signature in two consuming packages duplicates a line of *description*, not behaviour. The two interfaces are then free to diverge: one consumer can keep a single-method view forever while another grows a second method, and neither forces a change on the other or on the implementation. The rule of thumb: an interface with more methods than the calling code uses is a design smell, and one method is the common case. Two is fine when the same operation genuinely needs both — say a read and a write inside one transaction-shaped workflow. Beyond that, ask whether you are looking at one dependency or several, and whether the consumer should take two parameters instead of one. ## Naming and export A consumer-side interface used only inside the package is usually unexported (`userReader`), which is a signal that it is a local description and not part of anybody's API. Export it only if callers outside the package must name it — for example, because it appears in an exported constructor's signature. ## A test that gets easier as a result With one method, the fake is four lines and every case is a struct literal: ```go type fakeReader struct { user User err error } func (f fakeReader) UserByID(ctx context.Context, id int64) (User, error) { return f.user, f.err } ``` The review signal is simple: if writing the stand-in feels like clerical work, the interface is describing the wrong thing. Shrink the interface first, and the test double shrinks with it.
- Doesn't declaring the same method signature in three consuming packages duplicate code?It duplicates a description, not behaviour — one line per package, with no logic to keep in sync. The payoff is independence: each consumer's view can grow or shrink without touching the others, and the single concrete implementation satisfies all of them without knowing they exist.
- When is a two-method interface right rather than two one-method ones?When one operation in the calling code genuinely needs both, so splitting them would just make the constructor take two parameters that always arrive together. If different code paths use different methods, prefer separate narrow interfaces so each caller states its own need.
- Should a consumer-side interface be exported?Usually not. If it is only named inside the package, keep it unexported so it reads as a local description rather than API. Export it only when it appears in an exported signature callers must satisfy — and then treat the method list as something you cannot casually widen.
- How do you spot an over-wide interface in review without counting call sites?Look at the test doubles. A stand-in full of methods that panic, return zero values, or are marked unused is the interface telling you it describes an implementation. Also watch for the interface importing the producer's types, such as a method returning a database-specific handle.
saying these in an interview costs you the question
- Says the interface must list everything the implementation offers
- Treats a repeated signature in two packages as duplicated logic
- Widens the shared interface for one new caller without a thought
- Embeds the wide interface in the fake to silence the compiler
- Leaves database-specific types in the consumer's method signatures