Java: Rethink Domain Primitives with Valhalla

dfa112 pts1 comments

Rethink Domain Primitives with Valhalla | Davide Angelocola

Davide Angelocola

Rethink Domain Primitives with Valhalla

16 May 2026

Your Compiler Is Already Part of Your Security Team made the case for domain primitives — types that encode business constraints, as opposed to Java primitives (int, double, long). Wrap an int in a PositiveInt and the compiler rejects invalid states forever. The recurring objection: a wrapper class per int is one heap object per value, plus a pointer to reach it. Fine at the boundary; questionable in a hot loop where allocations and cache misses dominate.

Project Valhalla changes the trade-off. Value classes let the JVM flatten the wrapper into the array slot, the register, the enclosing object — no header, no indirection. All experiments here run on openjdk 27-jep401ea3, the current Valhalla EA build implementing JEP 401.

The blocker: wrapper overhead

A class PositiveInt { final int v; } is 16 bytes on HotSpot — 12-byte header plus 4-byte int (8-byte header with -XX:+UseCompactObjectHeaders, production-ready since JDK 251) — and the array holding it stores 4-byte references rather than the values themselves. A stream processor carrying millions of PositiveInt sequence numbers, one per event, means millions of heap objects — each one a cache miss waiting to happen, each one tracked by the GC. The wrapper costs 4× the memory of the int it wraps, and the pointer chase costs an extra cache-line load per access — a cache miss on random access patterns.

The practical rule that I followed, until now, has been: refine your types at the boundary, then stop before you hit any performance-sensitive code. You can refine your boundaries; you cannot refine your hot path. Refined types stayed in the outermost layer; the loops over millions of events kept using raw int, raw float, raw String.

That was the friction. Value classes lift it — and the same pattern now fits far more use cases.

What is a refined type?

In Scala and similar languages, it is possible to narrow a base type with a predicate and get a compile-time rejection on invalid literal values. Java has no such mechanism — the constraint check runs at construction time, not compile time:

public class RefinedT> {

private final T value;

public Refined(PredicateT> validator, T value) {<br>if (!validator.test(value)) {<br>throw new IllegalArgumentException("invalid value");<br>this.value = value;

public final T getValue() {<br>return value;

// ...provide equals/hashCode/toString

A concrete refined type extends Refined and supplies the predicate — here, a positive integer:

private static class PositiveInt extends RefinedInteger> {

public PositiveInt(int value) {<br>super(i -> i > 0, value);

A utility class provides the static factories:

public class Refining {

public static PositiveInt positiveInt(int value) {<br>return new PositiveInt(value);

That 2019 gist only scratched the surface of the design space, and left performance as an open question — all primitive types were boxed. refined-type — a library of domain primitives backed by value classes — is the answer.

Enter Project Valhalla

Value classes are objects without identity. They carry behavior and invariants, but store like primitives — the JVM is free to inline them wherever they appear: into a register, into another object’s fields, into an array slot.

Records share the same compact carrier syntax but are identity classes — one heap object per value, references in arrays, object header included. The value keyword is the distinction: it tells the JVM this type has no identity, which is what allows flat layout.

Java 27 EA ships Project Valhalla preview2. The new keyword is value:

public value class PositiveInt implements RefinedIntPositiveInt> {<br>private final int value;

public PositiveInt(int value) {<br>if (value 0) {<br>throw new IllegalArgumentException("must be positive: " + value);<br>this.value = value;

@Override public int value() { return value; }

public interface RefinedIntT extends RefinedIntT>> extends ComparableT> {

int value();

@Override<br>default int compareTo(T that) {<br>return Integer.compare(this.value(), that.value());

RefinedInt replaces the generic Refined to avoid boxing. The F-bound (T extends RefinedInt) constrains compareTo to the same type — Probability.compareTo(Price) is a compile error.

Same shape as a regular wrapper. The constructor still runs. The validation still happens. The static guarantee — anywhere I see a PositiveInt, the value is positive — still holds.<br>The JVM is allowed to inline the fields wherever a PositiveInt lives — into a register, into another object, into an array slot. No header, no pointer chasing.

The pattern scales to multi-field types. Coordinate carries a Latitude and a Longitude, each a double-backed value class. The JVM inlines both doubles per slot — 16 bytes contiguous, no pointers:

public value class Coordinate {<br>private final Latitude latitude;<br>private final Longitude longitude;

public Coordinate(Latitude latitude,...

value positiveint public class primitives valhalla

Related Articles