Java vs Kotlin in rel projects
A practical Java vs Kotlin guide based on real-world projects: syntax, performance, best practices, and how to decide which one to use in each scenario.

When someone compares Java vs Kotlin, they almost always run into feature tables and synthetic benchmarks. But in real-world projects, what truly matters is:
- Which language lets you deliver faster.
- Which one gives you fewer production scares.
- How it affects the team and long-term maintainability of the codebase.
In my case, I come from many years working with Java in all kinds of projects, and for several years now I’ve also been maintaining and developing Kotlin code in parallel. That means living daily with hybrid repositories, legacy Java modules, and new functionality already written in Kotlin. What you’re about to read comes from that constant contrast.
Big picture: why Java vs Kotlin is not a theoretical debate
More than choosing an absolute winner in Java vs Kotlin, the practical question is usually:
- What should I keep in Java because it works and is stable?
- Where does it make sense to introduce Kotlin to gain productivity?
- How do I manage a hybrid codebase without driving the team crazy?
The key point: Kotlin lives in the same JVM ecosystem as Java. It integrates with the same libraries and frameworks, and you can mix both languages in the same project. That lets you think in terms of strategy instead of a “total rewrite.”
Kotlin vs Java syntax in day-to-day work
Talking about Kotlin vs Java syntax is not an aesthetic discussion. It directly affects:
- How long it takes to implement a feature.
- How clear pull requests are.
- How easy refactoring becomes.
Data classes vs verbose POJOs
Typical simple model example:
Java
public class User {
private final Long id;
private String name;
private String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
// typical equals
}
@Override
public int hashCode() {
// typical hashCode
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
}Kotlin
data class User(
val id: Long,
var name: String,
var email: String
)In large codebases, this difference multiplies across hundreds of classes. After years maintaining both, you clearly notice that:
- Kotlin drastically reduces boilerplate code.
- PR diffs are smaller and easier to review.
- Refactoring models is simpler and less error-prone.
Null safety: from “surprise” NPEs to compile-time errors
One of the most important differences in Kotlin vs Java syntax is how null is handled.
Java: NullPointerException anywhere
public String getUserEmail(User user) {
return user.getProfile().getEmail().toLowerCase();
}If user, profile, or email is null, the bug appears at runtime. You can avoid it with nested ifs or Optional, but it’s not enforced.
Kotlin: nullable types and safe operators
fun getUserEmail(user: User?): String? {
return user
?.profile
?.email
?.lowercase()
}User?forces you to think about whether the user can benull.- The
?.operator safely chains accesses. - If something is optional, the type makes it explicit.
After several years using Kotlin alongside Java, you can clearly see how many errors move from runtime to compile time, which is critical in projects where many developers touch the same code.
Extension functions vs static utilities
In Java, it’s common to end up with *Utils classes full of static methods.
public class PriceUtils {
public static BigDecimal applyVat(BigDecimal basePrice, BigDecimal vatPercent) {
if (basePrice == null || vatPercent == null) {
return BigDecimal.ZERO;
}
BigDecimal multiplier = vatPercent
.divide(BigDecimal.valueOf(100))
.add(BigDecimal.ONE);
return basePrice.multiply(multiplier);
}
}Usage:
BigDecimal finalPrice = PriceUtils.applyVat(basePrice, vatPercent);Kotlin: extension functions
fun BigDecimal.applyVat(vatPercent: BigDecimal?): BigDecimal {
if (vatPercent == null) return this
val multiplier = vatPercent
.divide(BigDecimal(100))
.plus(BigDecimal.ONE)
return this.multiply(multiplier)
}Usage:
val finalPrice = basePrice.applyVat(vatPercent)This turns internal APIs into something closer to the domain: the code almost reads like a sentence. After working with both styles, it’s clear that Kotlin makes it easier to create small DSLs that improve business logic readability.
Kotlin vs Java performance in practice
When talking about Kotlin vs Java performance, there are two very different scenarios:
- Compilation performance.
- Runtime performance.
Compilation: fast builds vs powerful features
In large projects, especially with many modules and annotations, what you notice day to day is:
- Java tends to compile slightly faster and more consistently.
- Kotlin can introduce overhead when:
data classusage is heavy.- Coroutines and generics are used extensively.
- KAPT and annotation processors come into play.
It’s not dramatic, but if you’re used to very fast Java builds, you’ll notice it when introducing Kotlin without taking structure into account.
An approach that has worked well for me:
- Very stable “core” modules: keep them in Java if build time is a bottleneck.
- Business logic modules that change frequently: Kotlin usually pays off in productivity.
Runtime performance: the JVM is in charge
In terms of CPU and memory, in most enterprise applications there’s no significant difference between equivalent Java and Kotlin code:
- Both generate similar JVM bytecode.
- The real bottlenecks are usually:
- Database access.
- Calls to other services.
- Architectural design.
In performance-critical scenarios, the strategy that has worked best for me is:
- Write the logic normally in Kotlin (for productivity).
- Do real profiling under load.
- Optimize only the hotspots, even using more “classic Java-style” approaches when needed.
Concurrency: threads and futures vs coroutines
One of the most practical differences in Java vs Kotlin is how you model asynchronous work.
Java: ExecutorService and CompletableFuture
ExecutorService executor = Executors.newFixedThreadPool(10);
public CompletableFuture<OrderSummary> buildOrderSummary(Long orderId) {
return CompletableFuture.supplyAsync(() -> loadOrder(orderId), executor)
.thenCombineAsync(
CompletableFuture.supplyAsync(() -> loadOrderLines(orderId), executor),
(order, lines) -> new OrderSummary(order, lines),
executor
);
}- Powerful, but composing futures becomes verbose.
- Exception handling requires careful attention.
Kotlin with coroutines:
suspend fun buildOrderSummary(orderId: Long): OrderSummary = coroutineScope {
val orderDeferred = async { loadOrder(orderId) }
val linesDeferred = async { loadOrderLines(orderId) }
OrderSummary(
order = orderDeferred.await(),
lines = linesDeferred.await()
)
}- The code looks almost synchronous.
suspendandcoroutineScopemake the flow explicit.- Composing concurrent work is far more readable.
After years maintaining Java services and then introducing Kotlin coroutines, it feels like moving from “fighting the concurrency API” to “expressing the logic you actually want.”
Best practices for mixed Java and Kotlin codebases
In real projects, the most common scenario is not “100% Java” vs “100% Kotlin”, but a hybrid codebase. That’s where Java and Kotlin best practices really matter.
Separate by stability and domain
A very effective approach is to organize modules based on:
- Code stability:
- Very stable core: can remain in Java without issues.
- Frequently changing business layers: Kotlin provides agility.
- Proximity to the domain:
- Code that models business concepts → Kotlin fits especially well (data classes, sealed classes, DSLs).
Gradual migration strategy
Instead of rewriting everything, a strategy I’ve applied in several projects is:
- Keep existing Java code as is.
- Write new functionality in Kotlin when possible.
- Migrate to Kotlin only the Java classes where maintenance is painful:
- Lots of business logic.
- Recurring bugs.
- Frequent refactors.
This avoids the shock of a full rewrite and lets the team adopt Kotlin gradually.
Writing tests in Kotlin for mostly Java projects
A very practical, low-risk way to introduce Kotlin is to start with tests:
- This is where Kotlin’s concise syntax really shines:
- Builders for test data.
- Given/when/then DSLs.
- Extension functions for assertions.
Example: Kotlin test over Java code
class OrderServiceTest {
private val repository = InMemoryOrderRepository()
private val service = OrderService(repository)
@Test
fun `should create order with lines`() {
val request = orderRequest {
customerId = 123L
line {
productId = 1L
quantity = 2
}
line {
productId = 2L
quantity = 1
}
}
val order = service.createOrder(request)
assertThat(order.lines).hasSize(2)
assertThat(order.customerId).isEqualTo(123L)
}
}
fun orderRequest(block: OrderRequestBuilder.() -> Unit): OrderRequest =
OrderRequestBuilder().apply(block).build()
class OrderRequestBuilder {
var customerId: Long = 0
private val lines = mutableListOf<OrderLineRequest>()
fun line(block: OrderLineRequest.() -> Unit) {
lines += OrderLineRequest().apply(block)
}
fun build() = OrderRequest(customerId, lines)
}This kind of test DSL feels much more natural in Kotlin than in Java, and it works as a gentle entry point for the team.
Typical scenarios: Android, backend, and internal libraries
Android: Kotlin as the de facto standard
On Android, the Java vs Kotlin debate is mostly settled:
- Most examples, documentation, and modern libraries are Kotlin-first.
- Coroutines and
Flowintegrate extremely well with modern UI and state handling.
If I were starting an Android app from scratch today, I wouldn’t hesitate: Kotlin would be the foundation, with Java only for legacy SDKs or older libraries.
Backend: Spring, Ktor, and friends
On the backend, the picture is more balanced:
- If you have a large, long-lived Java + Spring platform, there’s no rush to migrate everything.
- But Kotlin fits extremely well in:
- New Spring Boot services.
- Lightweight services with Ktor or similar frameworks.
- Domain layers and DTOs.
REST controller example
Java (Spring)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService service;
public OrderController(OrderService service) {
this.service = service;
}
@GetMapping("/{id}")
public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) {
return service.findById(id)
.map(order -> ResponseEntity.ok(toDto(order)))
.orElse(ResponseEntity.notFound().build());
}
}Kotlin (Spring)
@RestController
@RequestMapping("/api/orders")
class OrderController(
private val service: OrderService
) {
@GetMapping("/{id}")
fun getOrder(@PathVariable id: Long): ResponseEntity<OrderDto> =
service.findById(id)
?.let { ResponseEntity.ok(it.toDto()) }
?: ResponseEntity.notFound().build()
}
fun Order.toDto() = OrderDto(
id = id,
customerId = customerId,
total = total,
lines = lines.map { it.toDto() }
)
fun OrderLine.toDto() = OrderLineDto(
productId = productId,
quantity = quantity,
price = price
)Same pattern, but with less cognitive overhead and heavy use of extensions.
Internal libraries and SDKs
For very low-level libraries or internal SDKs consumed by many Java projects, it often still makes sense to:
- Stick with Java for compatibility and predictability.
- Use Kotlin in the business code that consumes those libraries.
Practical checklist: Java, Kotlin, or both?
When deciding between Java vs Kotlin in a real project, this checklist helps:
- Do you have a large, stable Java codebase?
- Yes → start with Kotlin in new modules and tests; don’t rewrite everything.
- No → Kotlin is a very strong option as a primary language.
- What’s the team like?
- Very classic Java profile, little time for training → gentle hybrid approach.
- Mixed profile, eager to modernize → stronger Kotlin adoption.
- What matters more right now: delivery speed or extreme stability?
- Fast feature delivery → Kotlin usually boosts productivity.
- Extremely mature, critical systems → Java remains the conservative choice.
- What kind of project is it?
- Android → Kotlin by default.
- Enterprise backend → hybrid is often the most reasonable choice.
- Internal libraries consumed by many projects → Java can remain the backbone.
So… which one should you choose?
After years maintaining Java projects, Kotlin projects, and many hybrids, the most honest conclusion is that the Java vs Kotlin debate is rarely solved with an all-or-nothing answer.
- Java remains robust, stable, widely understood, and backed by excellent tooling.
- Kotlin brings more expressive syntax, null safety, coroutines, and an overall more pleasant developer experience.
The good news is that they coexist perfectly well in the same project. The practical decision comes down to:
- Using Java where you already have a solid, stable base.
- Introducing Kotlin where change velocity and business logic readability matter most.
- Designing a gradual strategy that considers people as much as technology.