skip to content

Kotlin makes non-null the default and requires opting into nullability with ?. What are the design trade-offs of this choice compared with treating every reference as nullable?

level: principalimportance: nice to knowfreq 18%

answer

  1. Safe case is the default; opt into risk with ?
  2. Type signature documents the contract
  3. Cost: platform types at Java boundary
  4. Escape hatches !!, lateinit reintroduce NPE
  5. Defaults shape behavior at scale

basics

~10 s

Making non-null the default means most values are guaranteed safe, so bugs surface at compile time. The cost is more friction at boundaries (like Java code) where the compiler can't prove nullability.

solid answer

~50 s

Non-null-by-default encodes the common case directly in the type system: most references are not meant to be null, so making T mean 'never null' means the majority of declarations need no annotation while still being NPE-safe. You opt into risk explicitly with ?, which makes nullability visible and forces handling via ?., ?:, smart casts, or !!. The benefit is that whole classes of NullPointerException become compile-time errors and APIs document their contracts in their signatures. The trade-offs: interop friction (Java references become platform types T! where the compiler relaxes checks, reintroducing NPE risk); some ceremony when a value is legitimately optional; and the temptation to escape the system with !! or lateinit, which silently reintroduce the very crashes the design prevents. Compared with all-nullable (Java's model), Kotlin shifts the default toward safety, trading a bit of boundary friction for far fewer runtime nulls.

go deeper

for a junior

Knows non-null is the default and you add ? for nullable, but not the trade-offs.

for a middle

Explains the compile-time safety benefit and that Java interop complicates it.

for a senior

Articulates platform types, !!/lateinit escape hatches, and contracts-in-signatures as concrete trade-offs.

for a principal

Frames it as a defaults-design decision, weighs boundary friction vs safety at scale, and directs review effort to where residual risk concentrates.

## The design choice Kotlin defines a bare type `T` as **non-null** and requires the explicit `?` to make it `T?` (nullable). This inverts Java, where **every** reference is implicitly nullable. The principle: make the **safe, common case the default** and require an explicit opt-in for the risky case. ## Why non-null is the right default - **Statistically common**: most references are never meant to be null, so the default needs zero annotation yet stays safe. - **Contracts in signatures**: `fun find(id: Id): User?` says 'may be absent'; `fun get(id: Id): User` says 'always present'. The type *is* the documentation, checked by the compiler. - **Compile-time NPE elimination**: dereferencing a `T?` without `?.`, `?:`, a smart cast, or `!!` is a compile error, so a large class of `NullPointerException`s never reaches runtime. - **Visible risk**: every `?` is a flag that says 'handle absence here', concentrating attention on the genuinely optional values. ## The trade-offs / costs 1. **Interop friction — platform types.** Values from Java have no nullability info, so the compiler treats them as **platform types** (`String!`) and relaxes checks. This is a pragmatic escape hatch but reopens NPE risk at the boundary; mitigations are `@Nullable`/`@NotNull` annotations on Java code and defensive checks. 2. **Escape hatches that undermine the guarantee.** `!!` asserts non-null and `lateinit var` defers initialization — both can throw at runtime, silently converting compile-time safety back into crashes if overused. 3. **Ceremony for genuinely optional values.** Deeply nullable chains can get verbose (`a?.b?.c?.d`), though `?.`, `?:`, `let`, and smart casts keep it manageable. 4. **Migration cost.** Porting Java mental models requires teams to think about which references are truly optional. ## Compared with all-nullable (Java) | Aspect | Kotlin (non-null default) | Java (all nullable) | |---|---|---| | Default safety | Safe; opt into risk | Risky; opt into checks manually | | NPE timing | Mostly compile-time | Runtime | | Annotation burden | `?` only where optional | `@Nullable`/`@NonNull` everywhere or nothing | | Boundary | Platform types blur the line | N/A (already nullable) | ## The deeper point The choice is a **defaults-design** decision: defaults shape behavior at scale. By making 'cannot be null' free and 'can be null' explicit, Kotlin biases millions of declarations toward safety while keeping a deliberate, clearly-marked escape hatch (`?`, `!!`, `lateinit`, platform types) for the cases the type system can't or shouldn't prove. The residual risk is concentrated exactly at the boundaries (`!!`, `lateinit`, Java interop), which is also where review effort should focus.

  • Where does residual NPE risk concentrate under this design, and what does that imply for code review?
    At the explicit escape hatches: !!, lateinit, and Java-interop platform types. Reviewers should scrutinize those specifically, since the compiler already proved the rest safe.
  • Why not just make everything nullable and force checks everywhere?
    That's Java's model — it pushes the burden onto every dereference and most checks would be noise, since most references are never null. Non-null default removes that noise and reserves attention for true optionality.

Like a door locked by default: safe unless you deliberately unlock it (?), versus a door unlocked by default that you must remember to lock everywhere.

saying these in an interview costs you the question

  • Claiming the design fully eliminates NPEs (platform types, !!, lateinit remain)
  • Ignoring Java interop / platform types as the main cost
  • Not recognizing the value of contracts being in signatures
  • Treating !! and lateinit as free rather than as risk-reintroducing escape hatches

context