Question

Kotlin Property Initialization: by lazy vs lateinit

kotlinproperties

Question

In Kotlin, if you do not want to initialize a class property in the constructor or at the top of the class body, two common options are by lazy and lateinit.

lazy() returns a delegate that initializes the value only on first access and then keeps that value for future reads:

class Hello {
    val myLazyString: String by lazy { "Hello" }
}

In this case, the first access to myLazyString computes the value, and every later access returns the same stored result.

Kotlin also supports late initialization with lateinit:

class MyTest {
    lateinit var subject: TestSubject

    @SetUp
    fun setup() {
        subject = TestSubject()
    }

    @Test
    fun test() {
        subject.method()
    }
}

lateinit is useful when a non-null property cannot be initialized immediately, such as in dependency injection or test setup. It can only be used on var properties declared in the class body, with non-null and non-primitive types, and without custom getters or setters.

How should you choose correctly between by lazy and lateinit in Kotlin, especially since both can appear to solve a delayed initialization problem?

Short Answer

By the end of this page, you will understand the difference between by lazy and lateinit in Kotlin, when each should be used, what rules apply to each one, and how to avoid common mistakes. You will also see practical examples from Android apps, tests, and general Kotlin code.

Concept

by lazy and lateinit both delay initialization, but they solve different kinds of problems.

by lazy means:

  • the value is computed automatically the first time you use it
  • the property is usually a val, so it is read-only after initialization
  • Kotlin manages the initialization for you
  • after the first access, the same stored value is returned

Example:

val config by lazy { loadConfigFromFile() }

This is ideal when:

  • the value is expensive to create
  • the value may never be needed
  • the value should be created once and reused
  • the object can define its own initialization logic

lateinit means:

  • the value is not created automatically
  • you promise Kotlin that you will assign it before using it
  • the property must be a var
  • if you access it too early, Kotlin throws UninitializedPropertyAccessException

Example:

lateinit var repository: UserRepository

This is ideal when:

  • another part of the program will assign the value later
  • setup happens after object creation
  • dependency injection, framework lifecycle, or test setup controls initialization

So the key difference is this:

  • lazy = initialize on first use
  • lateinit = initialize sometime later before use

Why this matters in real programming:

  • It helps you avoid unnecessary object creation.
  • It helps you keep properties non-null without making everything nullable.
  • It makes lifecycle-based code cleaner, especially in tests and frameworks.
  • It communicates intent clearly to other developers reading your code.

Mental Model

Think of these two features as two different ways to get a tool you need.

by lazy

You keep the tool in a sealed box with instructions attached. The first time you need the tool, the box opens, builds the tool, and hands it to you. After that, the same tool is reused every time.

lateinit

You leave an empty space on the shelf and say, "Someone will place the tool here before I need it." Kotlin trusts you. If you reach for it before someone puts it there, you get an error.

So:

  • lazy = Kotlin builds it when needed
  • lateinit = you or some external code must set it later
Take Quiz

Syntax and Examples

Core syntax

by lazy

val name: String by lazy {
    "Kotlin"
}
  • Works with val
  • Runs the lambda once on first access
  • Stores the result

lateinit

lateinit var name: String
  • Works only with var
  • Must be assigned before use
  • Cannot be primitive types like Int, Boolean, or Double

Example: use by lazy for computed setup

class DatabaseClient {
    val connectionString by lazy {
        println("Building connection string...")
        "postgres://localhost:5432/app"
    }
}

fun main() {
    val client = DatabaseClient()
    println("Object created")
    println(client.connectionString)
    println(client.connectionString)
}

Output:

Object created
Building connection string...
postgres://localhost:5432/app
postgres://localhost:5432/app

The message appears only once because the value is created only on first access.

Example: use lateinit for setup after object creation

class UserService {
    lateinit var repository: UserRepository

    fun printUserCount() {
        println(repository.countUsers())
    }
}

class UserRepository {
    fun countUsers(): Int = 42
}

fun main() {
    val service = UserService()
    service.repository = UserRepository()
    service.printUserCount()
}

Here, repository is supplied later. This is common when another part of the system wires dependencies together.

Simple rule of thumb

  • Use by lazy when the object knows how to create its own value.
  • Use lateinit when the value will be assigned from outside.

