skip to content

At what stage do all-open / no-arg operate, and what are the consequences for tooling, final-by-design, and modules that don't apply them?

level: seniorimportance: nice to knowfreq 28%

answer

  1. Compiler plugins -> bytecode, not runtime
  2. Per-module, compile-time effect
  3. Invisible in source (still reads final)
  4. Weakens final-by-default for trigger set
  5. No trigger annotation = no effect

basics

~20 s

They run inside the Kotlin compiler while building each module, changing the generated bytecode. They aren't libraries and don't run at runtime. A module only gets the effect if it applies the plugin, and the change is invisible in your source.

solid answer

~60 s

Both are **compiler plugins**: they hook into the Kotlin compiler's frontend/backend and alter the **generated bytecode** of the module being compiled — making triggered classes `open` or emitting a synthetic no-args constructor. Key consequences: (1) They are **compile-time, per-module** — a class is only opened/given a constructor in the module that compiles it with the plugin applied; consuming modules see the already-modified bytecode but their own classes are unaffected unless they apply the plugin too. (2) The effect is **invisible in source** — your `.kt` still reads `final`/no constructor, which can surprise readers and tools; IDE support reflects it but plain text review doesn't. (3) They subtly **weaken final-by-default** for the triggered set, so design-for-inheritance discipline is partly bypassed — be deliberate about which annotations trigger opening. (4) They interact with **incremental compilation and KSP/kapt** ordering but don't conflict semantically. (5) Test code (`@SpringBootTest`) is covered by kotlin-spring's annotation set. They cannot open a class lacking the trigger annotation, and they don't affect `final` members you mark explicitly inconsistently.

code

kotlin · 8 lines
kotlin
// Source (module applies kotlin-spring):
@Service
class Reporting    // reads 'final' in source

// Emitted bytecode (decompiled view):
// public class Reporting { ... }   // NOT final -> Spring can proxy

class Helper       // no annotation -> stays final in bytecode too

go deeper

for a junior

Knows they're build-time plugins, not runtime, at a basic level.

for a middle

Explains bytecode modification and that only annotated classes are affected.

for a senior

Articulates per-module compile-time scope, source-invisibility surprises, and the final-by-default trade-off.

for a principal

Sets policy on trigger-set breadth, documents plugin presence for reviewers/tooling, and reasons about the architectural cost of pervasive openness.

## Where they run `all-open` and `no-arg` are **Kotlin compiler plugins** — code that runs *inside* `kotlinc` while it builds a module. They are not runtime dependencies; nothing ships a library that "opens classes at runtime." The plugin inspects each class's annotations during compilation and: - **all-open**: clears the `final` flag on the class and its members in the emitted bytecode. - **no-arg**: emits an extra `<init>()` method marked **synthetic** in the bytecode. The result is ordinary `.class` files; consumers and frameworks just see normal JVM bytecode. ## Per-module, compile-time consequence The transformation happens **only when that module is compiled with the plugin applied**. Practical implications: - If module A applies kotlin-spring and module B doesn't, only A's beans are opened. B's classes stay final even if annotated, unless B also applies the plugin. - Once compiled, the openness/constructor is baked into the bytecode; downstream modules don't need the plugin to *use* an already-opened class. ## Invisible-in-source surprise Your source still literally says `class Foo` (final) and shows no extra constructor. The plugin's effect exists only in bytecode. Consequences: - Code review of raw `.kt` doesn't reveal that `Foo` is open — reviewers must know the plugin is applied. - Tools that reason purely from source text (some linters, naive codegen) may misjudge finality. - IntelliJ's Kotlin plugin understands all-open/no-arg, so IDE inspections and navigation reflect reality. ## Tension with final-by-default Kotlin chose final-by-default to force "design for inheritance or prohibit it." all-open quietly re-opens a chosen annotation set. That's a pragmatic trade-off for framework compatibility, but it means those classes are now extensible whether you intended it or not. Best practice: keep the trigger set tight (the presets are scoped to framework annotations) and don't broaden it casually. ## Interaction with other tooling - **kapt/KSP**: annotation processors run alongside; all-open/no-arg operate on the produced classes. No semantic conflict, though build-graph ordering matters. - **Incremental compilation**: changing the plugin's annotation list invalidates affected classes. - **Tests**: `kotlin-spring`'s annotation set includes `@SpringBootTest`, so Spring can proxy test classes; otherwise final test classes could block context proxying. ## Limits ```kotlin class PlainBean // no trigger annotation -> stays final even with kotlin-spring applied ``` The plugin does nothing to classes lacking a configured (or meta-) annotation. It cannot, for example, magically open a third-party final class you didn't annotate. ## Summary Think of them as build-time bytecode rewriters scoped by annotation and by module. Powerful and invisible — which is exactly why a senior engineer keeps the trigger set deliberate and documents that the plugins are in play.

  • If you decompile a kotlin-spring class, what's different from the source?
    The class (and members) lack the final flag in bytecode even though the source shows none of that change; no-arg additionally shows a synthetic <init>().
  • Does a downstream module need the plugin to subclass an already-opened class?
    No — the openness is in the published bytecode, so any consumer can extend it without applying the plugin themselves.

Like a printer that quietly removes a 'do not copy' watermark only on pages stamped with a specific mark — the original document still shows the watermark, but the printout doesn't.

saying these in an interview costs you the question

  • Calling them runtime libraries or reflection tricks
  • Thinking the effect applies project-wide regardless of which module applies the plugin
  • Assuming the source shows the class as open
  • Believing it can open un-annotated or third-party classes
  • Ignoring the erosion of final-by-default discipline

context