When a Kotlin Sealed Class Explodes Your Spring Boot Startup Time (from 6s to 3 minutes)
Yesterday, I was working on a new application (more details coming very soon), and my Spring Boot app was starting up in just a few seconds. Then suddenly, disaster struck: 3 minutes to start.
It's pretty common to see gradual startup time degradation as you add features to an application (Quartz initialization, new beans to instrument, etc.), but this was way beyond acceptable drift.
Spoiler: the culprit was a Kotlin sealed class.
The Symptom
Started MyApplicationKt in 186.641 seconds
186 seconds to start a Spring Boot app locally. That's far from normal. At first, I suspected open database connections creating locks, but I quickly ruled that out. Going back a few commits brought everything back to normal. The culprit was probably two JPA entities with JSONB columns I had added.
Turns out I rarely use PostgreSQL's JSON functionality because in most cases, you can avoid it.
But here, I needed to store dynamic configuration for settings.
@Entity
class Theme(
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
val schema: ThemeSchema,
// ...
)
I tested several things to check if a specific JPA/Hibernate mechanism was the cause:
Disabling ddl-auto → No effect Switching to validate then none → Still 3 minutes Removing Quartz (which I wasn't using, leftover code from hakanai.io) → Nothing
Then I tried replacing ThemeSchema with a simple Map<String, String>. This change brought startup time back down to under 6 seconds.
The problem? ThemeSchema contained a sealed class:
data class ThemeSchema(
val settings: Map<String, SettingDefinition>
)
sealed class SettingDefinition {
abstract val label: String
abstract val default: Any
abstract val category: String
}
data class SelectSetting(...) : SettingDefinition()
data class ColorSetting(...) : SettingDefinition()
data class BooleanSetting(...) : SettingDefinition()
Why is this slow? When Hibernate + Jackson discover this structure at startup:
- Jackson needs to scan the classpath to find all subclasses of SettingDefinition
- It needs to figure out how to serialize/deserialize polymorphism
- Without explicit annotations, it has to try multiple strategies
- All of this using heavy reflection
All this scanning and reflection usage is expensive.
The Solution
Explicitly tell Jackson how to handle polymorphism:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes(
JsonSubTypes.Type(value = SelectSetting::class, name = "select"),
JsonSubTypes.Type(value = ColorSetting::class, name = "color"),
JsonSubTypes.Type(value = BooleanSetting::class, name = "boolean")
)
sealed class SettingDefinition {
// ...
}
With these annotations, Jackson no longer needs to scan: we explicitly tell it which subclasses exist and how to differentiate them in JSON.
Lessons Learned
Several takeaways, some I already knew, others I learned:
- making atomic commits makes it easy to identify the responsible commit(s) (reducing feedback loops, though I'd hope this has become an industry standard by now)
- performance issues are always approached the same way: hypothesis, test, measure, validate, etc. (I'm used to that one)
- Murphy's Law: whenever I touch something I'm not familiar with, I break something ^^
Here Murphy's Law was JSONB + Kotlin sealed classes + Jackson don't play well together by default. You need to explicitly define polymorphism with @JsonTypeInfo.
It might have been possible to do it differently, but less type-safe:
data class SettingDefinition(
val type: String, // "select", "color", "boolean"
val label: String,
val default: Any,
val category: String,
val options: List<SelectOption>? = null
)
No sealed class, no polymorphism, no problem. But I would have lost Kotlin's pattern matching and compiler guarantees.
Anyway, if you landed on this post because of a performance issue, take a close look at how you're helping Jackson understand how to serialize/deserialize your schema.


