I built the same app thrice
Posted on February 12, 2025 • 8 minutes • 1641 words • Other languages: Español
- Inspiration
- Understanding the application
- Quick metrics
- There’s no The Good, the Bad and the Ugly
- The Good: Java
- The First Love: Groovy
- The Disappointment: Kotlin
- Conclusion
Inspiration
This blog is heavily inspired by Theo’s “I built the same app with 5 different stacks” video.
So I decided to do my take on it, but with two languages I already know well (Java and Groovy ) plus one new language I wanted to try for a long time: Kotlin .
Here are the code for the repos:
Understanding the application
I made the typical “Roundest Pokémon” programming exercise:
- Visit the final result at roundest-pokemon.pollito.tech
- I’m not planning to run the project forever, as I may need the computational power of the VPS it is running on for other personal projects. So if the link directs you nowhere, I’m sorry you are late.
I added a twist:
- On the top of the page, you can choose which backend system processes your vote (Next.js + _).
- No matter which backend you choose, all votes end up in the same place.
- The frontend application (the thing you interact with in the browser) is made in Next.js.
- Yes, I can do frontend.
- I’m a Next.js and Tailwind fanboy.
- I think react-query is the best package ever created (honorable mention swr ).
- I watch all Theo’s videos .
- I like to make fun of JQuery
even though half of the internet is made with it
… (flashbacks of JQuery + Bootstrap Argentinian government web pages still haunt me in my sleep).
Here’s the code for the Next.js frontend .
Quick metrics
This blog is mostly a comparison of backends. Spoiler alert, in terms of performance in this small sample project, they are the same.
Let’s do some comparison of the codebases:
- Overall the three backends are not that different.
- The frontend is its own different thing, not really much point in comparing it against the backend applications.
The following tables were generated using cloc .
Backend application Groovy
Language | files | blank | comment | code |
---|---|---|---|---|
Groovy | 22 | 174 | 1 | 727 |
YAML | 3 | 0 | 0 | 296 |
SQL | 1 | 1 | 2 | 157 |
Bourne Shell | 1 | 28 | 118 | 106 |
Gradle | 2 | 12 | 0 | 96 |
DOS Batch | 1 | 21 | 2 | 71 |
Markdown | 1 | 10 | 0 | 47 |
Dockerfile | 1 | 1 | 2 | 7 |
Properties | 1 | 0 | 0 | 7 |
SUM: | 33 | 247 | 125 | 1514 |
Backend application Java
Language | files | blank | comment | code |
---|---|---|---|---|
Java | 22 | 131 | 2 | 660 |
YAML | 3 | 0 | 0 | 296 |
SQL | 1 | 1 | 2 | 157 |
Gradle | 2 | 12 | 0 | 108 |
Bourne Shell | 1 | 28 | 118 | 106 |
DOS Batch | 1 | 21 | 2 | 71 |
Markdown | 1 | 9 | 0 | 45 |
Dockerfile | 1 | 1 | 2 | 7 |
Properties | 1 | 0 | 0 | 7 |
SUM: | 33 | 203 | 126 | 1457 |
Backend application Kotlin
Language | files | blank | comment | code |
---|---|---|---|---|
Kotlin | 24 | 134 | 4 | 589 |
YAML | 3 | 0 | 0 | 301 |
Gradle | 2 | 25 | 0 | 167 |
SQL | 1 | 1 | 2 | 157 |
Bourne Shell | 1 | 28 | 118 | 106 |
DOS Batch | 1 | 21 | 2 | 71 |
Markdown | 1 | 9 | 0 | 44 |
Dockerfile | 1 | 1 | 2 | 8 |
Properties | 1 | 0 | 0 | 7 |
SUM: | 35 | 219 | 128 | 1450 |
When it comes to deployment times, also there’s nothing remarkable.
- If it is a redeployment with no building, it takes around 1 minute and a half.
- If the deployment implies building, it takes around 4 minutes and a half.
I have all the backends with the same very conservative resource limits:
On idle they have acceptable CPU and memory usage. All of them present:
- CPU% = 0,2
- MEM = 270M
There’s no The Good, the Bad and the Ugly
All three options are totally valid for a serious big project, and they would fall into “The Good”.
I would say a better phrase would be “The Good, the First Love, and the Disappointment”. Let’s go one by one.
The Good: Java
Let’s start by inserting obvious public static void String main args
joke here.
Fun fact, public static void String main args
is no longer needed since Java 21
.
In one word, Java is reliable:
- There’s a certain comfort in knowing that you’re backed by a vast community and a wealth of documentation and best practices.
- Would be strange that you get an error that nobody else had before.
Everything just worked, probably because Java is what I’ve been doing for 8 hours a day, 5 days a week, for more than 2 years by now.
Java is not glamorous, but comfortable.
The First Love: Groovy
My journey with Groovy began back in 2021. I remember in the job interview I was only asked two things:
- Do you know Java?
- Do you know SQL?
It was a simpler time.
Without realizing, I had joined a project that was built using Grails , a very niche monolith framework that uses Groovy as its primary language.
- Fun fact: MercadoLibre heavily used Groovy and Grails before moving away to Go
.
- I suspect that the reason these particular projects were also using Grails was because someone from MercadoLibre started them. I don’t have any proof of it though.
I quickly fell in love with its expressive syntax and the way it aimed to make Java better by cutting down on boilerplate and embracing a more dynamic style.
- Semicolons? Optional.
- Checked exceptions? Handled.
- Java verbosity? Neutralized by closures and the
?.
safe navigation operator.
Yet Groovy remains the indie artist of JVM languages: beloved by Gradle buildscript writers and the few Grails developers that may exist out there, but never quite achieving Scala’s academic prestige or Kotlin’s JetBrains-backed fame.
Groovy relaxed typing
Groovy relaxed typing is a double edge sword.
During the writing of the Groovy version, I had a CORS issue. My first immediate suspect was a bad configured application.yml
(as I read the allowed origins from that file), but the solution was this:
I had as String
probably from an IntelliJ suggestion or ChatGPT copy-paste, but that was enough to break CORS in the application. These kind of mistakes simply don’t happen in Java.
Writing tests with Spock
You can use JUnit in a Groovy based project, but would be a waste to not use Spock (is like going to Madrid and not eating a tortilla).
I always found Spock syntax more readable, personal preference though. Here you have a snippet of code in Java Junit, and Groovy Spock, both testing the find Pokémon by id functionality
Java JUnit
@Test
void whenFindByIdThenReturnPokemon() {
when(pokemonRepository.findById(anyLong())).thenReturn(Optional.of(mock(Pokemon.class)));
assertNotNull(pokemonService.findById(1L));
}
Groovy Spock
def "when findById then return Pokemon"(){
given: "a mocked repository behaviour"
pokemonRepository.findById(_ as Long) >> Optional.of(new Pokemon())
when: "finding a pokemon"
def result = pokemonService.findById(1L)
then: "result is not null"
result != null
}
I would use Groovy again given the chance
Not because it’s objectively superior, but because maintaining code should feel like coming home. even if home has some leaky type checking and mysterious NoSuchMethodError ghosts in the closet. I guess I miss being part of a project I really care, and Groovy reminds me of those days.
The Disappointment: Kotlin
Disclaimer: This was my first time starting a Kotlin project solo, so maybe my bad experience is due to skill issue.
OpenAPI Generator didn’t work out of the box
I’m a big fan of OpenAPI Generator, and I don’t ever want to write a DTO ever again. Using the OpenAPI Generator Gradle Plugin was very simple in Java and Groovy, but in Kotlin I had two issues:
- DTO fields were declared as immutable using
val
, but I needed them to be mutable withvar
.- I ended up having to create a custom task that scanned the generated classes and swapped val for var.
- A parameter that should’ve been nullable (
List<String>?
), was not (it was missing the?
).- Same solution, created another custom task that did the replacement.
Those extra steps felt like a step backward in terms of efficiency. You can say “Bro just write the DTOs yourself”, to which I answer “I didn’t have to do that in Java and Groovy, why do I have to write them here?”
Handling java time in tests
You can also use JUnit in a Kotlin based project, but would be a waste to not give a try to MockK . It is quite close to JUnit syntax.
The problem arrived when it was unable to mock java.time.Instant
and java.time.format.DateTimeFormatter
.
- MockK simply couldn’t, or at least I was not able to find a way to do it.
- I had to introduce an interface just to abstract away the
java.time
functionality.- While this extra layer made the tests pass, it also added complexity that I hadn’t anticipated and somewhat muddied the clarity of the design.
If you are curious how MockK looks like, here’s a test on the find Pokémon by id functionality:
@Test
fun `when findById then return Pokemon`() {
val pokemon = Pokemon(name = "Bulbasaur", spriteUrl = "url")
every { pokemonRepository.findById(any<Long>()) } returns Optional.of(pokemon)
assertNotNull(pokemonService.findById(1L))
}
It was not bad
But it was the little things that didn’t convince me. Maybe my expectations for Kotlin were a bit too high, or perhaps I simply took a few wrong turns along the way. Despite these frustrations, I’m not closing the door on Kotlin entirely.
Conclusion
- With a solid foundation in Java, you can explore Groovy, Kotlin, and other languages in the JVM ecosystem.
- The image loading in the frontend app could be improved if I have the Pokémon images in the project
public
folder instead of relying on a GitHub api. Nonetheless, is not that painful of a loadtime. - I would’ve liked trying Scala , even did an initial research on the Play Framework , but got distracted by acquiring a VPS and the rest was history.