Skip to main content
The Java SDK instruments methods with @VPoint. Three pieces cooperate: the annotation processor generates schema-bearing aggregate classes at compile time, the varianz-agent intercepts annotated methods at class-load time, and CoordinatorBinding connects the process to the coordinator. The Gradle plugin wires all three.

Canonical imports

These are the only correct paths — io.varianz.annotations.VPoint (missing the vpoint package), io.varianz.sdk.VPoint, and similar guesses do not exist.

Defining VPoints

  • Always set an explicit name. Omitted names derive from package/class/method names and break under refactoring.
  • Any class and method visibility works — the agent rewrites bytecode, so private/final/synchronized methods are fine. (One limit: for private inner classes, the generated aggregate can’t expose direct typed invocation; stages by name still work.)
  • Java records, plain beans and public-field DTOs are introspected automatically — fields from getters (getX()/isX()) or public fields, constructors matched by type compatibility (static factories like Quote.of(...) included). No @Struct needed for typical types. Field names convert camelCase → snake_case in CEL (basePrice → base_price; consecutive capitals stay together, so HTTPSPort → https_port). The Kotlin/KSP path does not do this — see Kotlin SDK.
  • @VPoint takes three attributes: name, wrapped (default true) and autoDerive (default true). Setting autoDerive = false requires an explicit @Struct on every parameter and return type instead of deriving them from getters and public fields. It is not an escape hatch for the types below — a declined type still bricks the aggregate with autoDerive = false.
  • For gRPC handlers, extract business logic into a plain-typed @VPoint method — keep StreamObserver out of schemas. See the pattern.
Signature restrictions. None of these is diagnosed at compile time. The aggregate throws ExceptionInInitializerError on first use, which disables every @VPoint in that class, including your own plain method calls. It surfaces only when the SDK is enabled, so a build with VARIANZ_ENABLED unset looks healthy.Supported and verified at 0.2.3: all primitives and their boxed returns, String, void, record, List<T>, Map<K,V>, Optional<T>, arrays, getter beans, public-field DTOs, varargs, static, private, protected, synchronized and final methods, final classes, interface default methods, and nested classes.A record works, but do not add @Struct to one: that registers zero fields and makes the type unconstructable from CEL.
The processor generates a PricingServiceVPoints aggregate in the same package, wrapping the method with stage execution and embedding the VPD schema.

How interception works

The agent transforms each @VPoint method at class load: the original body is cloned to <method>$varianzDirect (keeping line tables, so breakpoints still land in your source), and the method itself becomes a dispatch stub into the stage chain. Calling the method normally routes through the pipeline — no bind()-proxy calls needed:
With no stages registered, dispatch reads one volatile and calls the clone directly — the no-stage path is essentially free after JIT warmup. If the agent is not attached, the SDK prints one loud warning at first class load and @VPoint methods run their original bodies. The Gradle/Maven plugins attach the agent for test and run tasks; production launchers must attach it explicitly:

Connecting to the coordinator

Call CoordinatorBinding.init() early — before your server starts serving — then bind() each service instance to register its VPDs:
In Spring Boot, do both in a @PostConstruct. Session propagation uses the pull-based @VarianzId @Function(global = true) pattern — the full setup is in Propagate sessions.

Control annotations

Aggregate.bind(service) returning a typed proxy exists for advanced cases (test harnesses, programmatic stage composition) — normal code doesn’t need it.

Disabling interception

In order of preference: don’t attach the agent (Gradle weave.set(false), Maven -Dvarianz.skipAgent=true); disable at runtime with -Dvarianz.proc.weave=false; or opt out one method with @VPoint(wrapped = false).

Maven

On Java 22+, append --enable-native-access=ALL-UNNAMED to the <argLine>. To disable interception for a build, pass -Dvarianz.skipAgent=true (and declare the empty <varianz.agentArgs/> property above).

Native access (Java 22+)

The SDK loads its Rust runtime via System.load, a restricted method under JEP 472. On the classpath — which is what Maven, Gradle and java -jar use — the Varianz jars are in the unnamed module, so the only grant that applies is:
io.varianz.native_loader is an Automatic-Module-Name, not a real module. Naming it from the classpath produces WARNING: Unknown module: io.varianz.native_loader specified to --enable-native-access in addition to the warning you were trying to silence. On the module path (JPMS), grant the module that declares the native methods: --enable-native-access=binding,io.varianz.native_loader. Most SDK jars carry no Automatic-Module-Name, so their module names derive from the jar filenames (java.annotations, binding, binding.api, encoder.proto).
The Gradle plugin injects --enable-native-access=io.varianz.native_loader into every Test and JavaExec JVM on Java 22+ — including when weave.set(false). It is added by a CommandLineArgumentProvider, so setting jvmArgs yourself does not remove it. Expect one WARNING: Unknown module: io.varianz.native_loader line per test JVM. It is harmless — the grant that matters on the classpath is ALL-UNNAMED, above.

Troubleshooting

  • @VPoint methods not intercepted — almost always the agent isn’t attached; look for the [varianz] Varianz agent not attached warning. Check weave (Gradle) or the prepare-agent execution + @{varianz.agentArgs} (Maven). For custom launchers, verify with -Dvarianz.proc.debug=true (the agent logs its transformer registration).
  • Unresolved reference or NoClassDefFoundError for a ...VPoints class — on the Kotlin/KSP path, run gradle clean build first: adding a source file makes the next incremental round delete the aggregates for unchanged files, and the error persists across rebuilds until you clean. If a clean build doesn’t fix it, the processor genuinely didn’t run — with Gradle, compileJava should log Note: [Varianz] Generated adapters....
  • ExceptionInInitializerError … “binding hints that reference unregistered structs” — a type in that VPoint’s signature could not be derived as a struct, and the whole aggregate is now dead. See signature restrictions above. Adding @Struct fixes it only for a plain class; for an enum, an interface or a future it does not.
  • System.load warnings on Java 24+ — the JVM fork is missing the native-access flag; see above.