When designing a public API, how would you decide between a multi-bound `where` clause (e.g. `where T : Persistable, T : Auditable`) versus introducing a single combined interface? What are the trade-offs?
answer
- `where` = additive/structural, works for types you don't own
- Combined interface = named concept, needs explicit conformance
- No `typealias` for a bound set
- Repetition vs. naming/docs trade-off
- Promote to interface only for stable, widely reused conjunctions
basics
~20 sUse a multi-bound where when callers' types already implement the separate interfaces and you don't want to force them to add a new marker. Introduce a combined interface only when the combination is a real, reusable domain concept worth naming.
solid answer
~50 sA multi-bound `where` clause keeps the constraint **structural and additive**: any type that already implements `Persistable` and `Auditable` qualifies automatically, with no edit to those types. That maximizes interoperability and avoids coupling callers to your API's vocabulary. The cost is repetition — the same `where T : Persistable, T : Auditable` may be copied across many signatures, and the combination is unnamed. A **combined interface** (`interface AuditablePersistable : Persistable, Auditable`) names the concept, shortens signatures (`<T : AuditablePersistable>`), and gives a place for shared documentation — but it requires every conforming type to **explicitly** declare it, creating coupling and friction for third-party types you don't control. Rule of thumb: prefer `where` for ad-hoc capability conjunctions and external types; promote to a named interface only when the conjunction is a stable, meaningful domain abstraction reused widely. You can also use a `typealias`-like pattern only via interface, since Kotlin has no alias for a bound set.
code
kotlin · 11 linesinterface Persistable { val id: Long }
interface Auditable { val updatedAt: Long }
// Additive: any type implementing both qualifies, no edits needed
fun <T> persist(e: T) where T : Persistable, T : Auditable {
println("saving ${e.id} @ ${e.updatedAt}")
}
// Named concept, reusable, but conformance must be explicit
interface AuditableEntity : Persistable, Auditable
fun <T : AuditableEntity> persistNamed(e: T) = persist(e)go deeper
Recognizes both approaches express 'must implement several interfaces.'
Notes the interface shortens signatures while where avoids editing conforming types.
Articulates additive/structural vs. nominal conformance and the third-party interoperability trade-off.
Gives a crisp heuristic tying ownership, reuse, and stability to the choice, and recognizes there is no bound-set alias plus the single-class-bound limit.
## The two options ```kotlin // Option A: multi-bound where (structural, additive) fun <T> save(entity: T) where T : Persistable, T : Auditable { /* ... */ } // Option B: combined interface (nominal, named) interface AuditablePersistable : Persistable, Auditable fun <T : AuditablePersistable> save(entity: T) { /* ... */ } ``` ## What `where` buys you - **Additive / structural-ish requirement:** any type that *independently* implements both interfaces qualifies — no need to retrofit a new supertype. Crucial for types you don't own (library/third-party). - **No new vocabulary:** callers aren't coupled to an interface invented by your API. - **Composability:** different functions can demand different conjunctions without an explosion of named combinations. ## What `where` costs - **Repetition:** the bound list is copy-pasted across signatures; changing it touches many sites. - **No name / no docs home:** the combination has no single declaration to document or to hang shared default methods on. - **No alias:** Kotlin has no `typealias` for a *set of bounds*, so you cannot abbreviate the conjunction except by introducing an interface. ## What a combined interface buys you - **A named concept** with one place for KDoc and possibly default implementations. - **Shorter signatures** (`<T : AuditablePersistable>`), reused everywhere. - **Discoverability:** the type system documents that this combination is a first-class idea. ## What it costs - **Explicit conformance required:** every type must declare `: AuditablePersistable`. Third-party types that already implement both pieces still won't qualify until edited — often impossible. - **Coupling:** spreads your API's naming into domain types. - **Rigidity:** a wrong-headed combination is harder to walk back once published. ## Decision heuristic 1. Is the combination a **stable, meaningful domain abstraction** reused widely, and do you **control** all conforming types? → Combined interface. 2. Is it an **ad-hoc** capability conjunction, or must **external/uncontrolled** types qualify automatically? → Multi-bound `where`. 3. Mixed? Offer the interface for your own types **and** keep functions bounded by the interface — external types can implement the small interfaces and add the combined one via an adapter. ## Subtlety: at-most-one-class still applies Both approaches inherit the JVM rule that at most one bound (or supertype) is a class; the rest are interfaces. So a `where` of `T : SomeBaseClass, T : SomeInterface` and the equivalent interface both face the same single-class-inheritance limit.
- Can you alias a set of bounds to avoid repeating the `where` clause?No — Kotlin has no `typealias` for a conjunction of bounds. The only way to name the combination is to declare an interface that extends them.
- Why might a combined interface fail for third-party types?Nominal conformance requires the type to explicitly declare the interface. A third-party type you cannot modify won't qualify even if it implements both component interfaces.
where is hiring for a list of skills; a combined interface is requiring a specific certification that bundles those skills — convenient internally but exclusionary to outsiders who have the skills uncertified.
saying these in an interview costs you the question
- Claiming a `typealias` can name a multi-bound set
- Asserting a combined interface always qualifies types that implement its parents (it needs explicit declaration)
- Ignoring the third-party / uncontrolled-type interoperability angle
- Treating the choice as purely cosmetic with no coupling implications