At Change, we decided from the start to use Kotlin as our main server-side language. Mostly because the initial crew had extensive Java background and all of us have regarded Kotlin’s feature set superior to Java. It enables us to deliver features faster than when using Java with less bugs.

What is Kotlin?

Kotlin started off in 2011 as a JVM language. Like Java, but with better features, like Scala, but with faster compilation times. By now it’s not only a server-side JVM language: you can use Kotlin on Android, compile it to JavaScript or even native binaries. On JVM it is fully interoperable with Java and it adds some interesting features like:

  • Null safety by default
  • No checked exceptions, primitive types, static members, non-private fields
  • Extension functions
  • Smart casts
  • String templates
  • Operation overloading
  • Type aliases
  • Delegation
  • etc.

None of us had actually used Kotlin in production before, but this was not mainly because of Kotlin itself (Kotlin has been stable since the start of 2016) but because it was painful to use Kotlin with Spring, our chosen backend framework. Biggest issues were that null-safety, one of the selling points of Kotlin, was not that useful, since platform types (types of values returned from Java code) are nullable by default and a lot of Spring annotations (like Component or Transactional) can’t be used on final classes, which is default in Kotlin. Fortunately in Spring 5, lot of issues with Kotlin-Spring interoperability were solved and we felt comfortable to try it out!

Most of the time our code doesn’t look a lot different from Java code, since we are using frameworks which were written with Java in mind, but when we can and we see it useful, we try to take advantage of Kotlin specific language features.

Data classes

One of our most used features is data classes. Kotlin makes it really easy to define classes that hold data, compare them, change properties (while maintaining immutability) and even destructure properties.

data class User(val id: String, val name: String, val age: Int)

//Usage val user = User("someid", "User Name", 45)

println(user.toString(()) //Prints: User(id="someid", name="User Name", age=45)

val (id, name) = user println(id) //Prints: someid println(name) //Prints: User Name

val newUser = user.copy(name = "New Name") println(newUser) //Prints: User(id="someid", name="New Name", age=45)

Operator overloading

Although operator overloading is often considered harmful, it enables us to write an easy to use Money (value and currency) class.

data class Money(val value: BigDecimal, val currency: Currency) {

operator fun plus(money: BigDecimal): Money = this.copy(value = this.value + money)

operator fun plus(money: Money) = this.doWhenCompatible(money) { this + it.value }

operator fun minus(money: BigDecimal): Money = this.copy(value = this.value - money)

operator fun minus(money: Money) = this.doWhenCompatible(money) { this + it.value }

operator fun times(money: BigDecimal): Money = this.copy(value = this.value * money)

operator fun times(money: Money) = this.doWhenCompatible(money) { this * it.value }

private fun Money.doWhenCompatible(money: Money, block: (Money) -> T): T {
return if (this.currency != money.currency)
throw IncompatibleCurrencyException("Currencies are not compatible: ${this.currency} ${money.currency}")
else block(money)
}

override fun toString(): String { return "$value $currency" } }

class IncompatibleCurrencyException(override val message: String) : RuntimeException()

// Usage
val oneEuro = Money(BigDecimal.ONE, Currency.EUR)
val twoEuros = oneEuro + oneEuro
val fourEuros = twoEuros * twoEuros
val oneBitcoin = Money(BigDecimal.ONE, Currency.BTC)
oneBitcoin * oneEuro // will throw IncompatibleCurrencyException

Now we can do easy arithmetic operations with Money and BigDecimal instances while having safety of not doing operations between different currencies.

NB! stick with arithmetic operations only when overloading or be hated by all your teammates!

Navigating nullable values

When you first start writing Kotlin as a Java developer, you might struggle with the null checks. If you have nullable values (which is fine in Kotlin because you are forced to handle null so there is no need for the Null Object pattern or Optional) then you might feel weird writing null checks every time you need to access a property of a nullable object. Fortunately, there are many solutions to this problem, depending on what do you need to do with that property or function.

val user: User?

// Get user ID

//If expression (userId is nullable)
val userId = if(user != null) user.id else null

//Elvis operator (userId is not nullable, fail fast)
val userId = user.id ?: throw IllegalStateException("User can not be null at this time")

//Any?.let (userId is nullable)
val userId = user?.let{it.id}

//Any?.let (userId is not nullable, is a String, can be empty)
val userId = user?.let{it.id}.orEmpty()

//Safe navigation (userId is nullable)
val userId = user?.id

In our codebase we use safe navigation when we just want to get the value of a property on the nullable object, elvis operator when the object should not be null and Any?.let when we want to apply some transformation on the property if it exists (basically how in a lot of functional languages you would use a map function).

Extension functions

Extension functions let you add new functions to classes that you do not compile yourself (libraries).

fun MessageChannel.sendMessage(o: Any): Boolean {
return this.send(MessageBuilder.withPayload(o).build())
}

// Usage
val mailMessageChannel // A MessageChannel instance

fun sendEmail(message: Message) {
mailMessageChannel.sendMessage(message)
}

We try to not overuse this feature because extension functions are not OOP and often cause readability issues.

Extension properties

Extension properties are pretty much the same as extension functions but just for properties.

val Number.eur get() = Money(BigDecimal(this.toLong()), Currency.EUR)
val Number.btc get() = Money(BigDecimal(this.toLong()), Currency.BTC)
val Number.ltc get() = Money(BigDecimal(this.toLong()), Currency.BTC)
val Number.eth get() = Money(BigDecimal(this.toLong()), Currency.ETH)

//Usage
val hundredBtc = 100.btc
val twentyEur = 20.eur

Test builders

Kotlin lets us define builders which can easily be used to construct instances of our domain objects in tests.

class OrderBuilder {
var id: Long = Random().nextLong()
var userId = 1L
var from = 100.btc
var to = 10.ltc
var rate = 0.5
var status = NEW

private fun build() = Order(id, userId, from, to, rate, status)

companion object {
fun anOrder(block: OrderBuilder.() -> Unit) = OrderBuilder().apply(block).build()
val anOrder = anOrder {}
}
}

//Usage
import com.getchange.test.OrderBuilder.Companion.anOrder

val order = anOrder
val sentOrder = anOrder {status = SENT}

Conclusion

Even though we do not use every cool feature, our choice of Kotlin over Java has definitely paid off: the code is much cleaner and it avoids entire categories of Java defects besides null safety. The language itself has been very stable, with small exceptions, but nothing serious and bugs get fixed fairly quickly. If something doesn’t work in Kotlin you can always write it in Java and call it from Kotlin and vice versa.

Bonus: Kotlin with ligatures
IntelliJ IDEA supports fonts with ligatures, which makes Kotlin code look really nice.

By Maido Käära, Full Stack Product Engineer at Change.

Be part of Change! We are always looking for unique and talented people to be part of our ever growing team. Apply here.

Related Articles

Share This