Guest post by Urs Peter, Senior Software Engineer and JetBrains-certified Kotlin Trainer. For readers who’d like a more structured way to build Kotlin skills, Urs also leads the Kotlin Upskill Program at Xebia Academy.
This is the second post in The Ultimate Guide to Successfully Adopting Kotlin in a Java-Dominated Environment, a series that follows how Kotlin adoption grows among real teams, from a single developer’s curiosity to company-wide transformation.
Read the first part: Getting Started With Kotlin for Java Developers
Once you’re comfortable with Kotlin in tests, it’s time for a more substantial evaluation. You have two main approaches:
Starting fresh with a new application or microservice provides the full Kotlin experience without the constraints of legacy code. This approach often provides the best learning experience and showcases Kotlin’s strengths most clearly.
Pro tip: Get expert help during this stage. While developers are naturally confident in their abilities, avoiding early mistakes in the form of Java-ish Kotlin and a lack of Kotlin-powered libraries can save months of technical debt.

This is how you can avoid common pitfalls when using Kotlin from a Java background:
Pitfall: Choosing a different framework from the one you use in Java.
Tip: Stick to your existing framework.
Most likely, you were using Spring Boot with Java, so use it with Kotlin too. Spring Boot Kotlin support is first-class, so there is no additional benefit in using something else. Moreover, you are forced to learn not only a new language but also a new framework, which only adds complexity without providing any advantage.
Important: Spring interferes with Kotlin’s ‘inheritance by design’ principle, which requires you to explicitly mark classes open in order to extend them.
In order to avoid adding the open keyword to all Spring-related classes (like @Configuration, etc.), use the following build plugin: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. If you create a Spring project with the well-known online Spring initializr tool, this build plugin is already configured for you.
Pitfall: Writing Kotlin in a Java-ish way, relying on common Java APIs rather than Kotlin’s standard library:
This list can be very long, so let’s focus on the most common pitfalls:
Tip: Always use Kotlin Collections.
Kotlin Collections are fully interoperable with Java Collections, yet equipped with straightforward and feature-rich higher-order functions that make Java Stream obsolete.
As follows is an example that aims to pick the top 3 products by revenue (price * sold) grouped by product category:
Java
record Product(String name, String category, double price, int sold){}
List<Product> products = List.of(
new Product("Lollipop", "sweets", 1.2, 321),
new Product("Broccoli", "vegetable", 1.8, 5);
Map<String, List<Product>> top3RevenueByCategory =
products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingDouble(
(Product p) -> p.price() * p.sold())
.reversed())
.limit(3)
.toList()
)
)
);
Kotlin
val top3RevenueByCategory: Map<String, List<Product>> =
products.groupBy { it.category }
.mapValues { (_, list) ->
list.sortedByDescending { it.price * it.sold }.take(3)
}
Kotlin Java interop lets you work with Java classes and records as if they were native Kotlin, though you could also use a Kotlin (data) class instead.
Tip: Embrace Nullable types.
One of the key reasons Java developers switch to Kotlin is for Kotlin’s built-in nullability support, which waves NullPointerExceptions goodbye. Therefore, try to use Nullable types only, no more Optionals. Do you still have Optionals in your interfaces? This is how you easily get rid of them by converting them to Nullable types:
Kotlin
//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
//it returns Optional, but we want nullable types
fun getOrderBy(id: Long): Optional<Order> = …
}
//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? =
getOrderBy(id).orElse(null)
//Now enjoy the safety and ease of use of nullable types:
//Past:
val g = repository.getOrderBy(12).flatMap { product ->
product.goody.map { it.name }
}.orElse("No goody found")
//Future:
val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"
Tip: Embrace Extension methods.
Extension methods give you many benefits:
Java
//Very common approach in Java to add additional helper methods
public class DateUtils {
public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatted(LocalDateTime dateTime,
DateTimeFormatter formatter) {
return dateTime.format(formatter);
}
public String formatted(LocalDateTime dateTime) {
return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
}
}
//Usage
formatted(LocalDateTime.now());
Kotlin
val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
//Use an extension method, including a default argument, which omits the need for an overloaded method.
fun LocalDateTime.asString(
formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String =
this.format(formatter)
//Usage
LocalDateTime.now().formatted()
Be aware that Kotlin offers top-level methods and variables. This implies that we can simply declare e.g. the DEFAULT_DATE_TIME_FORMATTER top level without the need to bind to an object like is the case in Java.
Tip: Use Kotlin’s slick counterpart.
The Kotlin standard library uses extension methods to make Java libraries much more user-friendly, even though the underlying implementation is still Java. Almost all major third-party libraries and frameworks, like Spring, have done the same.
Example standard library:
Java
String text;
try (
var reader = new BufferedReader(
new InputStreamReader(new FileInputStream("out.txt"),
StandardCharsets.UTF_8))) {
text = reader
.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " + text + "\n");
Kotlin
//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion
val text = FileInputStream("path").use {
it.bufferedReader().readText()
}
println("Downloaded text: $text\n");
Example Spring:
Java
final var books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body( new ParameterizedTypeReference<List<Book>>(){}); // ⇦ inconvenient ParameterizedTypeReference
Kotlin
import org.springframework.web.client.body
val books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body<List<Book>>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference
Tip: Combine related public classes in a single file.
This allows you to get a good understanding of how a (sub-)domain is structured without having to navigate dozens of files.
Java

Kotlin
//For domain classes consider data classes - see why below
data class User(val email: String,
//Use nullable types for safety and expressiveness
val avatarUrl: URL? = null,
var isEmailVerified: Boolean)
data class Account(val user:User,
val address: Address,
val mfaEnabled:Boolean,
val createdAt: Instant)
data class Address(val street: String,
val city: String,
val postalCode: String)
Tip: Embrace immutability – the default in Kotlin
The trend across many programming languages – including Java – is clear: immutability is winning over mutability.
The reason is straightforward: immutability prevents unintended side effects, making code safer, more predictable, and easier to reason about. It also simplifies concurrency, since immutable data can be freely shared across threads without the risk of race conditions.
That’s why most modern languages – Kotlin among them – either emphasize immutability by default or strongly encourage it. In Kotlin, immutability is the default, though mutability remains an option when truly needed.
Here’s a quick guide to Kotlin’s immutability power pack:
1. Use val over var
Prefer val over var. IntelliJ IDEA will notify you if you used a var, for which a val could be used.
2. Use (immutable) data classes with copy(...)
For domain-related classes, use data classes with val. Kotlin data classes are often compared with Java records. Though there is some overlap, data classes offer the killer feature copy(...), whose absence makes transforming record – which is often needed in business logic – so tedious:
Java
//only immutable state
public record Person(String name, int age) {
//Lack of default parameters requires overloaded constructor
public Person(String name) {
this(name, 0);
}
//+ due to lack of String interpolation
public String sayHi() {
return "Hello, my name is " + name + " and I am " + age + " years old.";
}
}
//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]
//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);
Kotlin
//also supports mutable state (var)
data class Person(val name: String,
val age: Int = 0) {
//string interpolation
fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)
//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)
Moreover, use data classes for domain-related classes whenever possible. Their immutable nature ensures a safe, concise, and hassle-free experience when working with your application’s core.
Tip: Prefer Immutable over Mutable Collections
Immutable Collections have clear benefits regarding thread-safety, can be safely passed around, and are easier to reason about. Although Java collections offer some immutability features for Collections, their usage is dangerous because it easily causes exceptions at runtime:
Java
List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException
Kotlin
//The default collections in Kotlin are immutable (read-only) listOf(1,2,3).add(4); //✅safe: does not compile val l0 = listOf(1,2,3) val l1 = l0 + 4 //✅safe: it will return a new list containing the added element l1 shouldBe listOf(1,2,3,4) //✅
The same applies for using Collections.unmodifiableList(...), which is not only unsafe, but also requires extra allocation:
Java
class PersonRepo {
private final List<Person> cache = new ArrayList<>();
// Java – must clone or wrap every call
public List<Person> getItems() {
return Collections.unmodifiableList(cache); //⚠️extra alloc
}
}
//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException
Kotlin
class PersonRepo {
//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:
private val cache: MutableList<Person> = mutableListOf<Person>()
fun items(): List<Person> = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List<...>, it only exposes the read-only interface
}
//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile
When it comes to concurrency, immutable data structures, including collections, should be preferred. In Java, more effort is required with special Collections that offer a different or limited API, like CopyOnWriteArrayList. In Kotlin, on the other hand, the read-only List<...> does the job for almost all use cases.
If you need mutable, Thread-Safe Collections, Kotlin offers Persistent Collections (persistentListOf(...), persistentMapOf(...)), which all share the same powerful interface.
Java
ConcurrentHashMap<String, Integer> persons = new ConcurrentHashMap<>();
persons.put("Alice", 23);
persons.put("Bob", 21);
//not fluent and data copying going on
Map<String, Integer> incPersons = new HashMap<>(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));
//wordy and data copying going on
persons
.entrySet()
.stream()
.forEach(entry ->
entry.setValue(entry.getValue() + 1));
Kotlin
persistentMapOf("Alice" to 23, "Bob" to 21)
.mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on
Tip: Use named arguments.
Builders are very common in Java. Although they are convenient, they add extra code, are unsafe, and increase complexity. In Kotlin, they are of no use, as a simple language feature renders them obsolete: named arguments.
Java
public record Person(String name, int age) {
// Builder for Person
public static class Builder {
private String name;
private int age;
public Builder() {}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age);
}
}
}
//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime
new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.
Kotlin
data class Person(val name: String, val age: Int = 0) //Usage - no builder, only named arguments. Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime Person(name = "Jack", age = 36) //✅
If you have no greenfield option for trying out Kotlin, adding new Kotlin features or whole Kotlin modules to an existing Java codebase is the way to go. Thanks to Kotlin’s seamless Java interoperability, you can write Kotlin code that looks like Java to Java callers. This approach allows for:
Rather than starting somewhere, consider these different approaches:
Outside-in:
Start in the “leaf” section of your application, e.g. controller, batch job, etc. and then work your way towards the core domain. This will give you the following advantages:
Inside-out:
Starting at the core and then moving to the outer layers is often a riskier approach, as it compromises the advantages of the outside-in approach mentioned above. However, it is a viable option in the following cases:
Module by module
Language features for converting Java to Kotlin
Kotlin offers a variety of features – primarily annotations – that allow your Kotlin code to behave like native Java. This is especially valuable in hybrid environments where Kotlin and Java coexist within the same codebase.
Kotlin
class Person @JvmOverloads constructor(val name: String,
var age: Int = 0) {
companion object {
@JvmStatic
@Throws(InvalidNameException::class)
fun newBorn(name: String): Person = if (name.isEmpty())
throw InvalidNameException("name not set")
else Person(name, 0)
@JvmField
val LOG = LoggerFactory.getLogger(KPerson.javaClass)
}
}
Java
//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john = new Person("John");
//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();
//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java
//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken = Person.newBorn("Ken");
//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");
//@Throws(...) will put the checked Exception in the method signature
try {
Person ken = Person.newBorn("Ken");
} catch (InvalidNameException e) {
//…
}
Kotlin
@file:JvmName("Persons")
package org.abc
@JvmName("prettyPrint")
fun Person.pretty() =
Person.LOG.info("$name is $age old")
Java
//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...) Persons.prettyPrint(ken)
IntelliJ IDEA’s Java to Kotlin Converter
IntelliJ IDEA offers a Java to Kotlin Converter, so theoretically, the tool can do it for you. However, the resulting code is far from perfect, so use it only as a starting point. From there, convert it to a more Kotlin-esque representation. More on this topic will be discussed in the final section of this blog post series: Success Factors for Large-Scale Kotlin Adoption.
Taking Java as a starting point will most likely make you write Java-ish Kotlin, which gives you some benefits, but will not unleash the power of Kotlin’s potential. Therefore, writing a new application is the approach I prefer.
This installment in our Ultimate Guide to Successfully Adopting Kotlin in a Java-Dominated Environment series of blog posts demonstrated how Kotlin experiments can evolve into production code. Our next post focuses on the human side of adoption: convincing your peers. It explains how to present clear, code-driven arguments, guide new developers, and create a small but lasting Kotlin community within your team.
Hey, Insiders! I’m Yash Kamalanath, a Principal Product Manager on the Microsoft 365 companion apps team. I’m excited to share that the companion apps – People, Files, and Calendar – are now smarter than ever: Microsoft 365 Copilot is built into People and Files, with Copilot in Calendar coming soon. These smart agents make finding what you need and getting things done in Windows lightning fast.
Looking up a colleague, finding a file, or checking what’s next on your calendar should be effortless, but it’s easy to get distracted between tasks. With People, Files, and Calendar apps, everything you need is just a click away on the taskbar. These Copilot-integrated apps surface the right content in real time so you can stay focused and keep your work moving.
With Copilot, each companion app will be grounded in your work data – people, files, meetings – making them the fastest and easiest way to prompt for relevant questions. Companion apps offer instant suggestions for every item, plus a freeform box for your own prompts. For example, you can catch up on the latest from your top collaborators, flag comments that need your input, or recap meetings you missed. Start simple with a search in a companion app and seamlessly hand off to the Microsoft 365 Copilot app with full context for more complex inquiries – no extra steps needed.
With Copilot, the People app goes beyond names and title searches – it surfaces recent communications, highlights key responsibilities, and suggests tailored prompts to help you connect and collaborate with teammates across your organization.
Gather more insights by asking Copilot:
In the Files app, you can start a Copilot conversation directly from the content to summarize documents or presentations, review changes, analyze data, and create action items – without breaking your flow.
Gather more insights by asking Copilot:
Coming soon, Copilot will be integrated into the Calendar app, where you will be able to get meeting summaries and prep material to catch up and prepare for your day, manage your schedule in real time from your taskbar, and get up to speed in seconds on missed conversations.
Gather more insights by asking Copilot:
Copilot in the companion apps is available for Windows 11 users who have Microsoft 365 companion apps installed, are on either Enterprise or Business SKUs, and have a Microsoft 365 Copilot license. Copilot in People and Files is available immediately, and Copilot in Calendar will become available soon. As an admin, you can pin the Microsoft 365 Copilot app together with selected companion apps to the Windows 11 taskbar on Intune-managed devices. This provides users with quick access to Copilot features such as Chat, Search, and Agents.
Learn more about setting up People, Files, and Calendar: Microsoft 365 companions apps overview - Microsoft 365 Apps.
Learn more about pinning Microsoft 365 Copilot and its companion apps to the Windows taskbar: Pin Microsoft 365 Copilot and its companion apps to the Windows taskbar.
We’re excited to bring you these new capabilities, and we’d love to hear your thoughts on how these companion apps are working for you! Share feedback or suggestions anytime using the Give Feedback button in the top-right corner of any of the companion apps.
Learn about the Microsoft 365 Insider program and sign up for the Microsoft 365 Insider newsletter to get the latest information about Insider features in your inbox once a month!
In this episode of Mailin’ It!, hosts Karla Kirby and Jeff Marino jump into the fast-paced world of e-commerce shipping with guest Heather Maday, Senior Director of Sales Enablement at the US Postal Service. Heather shares how USPS has transformed its operations to support today’s eCommerce economy, where shoppers expect speed, visibility, and value with every order. From data driven logistics and new fulfillment technologies to transparent pricing, Heather explains how USPS helps businesses reach customers affordably and efficiently.
Hosted by Simplecast, an AdsWizz company. See pcm.adswizz.com for information about our collection and use of personal data for advertising.
In this episode, I talk with Samantha Lopez, whose path into software development is anything but typical -- and that’s exactly what makes it so inspiring.
Whether you’re just starting out or pivoting mid-career, Samantha’s journey proves that your past experiences can be your biggest asset in tech -- you just have to connect the dots and keep going!
----
You can find Samantha at:
- LinkedIn: https://www.linkedin.com/in/samlopezdev/
- GitHub: https://github.com/samlopezdev
- Portfolio: https://samlopezdev.netlify.app/
----
🎥 Channels:
🔑 Membership & Subscriptions:
🧠 Courses:
🗣️ Social Media & Links: