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 uselateinit= 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 neededlateinit= you or some external code must set it later
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, orDouble
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 lazywhen the object knows how to create its own value. - Use
lateinitwhen 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
val ex = Example()creates theExampleobject.messageis not initialized yet.println("Created object")prints normally.- First access to
ex.messagetriggers thelazyblock. Initializing messageis printed.- The block returns
"Hello". - Kotlin stores that value.
Hellois printed.- Second access to
ex.messagedoes not run the block again. - 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
val ex = Example()creates the object.messageexists as a property, but no value is assigned yet.ex.message = "Hello"assigns the value.println(ex.message)reads the assigned value and printsHello.
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.
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
| Feature | by lazy | lateinit |
|---|---|---|
| Initialization happens | Automatically on first access | Manually before first use |
| Typical property type | val | var |
| Nullable required? | No | No |
| Can fail if accessed too early? | No, it initializes itself | Yes |
| Reassignable? | No, usually read-only | Yes |
| Primitive types allowed? | Yes, depending on the property type | No |
| Initialization owner | The property itself | External code or later setup |
| Best for | Expensive or optional computation | DI, tests, lifecycle setup |
lateinit vs nullable property
| Choice | Meaning | Trade-off |
|---|---|---|
lateinit var name: String | Will be assigned later | Runtime error if used too early |
var name: String? = null | May genuinely be absent | Requires 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
| Choice | Behavior |
|---|---|
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.
Cheat Sheet
Quick rules
- Use
by lazywhen a value should be created automatically on first use. - Use
lateinitwhen 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
nulla valid state? → nullable property
Common edge cases
lazydoes not recompute on every access.lateinitthrows if used too early.lateinitcannot be used withInt,Boolean,Long, etc.lateinitis 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.
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
- Create a
Loggerclass and aUserRepositoryclass. - In a service class, use
by lazyfor the logger. - In the same service class, use
lateinitfor the repository. - Assign the repository after creating the service object.
- Call a method that uses both properties correctly.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.