Supercharge your code with Kotlin Observable: Watch your properties recompute in real time!

Bruno Donizeti da Silva
Building Inventa
Published in
5 min readJun 13, 2023
Photo by Nick Loggie at Unsplash

Kotlin is a versatile programming language that is widely used in developing mobile and web applications. At Inventa, we’re using Kotlin extensively and having really nice results in terms of maintainability, reliability, scalability, and also in developer experience. One of the most prominent features of Kotlin is observable, which enables developers to create reactive and event-driven programs. In this article, we will explore what Kotlin’s observable feature is and how it can be used in practical applications.

Here on the order-management squad at Inventa, we are responsible for managing the lifecycle of an order from its payment at checkout until it is delivered to the retailer. During the order process, there are various steps that can result in changes to the order amounts. Some scenarios include:

  • Insufficient product quantity: If the supplier does not have the purchased quantity of a product in stock, they can communicate with the retailer and reduce the quantity accordingly. This reduction in quantity will lead to a decrease in the order's total value.
  • Recalculating service fees: When the order’s total changes, it may be necessary to recalculate the service fee for the order. The service fee calculation can be adjusted based on the updated order's total value.
  • Adjusting vendor's receivable amounts: In addition to service fees, changes in the order total may also impact the vendor’s receivable amount. If there are modifications to the service fee, the vendor’s receivable amount should be recalculated accordingly.

This is just one of the scenarios where order amounts ​​can change and cascade changes to other fields.

Initially, we implemented the logic for recalculating the values ​​in Hibernate’s listeners @PrePersist and @PreUpdate, with this we were able to guarantee that regardless of the use case that changed some value of the order, the persisted data would always have the correct value. The example below shows how it was implemented:

@PreUpdate
@PrePersist
fun updateReceivableAmount() {
this.receivableAmount = this.subtotal
.plus(this.taxes)
.plus(this.freight)
.minus(this.serviceFee)
.minus(this.discounts)
}

The orders table was getting too big with too many fields, so we split that table and created a new one with the shipping-related information. Among these moved fields, there is a field containing the value of the freight, so every time this field changes, we need to recalculate the vendor’s receivable amount. As they are now different entities, even if we change the shipping value using the order entity as a base, hibernate’s @PreUpdate will not be triggered, since the change was not the Order entity, but the Shipping entity. This solution presented above no longer works for us.

How to solve the problem

Looking for alternatives on how to figure this thing out, we found Kotlin’s Observable feature.

Observable properties are variables that can be monitored for any changes in their value. When a change is detected, the property notifies any listeners that have subscribed to it. This allows other parts of the application to react to changes in the property’s value.

To create an observable property in Kotlin, we can use the ‘observable’ function provided by the Kotlin standard library. Here’s an example:

import kotlin.properties.Delegates

class KotlinObservableExample {
var total: BigDecimal by Delegates.observable(BigDecimal.ZERO) {
prop, old, new ->
println("$old -> $new")
}
}

In this example, we create a KotlinObservableExample object with an observable property called total. The property is initialized with the value BigDecimal.ZERO. We use the ‘observable’ function to specify a lambda expression that will be called whenever the property’s value changes. The lambda expression takes three arguments: the property being changed, the old value, and the new value.

Applying this to our use case, we have something like this:

class Order(
val id: Long,
val uuid: UUID
... // other propeties
) {
var subtotal: BigDecimal by Delegates.observable(BigDecimal.ZERO, computeTotals())
var serviceFee: BigDecimal by Delegates.observable(BigDecimal.ZERO, computeTotals())
var discounts: BigDecimal by Delegates.observable(BigDecimal.ZERO, computeTotals())
var taxes: BigDecimal by Delegates.observable(BigDecimal.ZERO, computeTotals())
var shipping: Shipping? by Delegates.observable(null, computeTotals())

private fun computeTotals(): (
property: KProperty<*>,
oldValue: Any?,
newValue: Any?
) -> Unit = { _, _, _ ->
this.computeServiceFee()
this.computeTotalPrice()
this.computeReceivableAmount()
}
}

In the property declaration, instead of just declaring the type, we declare a delegate of the observable type, informing the default value of the property and the callback method to be triggered when the value changes. In our use case, for any value that changes we want to recalculate the service fee, the total price, and the receivable amount. We use the same callback for all properties, but you can create many different callbacks depending on your use case.

The callback method has three parameters with the following information:

1. The first parameter contains information about the changed property itself. It provides details such as whether the property is final or open, its type, and other relevant information that you can check here.

2. The second parameter holds the value of the property before the change occurs. It allows you to access the previous value and perform any necessary comparisons or operations.

3. The third parameter represents the value of the property after the change. It provides the updated value, allowing you to track the modification that took place.

With this solution, in addition to being able to solve the problem we have:

  • Loose coupling: we removed the dependency of the database layer for updating the properties value.
  • Testability: we can test this logic with unit tests. When using Hibernate's listener it was necessary to create integration tests, loading the application to insert and update an entity to verify if the business logic was right.
  • Maintainability: we don’t need someone to know how Hibernate’s listeners work to understand that value will only be updated when the entity is saved. We also can change our entity without having side effects on the business logic, and if it does, the unit tests will fail.

I hope to have helped you to get to know a little about this Kotlin feature and how can it be useful in a real-world problem. If you’ve used this feature in any other way that you find interesting to share, add your comment below to enrich the article. Thanks for reading.

If you want to know more about us, add me on LinkedIn or apply to one of our open positions! Let’s learn together!

Open Positions: LinkedIn Jobs

--

--