Why Should Your Company Consider Switching from Java to Kotlin
Kotlin is steadily gaining popularity. It’s the preferred language for Android development and is now making its way into web applications as well. While gradually replacing Java, Kotlin builds on Java’s established ecosystem. Created as a modern alternative, Kotlin addresses many of the limitations that come from Java’s focus on backward compatibility — though Java itself has seen faster evolution in recent years. In this blog post, we’ll explore why your company should consider switching to Kotlin, especially for web development projects.
Java's dominance
Java was invented almost 30 years ago. For many years, this mature language was considered the best choice for many sophisticated enterprise computer systems. Java was known as a very stable language, not only in terms of reliability, but also in terms of the pace of development. This stability could be seen as an advantage, especially for the enterprise use cases where Java excels — after all, it's probably not a good idea to experiment with new fancy features in production systems that generate millions of dollars for a company.
However, in the rapidly evolving field of software development, even well-established languages need to adapt. Those that do not evolve risk being left behind. Fortunately, the Java community has noticed and introduced some cool new features, such as immutable collections in Java 9 , records in Java 16, and pattern matching in Java 21. Despite these advances, Java still lags some of its competitors. More importantly, these new features were introduced on top of old foundations that are not quite compatible with them, so they don't work exactly as they should. As a result, we have a language that is trying to catch up with the competition but has inherent limitations due to some of the choices made during its original design.
OK, so maybe Java lags behind and doesn't have all these trendy new features, so what? This is just a matter of programmer preference, like the color of your shoes - nothing important, right? Wrong! It's very important from a business point of view, and I'll show you why in the rest of this article.
Billion-dollar mistake
The billion-dollar mistake is a concept introduced by Tony Hoare in 2009 [5]. Hoare regretted his invention of the null reference, later admitting it was a mistake because it led to loads of errors, crashes and vulnerabilities in software systems. The creators of Java incorporated the same mistake into their language design. This concept is a fundamental problem for Java, as all references in the language can be null by default. It is important to understand that this is a serious security issue. Studies show that 20-40% of exceptions in production environments are caused by NullPointerExceptions. That’s a lot of money wasted due to increased debugging, maintenance costs, and potential service downtime!
Efforts have been made to mitigate this problem, but all the solutions in use are incomplete. In fact, every few years, there's a new brilliant idea to deal with this problem, and it's the source of many arguments. In the end, it's not clear how we should deal with nulls in Java - there are so many ways, and none of them works well:
- Trust that a given reference will never be null (even though it might be).
- Check if the reference is null before every use.
- Use Jakarta's @Nonnull-like annotation for documentation purposes.
- Use Jakarta's @NotNull-like annotation for runtime checking.
- Use the Java 8’s Optional type.
- Keeping my fingers crossed and waiting for Null-Restricted and Nullable Types.
- Come up with yet another approach to make this list even longer.
The first, is of course, a source of many NullPointerException
bugs, as such assumptions are far from reality. The second clutters the code with numerous if
statements, which not only affects readability and maintainability but, in some cases, can introduce potential race conditions.
The third and fourth options seem better, but adding these annotations throughout the code is far from ideal. Developers often forget to use them, and those who do usually apply them only to public interfaces or package facades. Internal code is often ignored, which still leaves the potential for NullPointerException. On top of that, the third option is just a documentation tool — if you pass a null as @Nonnull parameter, the IDE can only show a warning if it's smart enough. The fourth option seems better, as it throws an exception when a null safety violation occurs. However, it's only a runtime check, so its main purpose is to prevent propagation by catching the problem earlier.
The fifth proposition came to the Java world from functional programming. It's just a type that wraps a reference, and its purpose is to explicitly indicate that an object can sometimes be empty
(another word for null
). So, it doesn't force the programmer to do anything at the syntax level, it's just another assumption.
The sixth proposition appears even more promising. It introduces syntax-level control over reference type nullability through !
and ?
markers, similar to Kotlin's approach (which will be covered in the next section). While it's currently a preview feature, the main limitation is that it won't modify existing library code — meaning it would only cover a fraction of our codebase if implemented.
To summarize, yes, Java has tools for null-safety, but none of them are perfect, and there's no single standard approach. Well, actually, there is one standard
way: accept nulls as part of Java, since the language was inherently designed with them. So what's the real solution? Well, one option would be to simply get rid of implicitly nullable reference types at the syntax level of the language. However, this would require making disruptive changes to Java, which is virtually impossible.
Kotlin's solution to nulls
One of Kotlin's key advantages is its focus on language safety from the start. By learning from the mistakes of other languages, Kotlin avoided introducing the billion-dollar mistake into its design. Kotlin took a simpler and more elegant approach to solving the problem of nullability:
- By default, all references in Kotlin are not nullable.
- Developers must explicitly declare nullable references by adding a
?
(question mark) to the end of the reference type. - A null check must be performed before dereferencing a reference to a nullable type.
This design is advantageous because:
- It eliminates the need to check nullability for standard (non-null) references.
- If nullability is required, developers make a conscious decision to allow it by adding the question mark symbol (
?
) at the end of the type. - There is a very limited way to reference a null instead of an object—the programmer must use special (
!!
) syntax, which is discouraged.
This approach is more elegant, easier, and safer than any other approach in Java. This design choice has saved many Kotlin applications from NullPointerException
, making the language safer and applications more robust. As a result, Kotlin has become an excellent choice for enterprise applications.
Immutability
Immutable collections are a key concept in providing overall language security. In most cases, collections that are passed to or returned by another part of the system are not intended to be modified. Allowing them to be modified not only can break encapsulation but is also a source of unexpected state changes that weren’t handled by the original author’s code. This can lead to many hard-to-detect bugs.
While Java 9 introduced support for immutable collections, it did so by providing simple wrappers on top of plain old mutable collections that throw an exception on write operations. This may be sufficient to provide encapsulation and prevent unexpected state changes, but it introduces another problem by breaking the Liskov Substitution Principle. In other words, this approach introduces types (immutable collections) that do not properly implement the methods (write operations) of the parent type (mutable collections). This incompatibility can lead to further errors and confusion. As with null security, this non-ideal (or even 'dirty') approach was taken to introduce this security feature without introducing breaking changes.
Kotlin has built-in support for immutable collections. This is done by properly rebuilding the collection hierarchy so that mutable types inherit from immutable types, so that the Liskov Substitution Principle is not broken. Also, collections, like other things in Kotlin, are immutable by default. In most cases, you have to add an extra word (mutable
) if you want to deal with mutable collections. This approach further discourages developers from using mutable collections, as they aren't needed in most cases. This improves code security by preventing unintentional changes to data, ultimately reducing debugging time and maintenance costs for businesses.
Kotlin’s syntax is more effective
Kotlin is known for its concise syntax and numerous built-in features, often resulting in code that is 20-40% shorter than equivalent Java code. This conciseness makes the language easier to write and analyze. It also enables earlier detection of certain types of errors. Furthermore, Kotlin programmers can implement the same functionality significantly faster than Java developers. While some companies report up to a 40% improvement in development speed, this figure is likely optimistic; a more realistic estimate would range between 10% and 30%.
Kotlin is also famous for its built-in functions inspired by functional languages (e.g. fold
, reduce
, associate
, groupBy
, zipWithNext
). They allow developers to do a lot in a few lines of code, effectively allowing software to be implemented faster. While most of them exist in Java or in libraries like Guava, they are certainly not as concise and fun to use as in Kotlin, which somewhat discourages programmers from using them.
While Kotlin is more concise than Java, its syntax is slightly more complex and requires some familiarity with new concepts. The code looks clean at first glance, but digging into what each piece actually does takes some work. Lambda receivers and extension functions can seem a bit exotic to Java developers. Sure, these features are great for building DSLs (just look at what Gradle does with them), but they can make your code feel like a puzzle at first. While conciseness can be tempting, using overly compact syntax can hinder readability — it’s recommended to avoid one-line hacks to maintain clear, understandable code. The good news? Once you get past the initial confusion, Kotlin's shortcuts and enhanced standard library will help you ship code faster.
Functional programming
The IT world is gradually moving towards functional programming (FP) and away from a pure object-oriented (OO) approach. The FP paradigm offers immutability, lack of side effects, and predictability, making code safer, easier to analyze, and better suited for concurrent execution. Pure OO applications, especially those that rely heavily on inheritance to extend functionality, often become complex and difficult to maintain over time. On the other hand, building systems purely in the FP paradigm can be more challenging, especially since most developers have been taught primarily in the OOP style. This makes the idea of a hybrid approach that combines the best of both paradigms more attractive. Kotlin strikes an ideal balance in this trend, with a syntax that allows developers to write FP-style code in an OOP environment, or stick to pure OOP when FP isn't appropriate.
Kotlin has both OOP and FP constructs, so you can mix both styles or choose one over the other if you prefer. Function types, extension functions, immutability, and null-safety make Kotlin an excellent choice for FP-like programming. You can use sealed classes and pattern matching to model the success or failure of a function, or just rely on exceptions if you like (although in most cases it's better not to). You can implement a function using a chain of functions, or use loops, ifs and variables if that makes the code cleaner (but probably not). You can use higher-order functions, or stick to strategy/command/template method patterns (though your colleagues might not be thrilled). The key point is that Kotlin gives you options, but this can also be a curse because you need to agree with your team on which approach to take.
Let's be honest - Java isn't sexy these days. Not only are companies turning to functional programming for its technical benefits, but it also helps attract talented developers to your company. These developers bring real business value through faster delivery, better code quality, and more innovative solutions. For example, when Amazon Prime introduced Kotlin, they saw both higher developer satisfaction and improved productivity.
Kotlin is compatible with Java
Kotlin runs on the Java Virtual Machine (JVM), which allows you to use a wide range of Java tools and libraries, including Spring. This also means that you can have both Kotlin files and Java classes in the same project. This eases the transition to Kotlin for many projects and teams. JetBrains also provides tools in its IDE to convert Java code to Kotlin, so Kotlin programmers can still easily incorporate code snippets from StackOverflow into their projects 😉. While the automatic conversion requires some manual refinement, especially to take full advantage of Kotlin's features like its handling of nullability, it provides a solid starting point for migration. The main point is that bringing Kotlin to your Java team isn't a revolution: the ecosystem remains the same, and all existing tools can still be used by your team, making the transition both safe and smooth.
How popular is Kotlin, exactly?
Most companies have already switched from Java to Kotlin for their Android projects, primarily for the reasons mentioned above, but also due to Google’s strong support for this transition. While Kotlin is an excellent choice for backend web application development, it hasn’t gained as much traction in this area. However, case studies from companies such as Amazon, Zalando, DoorDash and Revolut demonstrate that Kotlin is also a great choice for backend development.
Looking at job market data, it’s difficult to gauge Kotlin’s current popularity because Kotlin roles are typically paired with Java. In reality, many Kotlin backend services are developed by former Java developers. This is good news because, if you choose Kotlin, your company can still rely on one of the largest groups of software engineers in the job market. However, these Java developers would likely need some training before diving deep into Kotlin, especially if they lack functional programming experience. Fortunately, most good Java developers do have such experience in the FP paradigm.
Why not?
Kotlin has several advantages, but like any other programming language, it also has some drawbacks. The main business-related downside of Kotlin is that it is lesser known than Java, especially in web development. Kotlin's popularity shines mainly in mobile applications. However, viewing mobile as Kotlin's only domain is a mistake. Still — lower popularity means that there are fewer programmers on the job market. This also raises a question about the language's long-term viability — is it safe to use a less widely adopted language for projects that need to be maintained for decades? Although Kotlin is supported by JetBrains and Google, so sudden abandonment is not anticipated — at least not due to JetBrains 😉
On the technical side, Kotlin is a great, though not perfect, language as it lacks a few features. Kotlin has pattern matching, but it isn't fully-fledged — destructuring and complex expressions are not supported in pattern matching. Even Java has a more advanced pattern matching syntax that was introduced in Java 21 (check this out in our blog post). Secondly, the default accessor in Kotlin is public — this means that every class, function, or property is public by default — this may lead to bad practices, because we should rather limit access by default and open it only if needed. Kotlin also doesn't have a concept like package-private that's known from Java, so creating public facades to hide internal package details is not supported by Kotlin's syntax. There are also some other minor flaws, but that's a topic for another blog post — stay tuned, we have a plan to publish it in the future. The main point is that we do not consider Kotlin as a perfect language (there is no perfect language), but it's a very good one, probably one of the best choices for many use cases.
Conclusion
Kotlin offers several advantages over Java, including enhanced language security, faster development time, and better alignment with modern programming paradigms. Transitioning from Java to Kotlin doesn’t require extensive retraining for the team, as Kotlin remains part of the Java ecosystem. These factors make Kotlin an appealing choice for both new projects and teams seeking to modernize their existing Java codebases. However, Kotlin’s relative immaturity compared to Java introduces some risk, particularly if the team lacks trained Kotlin developers or plans to rely on Kotlin-specific tools that are not yet battle-tested, rather than established Java libraries.
At SoftwareMill, based on our experience, we believe that Kotlin is mature enough to be successfully used even in large backend projects. We understand the business impact of choosing the right tools. If you’re ready to unlock the potential of Kotlin for your organization, let’s connect. We are also well-known for our expertise in Scala — another JVM-based language that shares many of the benefits presented in this article.
So, are you still going to use Java and risk a billion-dollar mistake? 😉
🎵Build a safer future – with Kotlin! 🎵
Reviewed by: Rafał Maciak, Sebastian Rabiej