spring-projects / spring-projects/spring-data-commons

KotlinInstantiationDelegate: cache parameter optionality to avoid Kotlin reflection on every instantiation

Open
#3,479 0 comments 0 reactions 1 assignee View on GitHub

@mp911de is already working on this.

Since Apr 27, 2026.

type: enhancement
Dominant language
Java
Stars
838
Forks
730
PR merge metrics
No merged PRs in 30d

Description

Description

KotlinInstantiationDelegate.extractInvocationArguments() calls KotlinDefaultMask.forConstructor() on every entity instantiation. This method internally calls KFunction.getParameters() and KParameter.isOptional() via Kotlin reflection for each parameter. Since constructor parameter optionality is fixed per entity type, this work is redundant and should be cached.

Profiling Evidence

In a production Spring Data MongoDB service handling ~10k requests/sec, KotlinDefaultMask.from + KotlinInstantiationDelegate.extractInvocationArguments together consume ~0.9% of service CPU. The affected entity classes have 8–35 constructor parameters with Kotlin defaults.

Flame graph shows the hotspot is entirely in KParameter.isOptional() and KFunction.getParameters() calls inside the KotlinDefaultMask.from() loop.

Root Cause

In KotlinInstantiationDelegate.extractInvocationArguments() (line 136 of current main):

KotlinDefaultMask defaultMask = KotlinDefaultMask.forConstructor(constructorFunction, it -> {
    int index = indexByKParameter.get(it);
    Parameter<Object, P> parameter = parameters.get(index);
    Class<Object> type = parameter.getType().getType();
    if (it.isOptional() && (params[index] == null)) {  // <-- Kotlin reflection per param
        ...
    }
    return true;
});

KotlinDefaultMask.forConstructor()KotlinDefaultMask.from() calls, on every instantiation:

  1. function.getParameters() — Kotlin reflection to get parameter list
  2. parameter.isOptional() — Kotlin reflection per parameter
  3. parameter.getKind() — Kotlin reflection per parameter

Proposed Fix

Pre-compute which parameters are optional and their bit positions in the KotlinInstantiationDelegate constructor (which runs once per entity type), then build the default mask directly from cached int[] arrays in extractInvocationArguments() without any Kotlin reflection.

The fix adds four fields to KotlinInstantiationDelegate:

  • int[] optionalParameterIndices — indices of optional parameters
  • int[] optionalParameterBitPositions — bit positions for mask computation
  • int defaultMaskSlotCount — number of int slots needed for the mask
  • boolean hasOptionalParameters — fast check to skip mask building

Benchmark Results

Microbenchmark comparing per-invocation reflection (current) vs cached metadata (proposed), 2M iterations each:

Scenario Before (ns/op) After (ns/op) Speedup
8 params, 8 optional, all defaulted 923 34 27x
8 params, 8 optional, all provided 645 36 18x
35 params, 32 optional, mixed 3,266 148 22x
2 params, 0 optional (baseline) 235 21 11x

Benchmark source (timing-loop smoke test, not JMH): https://gist.github.com/snuderl/122c8fa3cb0ea304aff122fc63a48442

Versions Affected

  • Spring Data Commons 4.0.x (current main)
  • All previous versions with KotlinClassGeneratingEntityInstantiator (since 3.1)

Impact

Any Spring Data application using Kotlin data classes with default parameters pays this reflection tax on every entity read from the database. The cost scales with parameter count and query volume.

I have a patch ready and will open a PR shortly.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.