Design a companion factory that validates input and can fail gracefully and/or return different subtypes. Compare returning null, Result, throwing, and sealed-result types.
answer
- return type drives failure & polymorphism
- T? = absent, Result/sealed = failure-with-reason
- require/check = invariant violation → throw
- supertype/sealed return → factory picks subtype
- name failable variants: ...OrNull / tryParse
basics
~20 sA factory can check the input before building. If the input is bad it can return null, return a Result with the error, or throw. It can also decide which subtype to create based on the input and return them all as the common supertype.
solid answer
~40 sBecause a factory is an ordinary function with a chosen return type, you control the failure and polymorphism story. For failure: return `T?` (`parseOrNull`) for simple optional construction; return `Result<T>` (`runCatching`) or a custom sealed type when callers need the error reason; throw `IllegalArgumentException` via `require` for programmer-error preconditions that should never occur at runtime. For polymorphism: declare the return type as a sealed supertype or interface and pick the concrete subtype inside the factory (`when (input) { ... }`), returning each as the supertype — the private constructors of the subtypes keep the factory the sole creator. Convention: name failable variants explicitly (`fromStringOrNull`, `tryParse`) so the contract is visible at the call site. Prefer null for 'absent', Result/sealed for 'failed with reason', and exceptions for true precondition violations.
code
kotlin · 16 linessealed interface Money
private class Cash(val amt: Long) : Money
private class Card(val token: String) : Money
object MoneyFactory {
fun parse(s: String): Result<Money> = runCatching {
when {
s.startsWith("$") -> Cash(s.drop(1).toLong())
s.startsWith("card:") -> Card(s.removePrefix("card:"))
else -> throw IllegalArgumentException("unknown money: $s")
}
}
}
val ok = MoneyFactory.parse("$500") // Result.success(Cash)
val bad = MoneyFactory.parse("???") // Result.failure(...)go deeper
Knows a factory can validate and return null if input is bad.
Distinguishes nullable vs throwing and can write a require-based validating factory.
Maps each failure mode (null/Result/sealed/throw) to its use case and returns a sealed supertype with internal subtype dispatch.
Defines a consistent error-handling policy across the API surface, balancing exhaustiveness, ergonomics, and binary-compatible evolution of return types.
## Why the factory owns this decision A factory's **declared return type** is free to be nullable, a `Result`, a sealed class, or a supertype — a constructor cannot do any of these. This makes the companion factory the natural home for validation, graceful failure, and polymorphic dispatch. ## Failure strategies ```kotlin class Email private constructor(val value: String) { companion object { // 1) nullable: 'absent/invalid' with no reason fun parseOrNull(s: String): Email? = if ("@" in s) Email(s) else null // 2) Result: failure WITH a reason fun parse(s: String): Result<Email> = if ("@" in s) Result.success(Email(s)) else Result.failure(IllegalArgumentException("missing @")) // 3) throw: precondition that must hold fun of(s: String): Email { require("@" in s) { "invalid email: $s" } return Email(s) } } } ``` - **`T?` (nullable)** — cheapest; use when callers only need 'present or not' and Elvis/`?.` handling suffices. - **`Result<T>`** — carries the failure; `runCatching { ... }` or explicit `Result.success/failure`. Caller uses `getOrElse`, `fold`, `onFailure`. - **Throwing (`require`/`check`)** — for **programmer errors / invariants** that indicate a bug, not expected user input. - **Custom sealed result** — when you have several distinct, exhaustively-handled outcomes: ```kotlin sealed interface ParseResult { data class Ok(val email: Email) : ParseResult data object MissingAt : ParseResult data object Empty : ParseResult } ``` A `when` over a sealed type is exhaustive, so callers can't forget a case. ## Polymorphic / subtype-returning factories The return type can be a sealed supertype; the factory chooses the concrete subtype: ```kotlin sealed class Shape { private class Circle(val r: Double) : Shape() private class Square(val s: Double) : Shape() companion object { fun of(kind: String, v: Double): Shape = when (kind) { "circle" -> Circle(v) "square" -> Square(v) else -> error("unknown $kind") } } } ``` Callers receive `Shape` and never see the concrete classes — a constructor could never do this. Private subtype constructors keep the companion the sole creator (a flavor of the **factory-method** pattern). ## Choosing - Optional, reason not needed → **nullable**. - Failure with a reason, recoverable → **Result** or **sealed result**. - Must-not-happen invariant → **throw** via `require`. - Need to vary the concrete type → **supertype/sealed return** with internal dispatch. Name failable variants (`...OrNull`, `tryParse`, `parse`) so the contract is obvious.
- When is throwing preferable to returning a Result?When the bad input represents a programmer error or broken invariant (a bug), not an expected runtime condition — use require/check so the failure surfaces loudly during development.
- How does returning a sealed supertype help callers?A `when` over a sealed type is exhaustive, so the compiler forces callers to handle every outcome/subtype, preventing forgotten cases.
saying these in an interview costs you the question
- Throwing for ordinary, expected invalid user input
- Returning null when the caller clearly needs the failure reason
- Exposing concrete subtype constructors, defeating the factory
- Swallowing exceptions in runCatching without surfacing them
- Mixing several failure styles inconsistently across one API