Go's interface mechanism is one of the language's finest designs. Implicit satisfaction — no explicit implements declaration, types automatically satisfy interfaces by having the corresponding methods. This design encourages small interfaces, consumer-side definition, and precise dependency declaration.

But precisely because it's so lightweight and effortless, it's also one of the most abused features.

Open any medium-to-large Go project and you'll likely find this: dozens of interfaces, over half with only one implementation. The author beams with pride: "programming to interfaces, leaving room for future extension." Six months later, half the interfaces have had their signatures changed — because the original guesses were wrong. The other half have never been looked at again. What's left is a system that's harder to trace, harder to understand, and harder to modify.

The problem isn't "programming to interfaces" itself. It's the judgment around "when to use interfaces."

Starting With a Common Scenario

In many Go projects you'll find code like this:

type UserRepository interface {
    GetByID(id int64) (*User, error)
    GetByEmail(email string) (*User, error)
    List(offset, limit int) ([]*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id int64) error
    Count() (int64, error)
}

This interface usually lives in a domain or repository package. It has one implementation: MySQLUserRepository. No Redis implementation, no Postgres, no in-memory. Just MySQL.

Why does it exist? Three standard answers: "Easy to swap databases later." "So tests can mock it." "Programming to interfaces, for decoupling."

All three sound right. But look closer: this interface has seven or eight methods, while the OrderService that uses it only needs GetByID. The other six methods have nothing to do with OrderService.

The problem isn't "defining an interface early" — it's defining the wrong thing in the wrong place.

Rule 1: Define Interfaces at the Consumer Side

The Go community has a saying: "accept interfaces, return structs." The more precise version: interfaces should be defined by the consumer, not the implementor.

The consumer knows what it needs. OrderService needs to look up a user by ID. It defines its own interface in its own package:

type UserProvider interface {
    GetUserByID(id int64) (*User, error)
}

Only the methods it actually uses. Then MySQLUserRepository, by having a method with the same signature, automatically satisfies this interface. No need to modify MySQLUserRepository's package, no need for any annotation.

The direction matters. An implementor-defined interface expresses "what I can provide" and naturally tends to bloat — listing every method. A consumer-defined interface expresses "what I need" and naturally tends to be precise — listing only what's actually used.

This is the Interface Segregation Principle: a module shouldn't be forced to depend on things it doesn't need. A 15-method UserRepository forces OrderService to know about Count(), Delete(), List() — things OrderService doesn't need. And every time UserRepository changes a method signature, OrderService recompiles, even if the changed method has nothing to do with it.

Go's implicit satisfaction makes "consumer-side interface definition" virtually costless. So there's no reason to pre-define large interfaces in the implementor's package.

Rule 2: Wait for Two Production Implementations Before Extracting

Defining an interface with only one implementation is, essentially, guessing. Guessing which behaviors are "universal" and which details can be hidden. Two outcomes: over-guessing — the interface includes methods that will never be used; under-guessing — when the second implementation arrives, the interface doesn't fit and needs signature changes, dragging all consumers along.

Both outcomes are worse than "no interface."

Wait for two real production implementations — not test mocks, but two production implementations: MySQL and Redis storage, two different third-party payment APIs, two different message queues. With two implementations, you can see which behaviors truly share and which are single-implementation specifics. Extracting the interface at that point has far higher accuracy than guessing.

The counterargument: if consumers depend on concrete types directly, switching implementations later means changing all consumers. Pre-defining an interface avoids this. This counterargument is valid, but it sidesteps a premise: will the switch actually happen?

Most "just in case" interfaces serve a "what if" that never materializes. This isn't buying insurance — it's pre-paying a cost to solve a problem that may never appear. And with only one implementation, that cost — the extra indirection, the extra file, the extra concept — genuinely adds to today's comprehension burden. Every person who reads UserRepository mentally asks "where's the other implementation?" When the answer is "there isn't one, it's for the future," a bit of trust is consumed.

If you need mocks for testing, Go has options that don't require interfaces. Tests within the same package can access unexported types directly. For external dependency testing, use httptest, testcontainers, or integration tests against real dependencies. Not all mocks require interfaces.

Small Interfaces vs. Large Interfaces

The Go standard library is full of small interface exemplars: io.Reader (one method), io.Writer (one method), fmt.Stringer (one method). These interfaces are powerful not because they have many methods — but precisely because they have so few that nearly any type can satisfy them.

Rob Pike summarized it in the Go Proverbs: "The bigger the interface, the weaker the abstraction." Large interfaces have weak abstraction power because they assume too much. They assume all implementations share the same behavior set, and all consumers need all those behaviors. Both assumptions are almost always wrong in practice.

