Beyond the Law of Instrument
My Framework for Thoughtful Learning in Programming
Senior Android Developer with 6+ years of experience in Java and Kotlin development, specialising in high-performance applications, CI/CD optimisation, and cross-platform solutions. Proven ability to drive impactful business outcomes, including enhancing Porsche app performance, optimising e-commerce platforms for millions of users, and resolving critical SDK issues that secured major client contracts.
As developers, we all have our favourite tools and techniques, which we reach for instinctively because they’ve worked well before. But sometimes, that instinct leads us straight into the Law of Instruments problem: overusing a familiar tool in situations where it isn’t the best fit.
I first explored this idea in a previous article on recognising and breaking free from this cognitive bias. But recently, a discussion in the KotlinLang Slack workspace gave me a fresh perspective. A developer, Naveen, was advised to avoid using lateinit in his code review. His reaction? Defend his decision because lateinit seemed like the natural solution to his problem.

That conversation sparked an idea: How do we train ourselves to think critically before reaching for a familiar tool? Rather than just warning against pitfalls, I wanted to share a practical framework for evaluating tools and concepts before we default to using them.
In this article, I’ll walk through:
How the Law of Instrument problem played out in Naveen’s case (the
lateinitdebate)A simple framework I use to evaluate programming concepts before committing to them
Applying this framework to
lateinit, when it makes sense and when it doesn’tBroader takeaways on becoming a more intentional developer
This isn’t about avoiding tools like lateinit, it’s about making sure we use them for the right reasons. If you’ve ever caught yourself defaulting to a tool just because it’s familiar, this approach might help you code smarter and learn more effectively. Let’s dive in.
The Law of Instrument in Action: lateinit in Kotlin
To see the Law of Instrument problem in action, let’s revisit the KotlinLang Slack discussion that sparked this article.
Naveen had written a Kotlin data class with three properties: a, b, and c. However, he wanted two instances of the class to be considered equal if only a and b matched.
Since Kotlin automatically generates an equals method for data classes based on the properties declared in the constructor, Naveen had an idea:
💡What if I remove c from the constructor and declare it as a lateinit var inside the class?
This way, Kotlin would ignore c when generating equals, achieving his goal. Problem solved, right?
Not quite.
Why This Approach Is Problematic
When Naveen’s code was reviewed, others pointed out issues with this approach:
❌ Misuse of lateinit – lateinit is designed for situations where a property is guaranteed to be initialized before use (e.g., dependency injection, test setup). Here, it’s being used as a workaround to modify equality logic.
❌ Hidden side effects – Since c is no longer in the constructor, it must be set manually later. This could lead to uninitialized access issues or unexpected behaviour.
A Classic Law of Instrument Moment
Naveen wasn’t wrong to think creatively, he just defaulted to a familiar tool (lateinit) without fully evaluating whether it was the right fit. That’s the Law of Instrument problem in action.
So, what should he have done instead? More importantly, how can we train ourselves to avoid falling into this trap?
That’s where my learning framework comes in. Let’s dive into it next.
My Framework for Avoiding the Law of Instrument Trap
When faced with a new programming concept or tool, it’s easy to reach for it instinctively, especially if it has worked well in the past. But to use a tool effectively, you must first understand it deeply. That’s where my learning framework comes in.
Every well-designed programming tool or concept has three essential parts:
1. Object – What Is Its Single Responsibility?
This aligns with the Single Responsibility Principle. Every programming tool or concept is designed for one specific purpose. To determine if it’s a good fit for your problem, you must first identify:
What is the core problem this tool/concept is meant to solve?
If your problem doesn’t align with the tool’s responsibility, you’re likely misusing it. Let’s look at some examples:
lateinitin Kotlin → To mark a property whose value is unknown at the time of instantiation but cannot be null and has no meaningful default value.lazyin Kotlin → To defer the initialization of a property until the first time it is accessed, ensuring that it is only computed when needed.LiveData in Android → To hold observable, lifecycle-aware data, ensuring UI components only react to updates when they are active.
Now, let’s apply this to Naveen’s case:
Problem requirement: Define equality for a data class using only two of its three properties.
lateinit’s single responsibility: Deferring initialization for non-nullable properties.
🚨 Mismatch detected! Naveen’s use case had nothing to do with deferred initialization. Instead, he was using lateinit to work around Kotlin’s default equals behaviour.
Once you’ve identified whether a tool aligns with your problem, the next step is understanding how it works. Knowing a tool’s responsibility isn’t enough, missing it often comes from misunderstanding its mechanics.
This brings us to the second pillar of the framework.
2. Mechanics – How Does It Work?
The mechanics refer to the configuration, syntax, and operational rules of a tool or concept. In other words, what needs to be done in your code and environment for the tool to function correctly?
For lateinit, the mechanics include:
The property must be declared as a
var.The property type must be non-nullable.
The
lateinitkeyword must be used in the declaration.The property must be initialized before the first access to avoid an
IllegalStateException.
How do you master the mechanics?
Read the official documentation thoroughly.
Experiment in different environments.
Watch out for compilation errors or runtime exceptions.
Getting the mechanics wrong typically results in:
⚠️ Compilation and build errors.
⚠️ Runtime crashes (IllegalStateException).
Mastering the mechanics ensures that your code runs without errors. But just because something works syntactically doesn’t mean it’s being used correctly. This is where semantics come in. Even if you follow all the rules for using a tool, how does it impact your code, design, and maintainability?
Let’s explore the final pillar of the framework.
3. Semantics – What Are the Implications of Using It?
Semantics is all about understanding the deeper implications of using a concept. Even if the mechanics are correct, misusing a tool can lead to logic bugs, subtle errors, and unintended side effects.
For lateinit, let's break it down into three perspectives:
a. From the perspective of client code (callers of the class)
Ideally, a
lateinitproperty should be private and invisible for reading externally.If a
lateinitproperty is public, it must have a value at all times, otherwise, any read access can lead to runtime crashes. So, to client code, alateinitproperty cannot be read.If you need a property to be unset initially, it’s often better to make it nullable instead of using
lateinit.
b. From the perspective of the compiler
Normally, Kotlin disallows non-nullable properties without an initializer.
Using
lateinittells the compiler to relax this rule, shifting the responsibility to the developer.If the developer forgets to initialize the property before its first use, the program will crash at runtime.
c. From the perspective of the runtime
At runtime, a
lateinitproperty behaves like a regular var property, it can be modified at any time.The runtime does not track whether the property has been initialized before use.
Common pitfalls when semantics are misunderstood:
⚠️ Logic bugs due to incorrect assumptions about property initialization.
⚠️ Unexpected crashes if the property is accessed before being set.
⚠️ Unclear maintainability, making it harder for other developers to understand the code.
Applying the Framework to lateinit Use Cases
Now, let’s apply this framework to lateinit itself.
When lateinit Makes Sense
✅ Dependency injection – When an object is injected after class instantiation (e.g., with Dagger or Koin).
✅ Unit tests – When setting up variables before each test without requiring nullable types.
✅ View bindings in Android – When UI components are only available after onCreateView but must be non-null when used.
When lateinit Is a Red Flag
❌ For primitive types – lateinit only works with non-nullable reference types.
❌ To bypass constructor initialization – If a property is essential to an object, it should be initialized in the constructor.
❌ For optional values – If a property may not always have a value, a nullable type (?) is usually a better choice.
Conclusion & Takeaways
The Law of Instrument problem happens when we default to familiar tools without critically evaluating them. The key to avoiding it is understanding a concept fully before using it.
💡 Master the three pillars: Object, Mechanics, and Semantics.
💡 Problem-driven development beats tool-driven development – Instead of forcing a tool to fit your needs, start by clearly defining the problem first.
💡 Exploring alternatives helps you grow as a developer – The best solution is often not the first one that comes to mind.
💡 Best practices exist for a reason – If your approach goes against community recommendations, it’s worth reconsidering.
Next time you encounter a new tool or concept, run it through this framework:
✔ What is its single responsibility?
✔ What are its mechanics?
✔ What are its semantic implications?
This mindset will help you learn faster, write better code, and avoid the Law of Instrument trap. 🚀

