Since its earliest release, Java has touted itself as a "write once, run everywhere" programming language; the idea was that a programmer could develop an app in Java, have it compiled down to bytecode, and become an executable that can run on any platform, regardless of operating system or platform. It was able to do so in part by a runtime known as the Java Virtual Machine, or JVM.
To Java's credit, the JVM was (and still is!) an incredibly fine-tuned runtime that abstracted away a computer's underlying hardware. While Java as a programming language survives to this day, it is often viewed as cumbersome, stodgy, and representative of an outdated approach to implementing solutions.
In the last 10 years, more and more languages that run on the JVM have been developed but, they look and feel nothing like Java. One such language is Kotlin. Because of the JVM, it has no real performance advantages over regular Java. Still, its strength lies in the fact that it prioritizes legibility in a way that Java does not. Consider, for example, printing a substring in Java:
// Java
String input = "What is the answer to the Ultimate Question of Life, the Universe, and Everything? 42";
String answer = input.substring(input.indexOf("?") + 1);
System.out.println(answer);
You must first get the index of the character you want to be in the substring, add one (since strings are zero-indexed), and call System.out.println
to write to stdout.
In Kotlin, this is much shorter:
// Kotlin
val input = "What is the answer to the Ultimate Question of Life, the Universe, and Everything? 42"
val answer = input.substringAfter("?")
println(answer)
Kotlin has garnered so much interest that Google even recommends it over Java for developing Android apps.
In this post, we'll take a quick look at how to develop an app in Kotlin. We'll build a simple API with a PostgreSQL database and deploy it to Heroku to see it live.
Before we begin, you'll need to make sure you've got the following software installed on your machine:
You will also need to be a little familiar with Git and have it installed on your machine.
We’re going to be using the IntelliJ IDE for this Kotlin app. Their documentation provides some guidance on how to create a new project. Make sure you select the following options:
After the IDE sets everything up, you should have a directory structure that looks roughly like this:
kotlin-api
├── build.gradle.kts
└── src
├── main
│ ├── kotlin
Our Kotlin files will be kept in src/main/kotlin
And our build logic will be in build.gradle.kts.
Gradle is a build tool for a variety of languages. It also acts as a dependency management tool, similar to Maven. You’ll already have some lines in your build.gradle.kts file, which the IDE automatically added to be helpful. You can delete all of that and paste in these lines instead:
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.5.10"
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
group "com.example"
version "0.0.1"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter")
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
These lines specify our project's dependencies and where to find them. For example, we want to use org.springframework.boot
at version 2.4.3, which is why it's defined within the plugins
block. We point out the repositories where the packages can be found—at mavenCentral()
— and which exposed classes we want to use (implementation( "org.springframework.boot:spring-boot-starter-web")
).
Let's create two small files to test our setup. Create a file called Application.kt
in the src/main/kotlin folder and paste in the following:
package com.example
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
open class Application
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
This starts a default app using the Spring framework. The real magic happens in this next file, Controller.kt
, which you should create alongside Application.kt
in src/main/kotlin:
package com.example
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class GreetingController {
@GetMapping("/{name}")
fun get(@PathVariable name: String) = "Hello, $name"
}
Here, we define a route (@GetMapping("/{name}")
), where {name}
is a dynamic value. By placing this decorator over a Kotlin method (fun get
, or "a function named get"), we're able to match the route to whatever behavior we want—in this case, returning a greeting with the path name for a GET
request.
In order for the IDE to know how to launch our application, we need to create a run configuration. At the top of the IDE menu, click the button that says Add Configuration. Then, select Add new run configuration. Next, choose Gradle from the list. For the Gradle project name, enter kotlin-api. In the Tasks field, type bootRun
. bootRun
is a Gradle task provided by the Spring framework, which will compile our code and start the server.
Click Ok.
You should now have a green Play button in your IDE menu bar. When you click on this, the IDE will execute gradle bootRun
to build this Kotlin app and start the server. When that finishes, navigate to http://localhost:8080/world
. You should see a nice greeting.
Now, let's get to the (somewhat) serious stuff. Suppose we wanted to attach a database to this project. In a Maven/Java world, we'd need to update an XML file and add a reference to a JAR file. In Gradle, we can get by with just adding a few lines to our build.gradle.kts
file:
dependencies {
# ...
implementation("com.zaxxer:HikariCP:4.0.3")
runtimeOnly("org.postgresql:postgresql")
# ...
}
Here, we've included HikariCP in our project, which is a popular database connection driver. We also indicate that we want to "load" the org.postgresql
library during runtime. With just these two lines, we've let our configuration know that we want to interact with a PostgreSQL database. If you already have a PostgreSQL database running locally, that's great. You'll be able to continue the rest of this guide locally and see the results when browsing localhost. If you don't have PostgreSQL, don't fret — we'll show you just how easy it is to deploy this app on Heroku, which will take care of the infrastructure for you.
Head back to Controller.kt
and replace it with the contents below. This takes some of what we had from before but adds to it. We'll go over the changes shortly.
package com.example
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.http.MediaType
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import java.net.URI
import javax.sql.DataSource
@RestController
class GreetingController {
val dataSource = dataSource()
val connection = dataSource.connection
@GetMapping("/{name}")
fun get(@PathVariable name: String) = "Hello, $name"
@PostMapping(value = ["/add-name"], consumes = [MediaType.TEXT_PLAIN_VALUE])
fun post(@RequestBody requestBody: String) : String {
initDb()
val stmt = connection.createStatement()
stmt.executeUpdate("INSERT INTO names values('$requestBody')")
return "Added $requestBody"
}
@GetMapping("/everyone")
fun getAll() : String {
initDb()
val stmt = connection.createStatement()
val rs = stmt.executeQuery("SELECT name FROM names")
val output = ArrayList<String>()
while (rs.next()) {
output.add(rs.getString("name"))
}
val names = output.joinToString(", ")
return "Here are the names: $names"
}
internal fun initDb() {
val stmt = connection.createStatement()
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS names (name text)")
}
internal fun dataSource(): HikariDataSource {
val config = HikariConfig()
var dbUri = URI(System.getenv("DATABASE_URL") ?: "postgresql://localhost:5432/")
var dbUserInfo = dbUri.getUserInfo()
var username: String?; var password: String?;
if (dbUserInfo != null) {
username = dbUserInfo.split(":").get(0)
password = dbUserInfo.split(":").get(1)
} else {
username = System.getenv("DATABASE_USERNAME") ?: null
password = System.getenv("DATABASE_PASSWORD") ?: null
}
if (username != null) {
config.setUsername(username)
}
if (password != null) {
config.setPassword(password)
}
val dbUrl = "jdbc:postgresql://${dbUri.getHost()}:${dbUri.getPort()}${dbUri.getPath()}"
config.setJdbcUrl(dbUrl)
return HikariDataSource(config)
}
}
There's quite a lot going on here! Let's start from the bottom. We define a function called dataSource
which provides a connection to our database. Because we're building a 12-Factor app, our database credentials are stored in an environment variable called DATABASE_URL
. We fetch that URL and pull out the username and password from it if one exists. If not, we check another two environment variables for that information — DATABASE_USERNAME
and DATABASE_PASSWORD
. We then put all that information together into a format that the database connector needs. The initDb
function creates a table called names
, with a single text column called name
. The /everyone
endpoint has a @GetMapping
decorator just like before. This defines a GET /everyone
route, which gets all the names from the database.
Finally, we've added something rather new: a @PostMapping
decorator. Here, we need to define what types of content this POST
route can accept. In this case, it consumes
a TEXT_PLAIN_VALUE
media type (in other words, "Content-Type: text/plain"
). Whatever string of information we put in the request body will be added to the database. In just a few lines, we've built a small API that we can add to and query.
If you start this server now—and if you have PostgreSQL running locally — you should be able to interact with it. Try making the following request:
$ curl -H "Content-Type: text/plain" -X POST http://localhost:8080/add-name -d 'Frank'
If you navigate to http://localhost:8080/everyone
, you'll see that Frank
was included.
It's time to see just how easy it is to get Kotlin running on Heroku. First, we need to create a file that's specific to Heroku: the Procfile. This text file defines how our application should boot and run.
Create a file named Procfile in the root level directory, right next to your build.gradle.kts file. Copy-paste the following lines into it:
web: java -jar build/libs/kotlin-api.jar --server.port=$PORT
Here, we're saying that we want Heroku to run java -jar build/libs/kotlin-api.jar
. That JAR is packaged and built during the deployment process; Heroku will create it automatically for us because it knows how to execute the Gradle task to do so. We are also binding the $PORT
environment variable so that Heroku knows which port the server is listening to.
Next, run the following Git commands:
$ git init
$ git add .
$ git commit -m "Preparing my first Kotlin app"
Since we have the Heroku CLI installed, we can call heroku create on the command line to generate an app:
$ heroku create
Creating app... done, ⬢ desolate-plains-67007
Created http://desolate-plains-67007.herokuapp.com/ | [email protected]:desolate-plains-67007.git
Your app will be assigned a random name — in this example, it's desolate-plains-67007
— as well as a publicly accessible URL.
In order to provision a database, we use the heroku addons
command. Calling it without arguments will let us know if any exist:
$ heroku addons
No add-ons for app desolate-plains-67007.
No add-ons exist for our app, which makes sense — we just created it! To add a PostgreSQL database, we can use the addons:create
command like this:
$ heroku addons:create heroku-postgresql:hobby-dev
Heroku offers several tiers of PostgreSQL databases. hobby-dev
is the free tier, so we can play around with this without paying a dime.
Your code is ready; your Heroku app is configured — you’re ready to deploy. This is the easy part! Just type out the following command:
$ git push heroku master
Your code will be pushed to Heroku. From that point on, Heroku will take over. You'll see your build logs scrolling through your terminal. This will show you what Heroku is installing on your behalf and where you are in the build process. After it’s complete, you can visit your special URL in the browser (in this case, https://desolate-plains-67007.herokuapp.com
) and interact with the API on the internet!
Kotlin's performant design and legible syntax — combined with the ease of Gradle — make it ideal for enterprises that need to rely on battle-tested Java packages. Because of its interoperability with Java, Kotlin is ideal as a transitional language; vast swaths of Java code can be converted into Kotlin while still maintaining a functional app. Deploying to Heroku is smooth, and I didn't even take advantage of the different ways to fine-tune Java and JVM-based apps for deployment.