"Always implement things when you actually need them, never when you just foresee that you need them." Ron Jeffries
YAGNI is a practice from Extreme Programming with a specific, narrow claim: don't implement a feature until it is required. Not "keep it in mind." Not "leave a hook for it." Don't build it.
This sounds obvious. It isn't. The pull toward speculative design is strong; it feels like good engineering to plan ahead, to leave room for extension, to avoid painting yourself into a corner. But speculative features have real costs: they take time to write, time to test, time to maintain, and they constrain future design based on requirements that were never real.
The code you didn't write has no bugs.
The classic trap: speculative parameters
// BAD — config struct added "for flexibility", used by exactly one caller,
// which always passes the same values.
type FetchOptions struct {
Timeout time.Duration
Retries int
MaxBytes int64
UserAgent string
FollowRedir bool
}
func FetchPage(url string, opts FetchOptions) ([]byte, error) {
// ...
}
// Every caller does this:
FetchPage(url, FetchOptions{
Timeout: 5 * time.Second,
Retries: 3,
MaxBytes: 1 << 20,
UserAgent: "myapp/1.0",
FollowRedir: true,
})// GOOD — implement what callers actually use.
// Add options when a second caller needs different values.
func FetchPage(url string) ([]byte, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(io.LimitReader(resp.Body, 1<<20))
}Speculative interfaces
// BAD — defined a plugin interface for a feature that was never built.
// The interface is never implemented except by the one real type.
type StorageBackend interface {
Read(key string) ([]byte, error)
Write(key string, value []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
Stat(key string) (StorageInfo, error)
}
// The only implementation:
type DiskStorage struct{ root string }
// ... 200 lines of implementation// GOOD — use the concrete type directly.
// Define an interface at the call site if and when a second implementation appears.
type DiskStorage struct{ root string }
func (s *DiskStorage) Read(key string) ([]byte, error) {
return os.ReadFile(filepath.Join(s.root, key))
}
func (s *DiskStorage) Write(key string, value []byte) error {
return os.WriteFile(filepath.Join(s.root, key), value, 0644)
}The hidden cost of unused code
Unused code isn't free:
- Tests must cover it. A speculative code path still needs tests to stay green as the codebase evolves.
- It becomes load-bearing. Six months later, someone assumes the hook is there for a reason and builds on top of it.
- It rots. Code that isn't used isn't tested in practice. It quietly breaks.
- It signals false requirements. New engineers treat existing code as documentation of intent.
When to actually build ahead
YAGNI has a boundary. Some structural decisions are genuinely hard to reverse:
- Data formats. If you're defining a wire format or a storage schema, think about versioning. Not because you'll definitely need it, but because changing it later is disproportionately expensive.
- Public APIs. If you're shipping a library, the interface is a contract. Breaking it has real cost.
- Performance headroom. If you know from measurement (not intuition) that a naive approach will hit a wall, address it.
These are exceptions. The default is: don't.
Smell: You search for usages of a function and find exactly one caller: the test. Or a config struct with eight fields where every caller sets the same six. Or an interface defined in the same package as its only implementation.