Compare io.Reader with the 15-method UserRepository. Reader demands one thing: the ability to read. Any file, network connection, memory buffer, compression stream — as long as it can read, it satisfies the interface. Abstraction power is extremely strong because the requirement is minimal. UserRepository demands 15 things; the set of types that can satisfy all 15 is essentially one. This isn't abstraction — it's copying a concrete type's signatures into an interface.

Small interfaces also compose flexibly. io.ReadWriter composes Reader and Writer without defining any new methods. If a consumer needs "read and write," it composes two small interfaces; if only "read," it depends on just Reader. Dependencies are precisely controlled — no more, no less than what's needed.

Abstraction Isn't Free

Every interface definition adds one layer of indirection. Every code trace from call site to interface definition to actual implementation — this three-step detour slows understanding and debugging slightly. Not much per interface, but dozens of interfaces accumulated, and the cognitive load adds up.

This isn't an argument against interfaces. It's a reminder that before defining each interface, ask: does the benefit of this indirection (decoupling, substitutability, testability) outweigh its cost (longer trace chains, increased comprehension difficulty)? With only one implementation, the answer is almost always no.

One more often-overlooked point: interfaces add cost for tools, not just humans. IDE "go to implementation" works fine with one implementation, but it first needs to resolve the interface relationship. gopls's performance bottlenecks in large projects include maintaining interface satisfaction graphs. A project with a hundred single-implementation interfaces means gopls continuously expends compute on these never-productive indirection relationships. This cost is real.

Common Anti-Patterns

"Large interface."

Symptoms: one interface containing all methods, usually defined in the implementor's package. Aliases: "God interface," "universal repository."

Why it's a problem: large interfaces assume all consumers need the same method set, but different consumers almost always need different subsets. They also assume all implementations can provide the complete method set — if a new implementation can only provide some methods, it doesn't "satisfy" the interface, even though it could perfectly serve a specific consumer's needs.

Fix: split into consumer-side small interfaces, each containing only the methods that consumer actually uses. A UserRepository with 15 methods might decompose into three interfaces with 1-2 methods each, living in the order, auth, and report packages.

"Single-implementation interface."

Symptoms: only one struct in the codebase satisfies it. Common justifications: "for future extension," "for test mocking."

Why it's a problem: a prematurely defined interface freezes an unvalidated abstraction. Beyond the guessing risk already discussed, there's a subtler issue: it changes later arrivals' behavior. When an interface already exists, newcomers will shove new implementations into it, even if the interface's shape doesn't fit them — "there's already an interface, might as well use it." A guessed interface becomes a constraint across the entire system.

Fix: delete the interface. Consumers depend on the concrete type directly. Wait for the second implementation.

"Test interface."

Symptoms: the interface exists solely to enable unit test mocking.

Why it's a problem: mock tests can only verify "the code under test called the right methods," not "the calls produced correct side effects." A project with all tests passing can still break against real dependencies — because mocks won't tell you about SQL syntax errors, network timeouts, or serialization incompatibilities. Mocks give you confidence, but that confidence may not hold when interacting with the real world.

More importantly, interfaces defined for mock convenience often have odd shapes — designed not to express the consumer's real needs but to "make mocks easy to write." This is design pollution — production code structure shaped to serve test code convenience.

Fix: prefer real dependencies or test helpers for integration tests. If you genuinely need to abstract an external dependency (like an HTTP client), use standard library testing tools — net/http/httptest is designed for exactly this. Database tests use testcontainers to spin up real database instances. These are slower than mocks, but what they test is real.

"Implementor-defined interface."

Symptoms: an interface defined in the same package as the sole implementation, referenced by other packages.

Why it's a problem: it reverses the dependency direction. When the interface lives in the implementor's package, all consumers must import the implementor's package. A modification — adding a method to the interface — forces all consumers to recompile, even those that don't need the new method. Dependencies should flow from consumer to implementor, not the reverse.

In Go this is especially counterintuitive because implicit satisfaction makes "implementor-defined interface" syntactically as easy as "consumer-defined interface." Not easy to get wrong, but easy to do wrong.

Fix: move the interface to the consumer — whoever uses it defines it. The implementor doesn't need to know the interface exists.

The Verdict

If there's only one implementation right now, don't define an interface.

Not laziness, not tech debt, not a "temporary solution." This is the correct engineering decision at this moment. Abstraction's value comes from accurate identification of commonality, and accurate identification requires not imagination — it requires at least two real use cases in front of you.

When the second implementation genuinely appears, having examined both, having seen their commonalities and differences, you'll naturally know which methods the interface should include and which details to omit. No guessing needed. Abstractions aren't designed — they're extracted from real use cases.

An interface extracted from use cases has a far higher probability of being correct than one drawn up in advance. This isn't anti-design — it's anti making irreversible design decisions without sufficient information. Once an interface is defined and depended on by multiple consumers, modification cost multiplies. See clearly first, then define. The order can't be reversed.