Sannsyn Case Study: a High-performance Backend for Modern Machine Learning

Sannsyn provides artificial intelligence-based solutions for businesses that want to improve their user experience, increase their customer loyalty, and boost their conversion rates. Personalized customer recommendations are a core functionality for the solutions Sannsyn offers.

These solutions pose a unique technical challenge with a multitude of components on the server-side with billions of interactions across a complicated system, all while keeping the client-side simple.

Sannsyn has never looked back after choosing Kotlin, and here is why:

  • Great interoperability – Kotlin makes it possible to have all sorts of JVM components, including Scala and Clojure.
  • A perfect solution for high-performance backend applications.
  • Everyone on the team that has tried it has fallen in love with it!

We asked Hallvard, Sannsyn’s Community Manager for TellusR to tell us about the story of how Sannsyn applies Kotlin to complex system development.

Solution Overview

The Gravity platform is one of Sannsyn’s flagship products. On the surface it seems rather simple: submit customer events and retrieve customer recommendations. But under the hood, it’s a hive of components, structures, and messages that communicate back and forth between each other.


Kotlin is an excellent choice for backend application development. Go to the Kotlin for server-side page to learn more.


Requests come in over HTTPS to a frontend server and are then dispatched to an Akka-based system made up of small components, each with very specific and specialized tasks. Without going into too much detail, the process looks like this:

Since the Gravity platform is spread out over different machines, Akka is used for internal communication across the system. While the Akka ecosystem is based on Scala, the team prefers to stick to the JVM platform rather than limit the focus to just Scala and Akka. Different components are implemented in Scala, Java, Clojure, and Kotlin − sometimes there is code that is compiled from different languages that coexist in the same JVM. It is precisely this no-effort interoperability within this multitude of languages that is one of Kotlin’s key strengths.

Let’s take our Akka integration as a practical example. Sending messages between actors is performed in Akka/Scala with the very elegant ! command. This, for instance, will send theMessage to theActor:

theActor ! theMessage

Why not have this in our Kotlin code as well? We could use `!` to make it look as much like the original as possible, but Kotlin can’t do this without the backticks, and they really aren’t very elegant, so we chose to create a tell function instead. So, what we wanted was this:

    
theActor tell theMessage

And this can be easily achieved using the following:

    
private infix fun ActorRef.tell(msg: String) = this.tell(msg, self)

So, we didn’t end up with the elegant ! we wanted from the Scala code. But then, we implemented the same thing for the forward function. Akka hasn’t done this, but for consistency of expression, we would like the forward function to be used in the same manner as the tell function. So, this is how we defined it:

    
private infix fun ActorSelection?.forward(message: String) = this?.forward(message, context)

And what this gave us, was more of Akka’s elegance than Akka has by itself!

It is also worth noting that the extension function this time is on a nullable type. We have some ActorSelection objects lying around that need to be nullable since they are constructed from a configuration that might be missing. And, since we’re allowed to define the extension function on a nullable type, we can add this extra elegance without having to bloat the code with null checks on every call site. We’ve really appreciated this feature!

So, when we then started work on our newest product, the Solr refiner named TellusR, Kotlin was the natural choice for us. We quickly came to appreciate it. The thing with writing code that hooks into other people’s code is that the stack traces very often include lines that are unrelated to your own code. So, to simplify the reading of long stack traces we wrote two small extensions to pinpoint our own responsibility in different exceptions. The first extension picked up the last line of Sannsyn code involved:

    
val Throwable.stackTraceTopString
    get() = stackTrace.firstOrNull {
            it.className.contains("com.sannsyn")
        }?.toString()?.trimStart()

And then another extension which combined this with the actual error message before it was written into our logs:

val Throwable.messageAndCrumb
    get() = "${localizedMessage ?: toString()} (${stackTraceTopString ?: "-"})"

This small measure has saved us a lot of time when it comes to searching for the culprit of errors. This is our new favorite Logback one-liner:

logger.warn("HERE: {}", e.messageAndCrumb, e)

Since this is only six small lines of code, it quickly spread from TellusR over to all our other Kotlin-based projects.

Why Kotlin?

Sannsyn began using Kotlin because of Kotlin’s take on null objects. This was back before Kotlin had released a 1.0 version. Back then Sannsyn was maintaining a client’s eBook reading app, and struggled with the Android version’s many NPEs. The team conducted research to find a null-safe language for the JVM, hoping it might be usable on the Android platform.

Moving to Kotlin on Android proved simple and problem-free. Integrating it with the existing Java code was a piece of cake too. So the natural move was to try Kotlin with products on the server.

In addition to the nullability handling, there was also another great benefit to using Kotlin – its seamless integration with JVM languages. All other JVM based languages recognized and understood Kotlin’s bytecode immediately. And there was the fact it is supported in IntelliJ IDEA!

“Since the very beginning of our migration to Kotlin, IntelliJ IDEA has been around to support us. Making the move to Kotlin really felt like just upgrading from Java.”

Besides the goal of minimizing NPEs, exploring this new language in the making turned out to be fun! Multiplatform build options came out at exactly the right time. During the MPP solution release, the Sannsyn team were struggling with the port of an internal message format that had to be usable in both JavaScript and code for mobile devices. 

Instead of maintaining an external protocol that all these projects had to respect, they now simply program the protocol in an MPP project so there is only one codebase, and use the produced bytecode and JavaScript code everywhere afterwards. This has saved our developers a lot of effort and reduced the risk of introducing protocol errors.

Favorite features

“Our main focus originally was the NPEs, so our favorite feature in Kotlin is the nullability handling”.

  • The syntax of the higher order functions. Kotlin is almost at a Haskell level here. There is no need to split it into streams and aggregate afterwards.
  • Kotlin’s type inference makes code pretty non-verbose. This, along with data classes, keeps the code very concise and elegant.
  • The language design around coroutines is brilliant! More lightweight than threads, sure; non-blocking, sure; but let’s not forget that also the design of the syntax in this case makes programming in asynchronous environments easier.

Summary

Initially attracted to Kotlin as an NPE killer, Sannsyn engineers enjoy compact and neat syntax, well-designed solutions like coroutines, multiplatform projects with the option of code sharing, and of course first-class IntelliJ IDEA support. They also really love interoperability with the JVM and the happiness which Kotlin brings to the team.