Under the Interface Segregation Principle, who should define an interface — the component that implements it or the component that calls it? Explain the difference between a role interface and a header interface.
answer
- role interface = named by need; header interface = class mirror
- IUserService = header smell
- Go declares the interface in the consuming package
- port = ISP-shaped, consumer-owned abstraction
- DIP sets direction, ISP sets width
basics
~20 sThe caller should define it. An interface written from the caller's needs (a "role interface") stays small and stable. An interface that just mirrors an existing class's full public surface (a "header interface") adds a file but removes no coupling.
solid answer
~50 sInterfaces should be shaped, named, and ideally owned by the **consumer**. A **role interface** is declared in terms of what a caller requires — `PasswordHasher`, `OrderReader`, `Clock` — and typically has one to three members. A **header interface** simply duplicates every public method of one existing class (`UserService` → `IUserService`); it is a compile-time echo that segregates nothing, and it usually indicates interfaces were added reflexively for mocking rather than for design. Consumer ownership matters beyond naming: when the abstraction lives with the high-level policy that uses it, that policy no longer depends on the low-level implementation's package or module, which is Dependency Inversion; combined with ISP you get the small ports of hexagonal architecture. Go institutionalizes this — interfaces are conventionally declared in the consuming package (`io.Reader`), so implementers do not even know they satisfy them. Practically: name the interface after the need, put it beside the need, keep it to the members the need actually invokes, and let one concrete class satisfy several such ports.
code
go · 17 lines// Consumer-owned role interface: declared where it is USED, one method.
package report
type PriceSource interface { // named for the need, not for the impl
PriceOf(sku string) (Money, error)
}
func Build(src PriceSource, skus []string) Report { /* ... */ }
// The implementer never imports `report` and never says "implements".
package catalog
type Service struct{ /* 20 other methods too */ }
func (s *Service) PriceOf(sku string) (Money, error) { /* ... */ }
// catalog.Service satisfies report.PriceSource structurally.
// The report package depends on ONE method, not on the catalog package.go deeper
Say interfaces should describe what the caller needs, and that copying a class's whole method list into an IClass interface does not reduce coupling.
Contrast role vs header interface with examples and naming tells; note that one class can implement several role interfaces.
Add ownership/location: consumer-declared ports, DIP direction, hexagonal architecture, Go's structural convention, and the honest costs (type count, look-alike ports, discovery).
Set the boundary policy: which modules own ports, how the composition root wires adapters, when a broad implementer-owned contract is right (public API/plugins), and how to keep enforcement mechanical via module/dependency rules rather than review habit.
## The question behind the question ISP tells you interfaces should be narrow. But *narrow according to whom?* The answer is what makes ISP operational rather than aspirational: **narrow according to the client's requirement**. That immediately implies the client's needs should drive the interface's shape — and often its physical location and ownership too. ## Two shapes, named (Martin Fowler's distinction) ### Header interface An interface that restates the entire public surface of one concrete class, usually 1:1, usually named `IThing` or `ThingInterface` for class `Thing`. ``` class UserService { register(); login(); resetPassword(); exportCsv(); deactivate(); } interface IUserService { register(); login(); resetPassword(); exportCsv(); deactivate(); } ``` What it achieves: a seam for mocking, and a name to inject in a DI container. What it does **not** achieve: any reduction in what clients depend on. Every caller still sees five methods; every change to `UserService`'s surface still changes the interface; every mock still fakes five members. The term "header interface" comes from C/C++ header files, which likewise mirror an implementation's declarations. It is not always wrong — it can be a deliberate compatibility or plugin boundary — but it is not ISP, and shipping one per class as a policy is a well-known anti-pattern ("interface for every class"). ### Role interface An interface named for a **capability a caller needs**, containing only the members that capability requires. ``` interface PasswordHasher { hash(pw); verify(pw, digest); } interface Clock { now(); } interface OrderReader { findById(id); } ``` One concrete class may satisfy several roles. A role interface is stable because it changes only when the *requirement* changes, not when the implementation grows a new feature. ### Naming tell Header interfaces are named after the implementation (`IUserRepository`, `UserServiceInterface`). Role interfaces are named after the need, often with an `-er`/capability noun (`Hasher`, `Notifier`, `Printer`, `Sink`, `Source`, `AuditReader`). If deleting the concrete class would make the interface's name meaningless, it is probably a header interface. ## Ownership: where the interface *lives* Beyond shape, there is the question of which module the interface source file belongs to. - **Implementer-owned** (the traditional layout): the `persistence` module declares `OrderRepository` and `JdbcOrderRepository`; the `orders` domain depends on the persistence module to get the type. The high-level policy now has a compile dependency on the low-level module. - **Consumer-owned** (Dependency Inversion applied): the `orders` domain declares the `OrderRepository` port it needs; the persistence module depends on the domain and implements it. Dependencies now point inward, toward policy. This is the **Ports and Adapters / hexagonal / clean architecture** arrangement, and the "port" is exactly an ISP-shaped role interface. Consumer ownership makes ISP self-enforcing: you literally cannot put a method in the port that the consumer does not call, because the consumer wrote it. ### Language support - **Go**: interfaces are satisfied *structurally*; the convention is to declare them in the consuming package. `io.Reader` is one method. An implementer need not import the consumer at all. Go's proverb — "the bigger the interface, the weaker the abstraction" — is ISP as folklore. - **TypeScript**: structural typing lets a function parameter declare an inline shape (`{ read(k: string): Blob }`) covering exactly what it uses; no nominal declaration or implementer cooperation required. - **Java/C#/Kotlin**: nominal typing means the implementer must explicitly declare the interface, so consumer-owned ports require the implementing module to depend on the consumer's module. That is the intended direction under DIP; module systems (Java modules, Spring Modulith, .NET assemblies) can enforce it. - **Rust traits / Swift protocols**: traits can be implemented for foreign types (with orphan-rule constraints), and protocol extensions let you retrofit conformance — both make consumer-defined roles practical. ## Trade-offs and honest limits 1. **More types.** Several small ports instead of one interface. Mitigate by naming them well and colocating them with the consumer, not in a global `interfaces/` dumping ground. 2. **Duplicate-looking ports.** Two consumers may each define a nearly identical one-method port over the same implementation. This is usually *correct* — they can evolve independently — but it looks like duplication in review; be ready to defend it. 3. **Discovery.** With Go-style structural interfaces, finding all implementers is harder; with nominal languages, `implements` gives you the list. 4. **Public API/plugin boundaries** genuinely may need a broad, implementer-owned contract that third parties code against; there, stability and documentation beat minimality, and default methods exist for evolution. 5. **Don't add an interface at all** if there is exactly one implementation, no test seam need (because you can construct the real thing cheaply), and no policy/detail boundary to invert. "Interface per class" is the failure mode this whole discussion exists to avoid. ## Practical checklist - Name it after the requirement, not the implementation. - Include only members the requirement invokes. - Put it next to the consumer when it is a policy/detail boundary. - Let one class implement several ports; do not create a class per port. - Delete it if it exists only because "everything should have an interface".
- In Java or C#, consumer-owned ports mean the implementation module must depend on the domain module. Isn't that backwards?That is the point of Dependency Inversion: source dependencies point toward high-level policy, opposite to the runtime call direction. The domain declares `OrderRepository`; the persistence adapter imports the domain and implements it. Wiring happens at the composition root (main / DI configuration), which is the only place allowed to know both sides.
- Two consumers define almost identical one-method interfaces over the same class. Should I merge them?Usually not. Their similarity today is a coincidence; merging couples the two consumers so that either one's future change touches the other. Merge only when they represent the same domain concept, not merely the same current signature.
- If a class ends up implementing six role interfaces, hasn't the complexity just moved?The class being broad is not an ISP problem — ISP constrains what callers see. Six roles on one class may, however, be an SRP signal worth examining separately: ask whether those roles have different reasons to change and different collaborators.
- When is a header interface actually the right choice?At a published API or plugin boundary where third parties implement or code against a stable, documented contract, and for legacy seams where you must mock a class you cannot otherwise decouple. The tell that it is deliberate rather than reflexive is that its shape is justified by external stability, not by mirroring one class.
Hiring a contractor: you write a job description for the work you need done ("needs to hang drywall"), not a copy of the contractor's résumé. The résumé-shaped requirement (header interface) changes every time they learn a new skill; the job description (role interface) changes only when the job changes.
saying these in an interview costs you the question
- Creating an `IFoo` for every `Foo` and calling it dependency inversion
- Naming interfaces after implementations rather than capabilities
- Parking all interfaces in one global `interfaces/` package, which destroys the colocation benefit
- Merging two consumers' similar ports "to avoid duplication", coupling unrelated consumers
- Believing interfaces exist primarily to enable mocking rather than to shape dependencies
- Claiming Go-style consumer-side interfaces are impossible in nominal languages — they just require the implementer to declare conformance