Rethinking Domain Primitives in Java with Project Valhalla
For years, Java developers have faced a frustrating trade-off between type safety and performance. On one hand, using domain primitives—types that encode business constraints (e.g., PositiveInt instead of a raw int)—allows the compiler to reject invalid states and prevents bugs. On the other hand, wrapping a primitive in a class creates a heap object with a header and a pointer, leading to significant memory overhead and cache misses in performance-critical "hot loops."
Historically, the rule of thumb was to refine types at the system boundaries and revert to raw primitives in the core logic to maintain speed. Project Valhalla changes this equation by introducing value classes, allowing the JVM to flatten wrappers directly into registers, enclosing objects, or array slots.
The Performance Tax of Wrapper Classes
To understand why Valhalla is necessary, we must look at the memory layout of a standard Java wrapper. A simple class PositiveInt { final int v; } typically consumes 16 bytes on HotSpot (a 12-byte header plus a 4-byte integer). Furthermore, an array of these objects doesn't store the values themselves, but rather 4-byte references to those objects scattered across the heap.
For a stream processor handling millions of events, this architecture is disastrous. The wrapper costs four times the memory of the raw int, and every access requires a "pointer chase," which often results in a cache-line load and a potential cache miss. This overhead is why developers have traditionally avoided refined types in hot paths.
Implementing Refined Types
In languages like Scala, refined types can be narrowed with predicates at compile time. Java lacks this native mechanism, meaning constraints must be checked during construction. A typical implementation involves a base Refined<T> class and concrete extensions:
private static class PositiveInt extends Refined<Integer> {
public PositiveInt(int value) {
super(i -> i > 0, value);
}
}
While this provides the static guarantee that any PositiveInt instance is valid, the boxing of primitives remains a bottleneck. This is where Project Valhalla's value keyword enters the picture.
Enter Project Valhalla and Value Classes
Value classes are objects without identity. They carry behavior and invariants but are stored like primitives. By using the value keyword, the developer tells the JVM that the type has no identity, granting the JVM permission to inline the fields wherever the object appears.
Consider the following implementation of a domain primitive using the Valhalla preview (JEP 401):
public value class PositiveInt implements RefinedInt<PositiveInt> {
private final int value;
public PositiveInt(int value) {
if (value <= 0) {
throw new IllegalArgumentException("must be positive: " + value);
}
this.value = value;
}
@Override public int value() { return value; }
}
By using a specialized interface like RefinedInt instead of a generic Refined<T>, boxing is avoided. The result is a type that provides the same safety as a class but the performance of a primitive.
Scaling to Multi-Field Types
This pattern extends beyond single-value wrappers. A Coordinate value class containing Latitude and Longitude (both double-backed value classes) allows the JVM to store 16 bytes of contiguous doubles per slot. In contrast, an identity-class equivalent would store references to scattered heap objects, each with its own header.
The Impact in Numbers
Benchmarks on 64-bit HotSpot with compressed oops demonstrate the dramatic difference in memory footprint for arrays of 10 elements:
int[10]: 56 bytes (Bare primitive)PositiveInt[10](Identity Class): 216 bytes (Domain-aware but expensive)PositiveInt[10](Value Class): 56 bytes (Cheap and domain-aware)
Value classes achieve the exact same memory layout and cache behavior as bare primitives, effectively removing the "performance tax" on domain modeling.
Critical Considerations for Adoption
While value classes are a powerful tool, they introduce several semantic shifts that developers must consider:
1. Equality Semantics
Value classes compare by value, not by pointer. The == operator on a value class tests field-wise substitutability, making it equivalent to a well-implemented equals() method. Migrating an existing identity class to a value class will silently change the behavior of any code relying on reference equality.
2. Nullability
Value classes cannot be null. Project Valhalla is introducing null-restricted references (e.g., PositiveInt! for non-null and PositiveInt? for nullable), which allows the compiler to eliminate null-checks in hot paths.
3. The Generics Gap
Currently, List<PositiveInt> and Optional<PositiveInt> still cause boxing because of type erasure. Generic specialization is not yet fully shipped; therefore, for maximum performance in critical paths, typed arrays (PositiveInt[]) should be used instead of collections.
4. Framework Integration
Most Java frameworks (Jackson, JPA, Bean Validation) expect primitives or String. Value types require thin adapters. For example, a Jackson deserializer for PositiveInt would simply wrap the p.getIntValue() call in a new PositiveInt constructor.
Conclusion
Project Valhalla is bridging the gap between high-level domain modeling and low-level performance. By allowing types to "code like a class but work like an int," Java is removing the final barrier to using strong domain primitives throughout an entire application. While still in preview, value classes promise a future where security and correctness—encoded in the type system—carry no performance penalty.