skip to content

Kotlin classes are final by default. Why does that break Spring and JPA, and what compiler plugins solve it?

level: juniorimportance: must knowfreq 75%

answer

  1. Kotlin = final by default
  2. Spring CGLIB proxies need open classes
  3. Hibernate needs no-args ctor + open entities
  4. kotlin-spring = all-open preset
  5. kotlin-jpa = no-arg preset

basics

~20 s

Kotlin classes can't be subclassed unless you write 'open'. Spring and Hibernate need to subclass your classes to add behavior. The all-open and no-arg plugins make the needed classes open and give them an empty constructor automatically.

solid answer

~30 s

By default every Kotlin class and method is `final` (the opposite of Java). Spring creates runtime CGLIB/proxy subclasses of `@Configuration`, `@Component`, `@Transactional`, etc. to add AOP/proxy behavior, and Hibernate subclasses `@Entity` classes for lazy loading — both fail on final classes. The `all-open` (kotlin-allopen) plugin makes classes annotated with configured annotations implicitly `open`. The `kotlin-spring` preset configures all-open for Spring's stereotype annotations. Separately, JPA/Hibernate requires a no-args constructor to instantiate entities reflectively; the `no-arg` (kotlin-noarg) plugin, via the `kotlin-jpa` preset, synthesizes a (synthetic, non-public) no-args constructor for `@Entity`/`@Embeddable`/`@MappedSuperclass` classes. You apply them as Gradle plugins (`kotlin("plugin.spring")`, `kotlin("plugin.jpa")`).

code

kotlin · 8 lines
kotlin
// Without plugins this fails at runtime under Hibernate/Spring:
@Entity
class Book(           // final, has no no-args constructor
    @Id val id: Long,
    var title: String,
)
// kotlin-jpa makes it effectively open + synthesizes Book()
// kotlin-spring makes @Service/@Configuration classes open

go deeper

for a junior

Knows Kotlin is final-by-default and that kotlin-spring/kotlin-jpa exist to fix framework breakage.

for a middle

Distinguishes all-open (removes final) from no-arg (adds constructor) and names the presets.

for a senior

Explains the runtime mechanics — CGLIB proxies, Hibernate lazy proxies, reflective instantiation — driving each requirement.

for a principal

Reasons about whether to prefer final-by-default discipline, immutable design, and when to lean on plugins vs. explicit modeling.

## The problem: Kotlin is final-by-default In Kotlin, every class and member function is `final` unless you explicitly mark it `open`. This is a deliberate design choice (Effective Java's "design for inheritance or prohibit it"). Java is the opposite: everything is open unless marked `final`. Two major Java frameworks assume they can subclass your code: - **Spring** generates **proxy subclasses at runtime** (CGLIB byte-code subclasses) for things like `@Configuration` classes, `@Transactional` methods, `@Async`, and other AOP. A proxy is a generated subclass that wraps each method to add cross-cutting behavior. It cannot subclass a `final` class or override a `final` method. - **Hibernate/JPA** subclasses `@Entity` classes to implement **lazy loading** (the entity returned is actually a proxy that loads data on first access). It also instantiates entities via **reflection using a no-args constructor**. So plain Kotlin classes break both frameworks: `final` blocks proxying, and the absence of a no-args constructor blocks reflective instantiation. ## Plugin 1: all-open (kotlin-allopen) The `all-open` compiler plugin makes a class — and its members — implicitly `open` **if** the class carries one of a configured set of annotations. You don't write `open` by hand. The **`kotlin-spring`** Gradle plugin is a preset of all-open pre-configured with Spring's annotations: `@Component` (and meta-annotated `@Service`/`@Repository`/`@Controller`/`@Configuration`), `@Async`, `@Transactional`, `@Cacheable`, `@SpringBootTest`. So an `@Service class Foo` becomes effectively `open class Foo` at compile time. ## Plugin 2: no-arg (kotlin-noarg) The `no-arg` plugin **synthesizes a zero-argument constructor** for classes carrying configured annotations. The generated constructor is **synthetic** — it isn't visible in normal Kotlin/Java source and you can't call it directly; frameworks reach it via reflection. The **`kotlin-jpa`** plugin is a preset of no-arg pre-configured for `@Entity`, `@Embeddable`, and `@MappedSuperclass`. (kotlin-jpa also pulls in all-open behavior for those JPA annotations, since Hibernate proxies entities too.) ## Applying them (Gradle Kotlin DSL) ```kotlin plugins { kotlin("jvm") version "2.1.0" kotlin("plugin.spring") version "2.1.0" // all-open preset for Spring kotlin("plugin.jpa") version "2.1.0" // no-arg (+ all-open) preset for JPA } ``` ## Key terms - **final / open**: `final` = cannot be subclassed/overridden; `open` = can be. - **Proxy / CGLIB subclass**: a generated subclass wrapping your methods to inject behavior. - **Synthetic constructor**: a constructor emitted by the compiler, not present in source, callable only reflectively. - **Stereotype annotation**: Spring's `@Component`-family markers.

  • Does all-open make every class in the project open?
    No — only classes annotated with a configured annotation (or meta-annotated by one). Untouched classes stay final.
  • If you mark a Spring bean class final by hand, what happens?
    all-open's effect overrides the default finality based on the annotation, so it still compiles open; but you generally shouldn't fight the plugin.

all-open unlocks a door frameworks need to walk through (subclassing); no-arg hands them a key to start the engine (a constructor) without you writing either.

saying these in an interview costs you the question

  • Claiming Kotlin classes are open by default (they're final)
  • Saying you must manually write 'open' on every entity/bean
  • Confusing the two plugins: all-open does NOT add a constructor
  • Thinking the plugins are runtime libraries rather than compile-time plugins
  • Believing all classes become open globally

context