Step by Step Execution

Trace example with by lazy

class Example {
    val message by lazy {
        println("Initializing message")
        "Hello"
    }
}

fun main() {
    val ex = Example()
    println("Created object")
    println(ex.message)
    println(ex.message)
}

What happens step by step

  1. val ex = Example() creates the Example object.
  2. message is not initialized yet.
  3. println("Created object") prints normally.
  4. First access to ex.message triggers the lazy block.
  5. Initializing message is printed.
  6. The block returns "Hello".
  7. Kotlin stores that value.
  8. Hello is printed.
  9. Second access to ex.message does not run the block again.
  10. Kotlin returns the stored "Hello" directly.

Trace example with lateinit

class Example {
    lateinit var message: String
}

fun main() {
    val ex = Example()
    ex.message = "Hello"
    println(ex.message)
}

What happens step by step

  1. val ex = Example() creates the object.
  2. message exists as a property, but no value is assigned yet.
  3. ex.message = "Hello" assigns the value.
  4. println(ex.message) reads the assigned value and prints Hello.

If you try to read ex.message before assignment:

val ex = Example()
println(ex.message)

Kotlin throws an UninitializedPropertyAccessException.

Real World Use Cases

When by lazy is useful

Expensive objects

val parser by lazy { JsonParser() }

Create the object only if it is actually needed.

Configuration loading

val config by lazy { loadConfig() }

Useful in CLI tools, backend apps, or desktop apps where startup cost matters.

Cached values

val fullName by lazy { "$firstName $lastName" }

Good when the value should be calculated once and reused.

Android view or resource setup

Historically, lazy has often been used for values that should be created only when first used.

When lateinit is useful

Dependency injection

lateinit var apiClient: ApiClient

A framework or container may assign this after object creation.

Unit tests

lateinit var service: UserService

@Before
fun setUp() {
    service = UserService()
}

This avoids making the property nullable just for test setup.

Lifecycle-based initialization

Some frameworks create objects first and provide required dependencies later.

A useful distinction

  • If initialization depends on using the property, prefer lazy.
  • If initialization depends on application lifecycle or external assignment, prefer lateinit.
Take Quiz

Real Codebase Usage

In real projects, developers usually choose between these based on ownership of initialization.

Common lazy patterns

1. Deferred creation of helpers

class ReportService {
    private val formatter by lazy { ReportFormatter() }
}

The class itself owns the creation logic.

2. Cached derived values

class User(private val firstName: String, private val lastName: String) {
    val displayName by lazy { "$firstName $lastName" }
}

3. Read-only dependencies built from configuration

class AppContainer {
    val logger by lazy { Logger("app.log") }
}

Common lateinit patterns

1. Test setup

class UserServiceTest {
    lateinit var service: UserService

    @Before
    fun setup() {
        service = UserService()
    }
}

2. Framework-assigned dependencies

class UserController {
    lateinit var repository: UserRepository
}

3. Multi-step object setup

Sometimes an object must be created first, then wired later.

Guarding lateinit usage

Kotlin lets you check whether a lateinit property is initialized:

if (::repository.isInitialized) {
    println(repository.countUsers())
}

This is useful, but if you often need this check, it may be a sign that the design could be improved.

Early-return and validation patterns

When using lateinit, developers often validate setup before continuing:

fun process() {
    if (!::repository.isInitialized) return
    println(repository.countUsers())
}

This prevents runtime errors, but lazy is usually cleaner when the value can be created automatically.

Common Mistakes

1. Using lateinit when val would be better

Broken approach:

class Config {
    lateinit var settings: String
}

If the value should be created once and never changed, lazy may better express that:

class Config {
    val settings by lazy { "production" }
}

2. Accessing a lateinit property before assignment

Broken code:

class Example {
    lateinit var text: String
}

fun main() {
    val ex = Example()
    println(ex.text)
}

This throws UninitializedPropertyAccessException.

Fix it by assigning first:

val ex = Example()
ex.text = "Ready"
println(ex.text)

3. Trying to use lateinit with primitive types

Broken code:

lateinit var count: Int

This is not allowed.

Use another approach, such as:

var count: Int = 0

or nullable types when appropriate.

4. Expecting lazy to recompute every time

Broken expectation:

