Kotlin is a statically typed programming language for the JVM, designed by JetBrains in 2011, which fixes nearly everything wrong with Java — the verbosity, the null pointer exceptions, the ceremony, the boilerplate — while changing absolutely nothing about the architecture that made those problems systemic in the first place.
This is not a criticism. It is an observation. The observation is: Kotlin is a beautiful renovation of a building with a cracked foundation. The countertops are marble. The plumbing works. The light is excellent. The building is still the cathedral that Design Patterns built and Oracle owns, sitting on the JVM, surrounded by Spring, sinking slowly into the same geological stratum the renovation was supposed to escape.
Kotlin is what Java would be if Java had been designed by people who had used Java.
“The migration away from Java began slowly — Scala, Kotlin, Clojure, all running on the JVM, all trying to fix Java without leaving the ecosystem.”
— Java
The Best Friend’s Language
Every developer knows a Kotlin enthusiast. Not an evangelist — evangelists are loud and eventually alienating. A Kotlin enthusiast is a friend. A good friend. The kind who genuinely wants to help. The kind who texts you an article about null safety at 11 PM and follows up the next morning with “did you read it?” The kind who says “just try it” with the earnest warmth of someone offering you a home-cooked meal, and who is, objectively, correct that the meal is delicious.
The friend is right. Kotlin is better than Java. This is not disputed. It is not even debatable. Data classes replace two hundred lines of boilerplate with one line:
data class User(val name: String, val email: String)
In Java, this is:
public class User {
private final String name;
private final String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
@Override
public boolean equals(Object o) { /* 12 lines */ }
@Override
public int hashCode() { /* 4 lines */ }
@Override
public String toString() { /* 3 lines */ }
}
Kotlin’s version is shorter. Kotlin’s version is correct. Kotlin’s version generates the same bytecode. The friend is right. The friend is always right. That is what makes respectfully declining so difficult.
The Respectful Decline
The respectful decline is not about Kotlin. The respectful decline is about the JVM.
The JVM is a virtual machine that starts in seconds, runs a garbage collector that sometimes pauses, and deploys as a JAR file that requires another JVM to run it, which requires a specific version of that JVM, which requires a container to isolate the version, which requires an orchestrator to manage the containers. The ceremony that Kotlin removed from the language remains in the deployment.
Go produces a binary. The binary has no runtime. The binary has no dependencies. The binary runs. The deployment is scp. The ceremony is zero.
Kotlin fixes Java the language. Go fixes Java the experience. The language was never the whole problem. The language was the part of the problem you spent the most time looking at, which made it feel like the whole problem, the way a leaky faucet feels like the whole problem until you notice the foundation crack.
The respectful decline goes: “I know. I’ve seen it. It’s genuinely good. I chose Go.” The friend nods. The friend understands. The friend sends another article about coroutines at 11 PM anyway, because the friend is a friend, and friends do not give up.
What Kotlin Gets Right
To be clear — and the clarity matters, because the respectful decline must not be confused with dismissal — Kotlin gets almost everything right:
Null safety. The billion-dollar mistake, caught at compile time. String cannot be null. String? can. The compiler enforces this. A generation of NullPointerException stack traces, prevented. This alone justifies Kotlin’s existence.
Extension functions. Add methods to existing classes without inheritance, without wrapper classes, without the UserUtils static method cemetery. The function looks like it belongs to the class. The compiler rewrites it as a static call. Elegant.
Coroutines. Structured concurrency that is — and this must be acknowledged — dramatically more approachable than Rust’s Pin<Box<dyn Future<Output = Result<T, Box<dyn Error + Send + Sync>>> + Send + 'static>>. Coroutines launch, suspend, and resume with syntax a human can read. Not as simple as goroutines, but nothing is as simple as goroutines. Goroutines are go f(). Everything else is an apology for not being that.
Sealed classes. Algebraic data types that the compiler can exhaustively check. when expressions that guarantee every case is handled. Pattern matching that works.
Interoperability. Kotlin calls Java. Java calls Kotlin. The migration path is incremental, file by file, class by class, which means it is actually possible, unlike Rewrite, which is never possible.
All of this is real. All of this is good. None of this changes the fact that the binary still runs on the JVM, the JVM still needs a container, and the container still needs Kubernetes, and the Go developer has already deployed and gone home.
The Scala Lesson
Scala tried this first. Scala ran on the JVM, fixed Java’s verbosity, added functional programming, and produced code so clever that only Scala developers could read it — and not all of them. Scala proved that making the JVM’s surface language more powerful does not make the JVM’s ecosystem simpler. It makes the language a monad and the build system a PhD thesis.
Kotlin learned from Scala’s mistakes. Kotlin is simple where Scala is clever. Kotlin is practical where Scala is theoretical. Kotlin is readable where Scala requires a type theory textbook. This is genuinely admirable. The JetBrains team looked at Scala and said “yes, but for humans,” and they were right, and the result is a language that any Java developer can learn in a week and prefer within a month.
The problem is that “better than Java on the JVM” and “better than Go with a single binary” are different competitions, and the second one has a lower bar and a faster deployment.
The Android Exception
One context exists where the respectful decline does not apply: Android.
Android runs on the JVM (or its descendant, ART). Android’s alternative to Kotlin is Java. There is no Go option. There is no single-binary option. There is the JVM and whatever language you choose to write for it, and in that constrained competition — Java versus Kotlin, no other contestants — Kotlin wins unanimously, totally, and without qualification.
Google made Kotlin the official Android language in 2019. This was correct. On Android, Kotlin is not a renovation. Kotlin is the only reasonable way to build. The friend who advocates Kotlin for Android is not just right but urgently right, and the respectful decline does not apply, and the 11 PM article about coroutines should be read immediately.
The respectful decline applies only on the server, where the JVM is a choice, and other choices exist, and one of those choices is a gopher carrying a single binary and a lizard carrying fifty years of knowing that simple wins.
The Caffeinated Squirrel’s Opinion
The The Caffeinated Squirrel adores Kotlin. This should be noted. The Squirrel finds Go insufficiently expressive, insufficiently clever, insufficiently interesting. The Squirrel wants sealed classes and extension functions and scope functions (let, apply, also, run, with — five ways to do the same thing, each subtly different, which the Squirrel considers richness and the Lizard considers five ways to be wrong).
The Squirrel’s Kotlin code is beautiful. It is concise. It is idiomatic. It uses every feature the language offers. It runs on the JVM, in a container, managed by Kubernetes, deployed by a CI/CD pipeline with fourteen stages.
The Lizard’s Go code is plain. It uses if err != nil. It compiles to a binary. It runs.
They both serve the same twelve hundred users.
The Measured Difference
| Metric | Kotlin | Go |
|---|---|---|
| Lines to fetch a user from a database | 15 | 12 |
| Null safety | Compile-time | Not applicable (no null) |
| Deployment artifact | JAR + JVM + container | Binary |
| Startup time | 2-8 seconds (Spring) | 10 milliseconds |
| Memory at idle | 200-500 MB (JVM) | 10-30 MB |
| Build system | Gradle (XML or Groovy or Kotlin DSL) | go build |
| Time to explain deployment to a junior | 45 minutes | 2 minutes |
| 11 PM articles from best friend | Weekly | Never (Go is not interesting enough to text about) |
The Lizard’s Scroll
The Lizard, presented with a Kotlin data class and a Go struct that did the same thing, blinked once:
THE RENOVATION IS BEAUTIFUL
THE FOUNDATION IS THE SAMETHE LANGUAGE FIXED THE LANGUAGE
THE ECOSYSTEM KEPT THE ECOSYSTEMTHE FRIEND IS RIGHT
THE FRIEND IS ALWAYS RIGHT
THE FRIEND IS RIGHT ABOUT THE COUNTERTOPSTHE GOPHER IS RIGHT ABOUT THE FOUNDATION
