skip to content

When should you prefer secondary constructors versus default parameter values or factory functions in Kotlin?

level: seniorimportance: should knowfreq 40%

answer

  1. Defaults for simple optional args
  2. Factories for naming, failure, subtype, caching
  3. Secondary for different types / framework signatures
  4. Java doesn't see defaults as overloads -> @JvmOverloads
  5. Constructor must return exact type + fresh instance

basics

~20 s

Prefer default parameter values for simple optional arguments because they need less code. Use secondary constructors mainly when you need different parameter types, extra setup logic, or to satisfy frameworks and Java that call specific constructors.

solid answer

~40 s

In idiomatic Kotlin, default parameter values on the primary constructor replace most overloaded constructors, since a single primary constructor with defaults covers many call shapes. Factory functions (top-level or companion-object) are preferred when construction needs a meaningful name, can fail/return a subtype, or should be cached. Secondary constructors are justified when: you need an alternative parameter list with different types (e.g., construct from a String vs. parsed fields), you need to run logic that doesn't fit a default expression, or you must expose specific JVM constructor signatures for Java callers or frameworks (Jackson, JPA, Android Views). Note default-valued constructors aren't automatically overloaded for Java; @JvmOverloads generates the overloads. Choose based on naming, failure semantics, type variety, and interop.

go deeper

for a junior

Knows default parameter values exist and reduce constructor count.

for a middle

Chooses defaults vs secondary correctly for common cases and knows factory functions exist.

for a senior

Articulates failure semantics, naming, subtype/caching, and Java-interop trade-offs across all three tools.

for a principal

Sets team-wide API conventions, weighs binary compatibility and framework contracts, and uses @JvmOverloads deliberately for stable Java-facing APIs.

## The three tools - **Default parameter values**: `class C(val x: Int = 0)` — one constructor covers many call shapes. - **Factory functions**: a `companion object` function or top-level function that returns an instance. - **Secondary constructors**: alternative `constructor(...)` blocks delegating via `this(...)`. ## Prefer default parameter values when You just want **optional arguments** of the **same conceptual shape**: ```kotlin class Server(val host: String, val port: Int = 8080, val tls: Boolean = true) ``` This collapses what Java would express as many overloaded constructors into one. Combined with **named arguments**, callers pick exactly which to override. ## Prefer factory functions when - Construction deserves a **descriptive name**: `User.fromJson(...)`, `Color.fromHex(...)`. - It may **return a subtype** or a cached/shared instance (a constructor must return its exact type and a fresh instance). - It can **fail** and you want to return `null`/`Result` rather than throw. ```kotlin class Color private constructor(val rgb: Int) { companion object { fun fromHex(hex: String): Color = Color(hex.removePrefix("#").toInt(16)) } } ``` ## Prefer secondary constructors when - You need an **alternative parameter list with different types** that can't be expressed as defaults — e.g., one constructor takes a `String`, another the parsed components. - A **framework or Java caller** requires a specific constructor signature: JPA needs a no-arg constructor; Jackson, Android `View` subclasses, and DI frameworks call specific constructors. - You need **setup logic** in the body beyond what a default expression conveniently allows. ## Java interop nuance Kotlin default parameter values are **not** seen as overloads by Java. To expose overloaded constructors to Java, annotate the constructor with `@JvmOverloads`, which generates the overload set. Secondary constructors, by contrast, are real distinct JVM constructors visible to Java directly. ## Decision summary | Need | Best tool | |------|-----------| | Optional args, same shape | Default parameter values | | Named construction, may fail, return subtype/cache | Factory function | | Different parameter types / framework signatures / body logic | Secondary constructor | | Java-visible overloads of one primary | `@JvmOverloads` |

  • Why can a factory function do things a constructor cannot?
    A factory can have a name, return a subtype or a cached instance, and signal failure by returning null/Result. A constructor must return a brand-new instance of exactly its own type.
  • How do you expose Kotlin default-parameter combinations as overloaded constructors to Java?
    Annotate the constructor with @JvmOverloads; the compiler generates the overload set. Default values alone are invisible as overloads to Java callers.

saying these in an interview costs you the question

  • Always reaching for secondary constructors where a default parameter would do
  • Claiming Java automatically sees Kotlin default parameters as overloaded constructors
  • Thinking a constructor can return null or a subtype
  • Ignoring framework/Java interop reasons that legitimately require secondary constructors

context