val time by lazy { System.currentTimeMillis() }

This runs once, not every time. If you need a fresh value on each access, use a getter:

val time: Long
    get() = System.currentTimeMillis()

5. Using lateinit to avoid proper construction

Sometimes beginners use lateinit just to skip constructor design. That can make objects invalid for part of their lifetime.

Prefer constructor injection when the dependency is always required:

class UserService(private val repository: UserRepository)

This is often safer than:

class UserService {
    lateinit var repository: UserRepository
}

6. Forgetting thread behavior with lazy

lazy is thread-safe by default, which is often helpful, but it has a cost. In some cases developers choose a different lazy mode for performance. For beginners, the main point is that lazy has built-in initialization behavior, while lateinit does not.

Comparisons

by lazy vs lateinit

Featureby lazylateinit
Initialization happensAutomatically on first accessManually before first use
Typical property typevalvar
Nullable required?NoNo
Can fail if accessed too early?No, it initializes itselfYes
Reassignable?No, usually read-onlyYes
Primitive types allowed?Yes, depending on the property typeNo
Initialization ownerThe property itselfExternal code or later setup
Best forExpensive or optional computationDI, tests, lifecycle setup

lateinit vs nullable property

ChoiceMeaningTrade-off
lateinit var name: StringWill be assigned laterRuntime error if used too early
var name: String? = nullMay genuinely be absentRequires null checks

Use nullable types when null is a real valid state. Use lateinit when the property is expected to exist before use.

lazy vs custom getter

ChoiceBehavior
val x by lazy { createValue() }Computes once, caches result
val x get() = createValue()Computes every access

If you want memoization, use lazy. If you want recalculation, use a getter.

Take Quiz

Cheat Sheet

Quick rules

  • Use by lazy when a value should be created automatically on first use.
  • Use lateinit when a value will be assigned later by other code.
  • Prefer constructor parameters when the dependency is always required.

by lazy

val value by lazy { createValue() }
  • Usually used with val
  • Runs once on first access
  • Caches the result
  • Good for expensive setup or optional values

lateinit

lateinit var value: MyType
  • Only for var
  • Only for non-null, non-primitive types
  • Must be assigned before access
  • Good for test setup and dependency injection

Check if lateinit is initialized

if (::value.isInitialized) {
    println(value)
}

Choose this way

  • Can the class create the value itself? → lazy
  • Will someone else assign it later? → lateinit
  • Is the value always required? → constructor parameter
  • Is null a valid state? → nullable property

Common edge cases

  • lazy does not recompute on every access.
  • lateinit throws if used too early.
  • lateinit cannot be used with Int, Boolean, Long, etc.
  • lateinit is not a substitute for good object construction.

FAQ

When should I use by lazy instead of lateinit in Kotlin?

Use by lazy when the property can create its own value on first access and should keep that value afterward.

When should I use lateinit in Kotlin?

Use lateinit when the property must be assigned later, usually by setup code, dependency injection, or a framework lifecycle.

Is lateinit better than nullable properties?

Not always. Use lateinit when the property should definitely be initialized before use. Use nullable types when missing data is a real valid state.

Can lateinit be used with val?

No. lateinit only works with var because the value is assigned after object creation.

Can by lazy be used with var?

The common pattern is val by lazy. It represents one-time initialization and cached read-only access.

What happens if I access a lateinit property before setting it?

Kotlin throws UninitializedPropertyAccessException at runtime.

Does lazy run every time I access the property?

No. It runs only once, on the first access, then returns the cached result.

What is the safest default when a dependency is required?

Constructor injection is usually the safest and clearest choice when the property is always needed.

Take Quiz

Mini Project

Description

Build a small Kotlin example that shows both delayed initialization styles in one place. The program will simulate an app service that creates a logger only when needed and receives a repository later during setup. This demonstrates the real difference between self-managed initialization and externally assigned initialization.

Goal

Create a class that uses by lazy for a read-only helper object and lateinit for a dependency assigned after object creation.

Requirements

  1. Create a Logger class and a UserRepository class.
  2. In a service class, use by lazy for the logger.
  3. In the same service class, use lateinit for the repository.
  4. Assign the repository after creating the service object.
  5. Call a method that uses both properties correctly.
Take Quiz

Keep learning

Related questions

View more