I am a class storing a single public and immutable value and at runtime I'm allocated on stack. What's my name ?
Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
This new post from "One Scala feature per week" series presents value classes. The very first section mentions shortly value object pattern that is very similar to the main topic of the article. The next part part goes directly to the subject and presents some theoretical and important points about value classes. The 2 next parts shows some examples when value class doesn't and does need allocated memory.
Value object pattern
The idea behind value object pattern consists on representing data per value rather than per reference. It means that 2 different (in terms of memory addresses) value objects with the same value(s) are equal. From this short description you can already deduce that value objects are compared not through the identity (e.g. memory address, a database identifier) but by values they contain.
Value objects are usually small (e.g. amount with currency) and should be immutable in order to keep consistency in the business logic (since compared by values). Even though they could represent few values, they can still contain some helper logic. One of this kind of logic can be Java's String toUpperCase() method.
Value class
Scala's value class is very similar to the pattern described above. It also represents simple values that at JVM's level don't need an allocated place in the heap.
In order to create a value class, the following points need to be respected:
- extending AyVal - the value class must extend scala.AnyVal. It's a root class for all value types. As specified in the documentation, it "describes values not implemented as objects in the underlying host system"
- compile and runtime representations - the value class is represented differently on compile and run time. During the compilation it's exposed as a class. But during the execution (runtime), the value class is transformed to the value contained in it. For instance, the class Age(30) will be represented on runtime as int=30 allocated on stack and not on heap.
- single, publicly accessible val parameter in the constructor - the value class can contain only 1 val parameter in the constructor. So the following is not a valid value class:
class NotValueClass(val param1: Int, val param2: Int)
- no additional vals, vars or nested elements (traits, classes or objects) - the value class can't contain any additional nested elements. So the following snippet doesn't represent a valid value class:
class NotValueClass(val param1: Int) extends AnyVal { val param2 = param1 * 2 var wasCalled = false trait NotValueClassNestedTrait }
- only universal traits can be extended
- no override for equals and hashCode methods
- cannot be extended by other classes
- must be a top-level class or a member of a statically accessible object - it means that value class can't be nested in another non-static class but it can be in an object that lets to access to it statically:
// It's allowed - statically accessible member object ValueClassHolder { class Age(val age: Int) extends AnyVal } // Not allowed - to access the value class we must create an instance of NotStaticValueClassHolder class NotStaticValueClassHolder { class Age(val age: Int) extends AnyVal }
- when it's used in runtime type tests as for example in pattern matching
- when it's considered as other type, for instance:
def doSomething(someObject: Any) = someObject where someObject, even if it's defined as a value class, it's casted to Any type and allocated on the heap. The same rule applies for the case when the value class implements a trait (even universal one):
trait ImplementedTrait class Implementation extends AnyVal with ImplementedTrait // For the case below, the value class will be allocated since it's // represented by another type ImplementedTrait def takeImplementedTrait(trait: ImplementedTrait)
- when they're assigned to an array
Universal trait
A universal trait is a trait: extending Any, having only defs as members and not doing any initialization. An example of universal trait could be:
trait Printable extends Any { def print(): Unit = println(this) }
But under some circumstances, the value class will be compiled into an object and allocated on the heap:
Value class compilation
Before showing what happens under-the-hood when the value classes are compiled, let's show the tested code. To illustrate the compilation process, the below code will be used:
case class Age(val value: Int) extends AnyVal case class AgeWithChecks(val value: Int) extends AnyVal { def isAdult(): Boolean = value > 18 } case class AgeNotAnyVal(val value: Int) trait UniversalTraitAge extends Any { def isUniversalTrait: Boolean = true } case class AgeWithUniversalTrait(val value: Int) extends AnyVal with UniversalTraitAge // They don't compile trait NotUniversalTraitAge extends Any { val bornYear: Int def isUniversalTrait: Boolean = false } case class AgeWithNotUniversalTrait(val value: Int) extends NotUniversalTraitAge { override val bornYear: Int = 1900 } case class AgeExtendedByOtherClasses(val value: Int) extends AnyVal case class AgeExtendingAgeExtendedByOtherClasses(override val value: Int) extends AgeExtendedByOtherClasses(value) case class AgeWith3Args(val value1: Int, val value2: Int, val value3: Int) extends AnyVal case class AgeWithValueClassDependencies(val age: Age) extends AnyVal case class AgeWithVarsAndVals(val age: Int) extends AnyVal { val birthdayDay: Option[String] = None }
As you can correctly deduce, the compilator has some checks for the value classes definitions and the classes written after // They don't compile comment make the compilation fail with the following messages:
Error:(21, 7) field definition is not allowed in universal trait extending from class Any val bornYear: Int Error:(32, 83) illegal inheritance from final class AgeExtendedByOtherClasses case class AgeExtendingAgeExtendedByOtherClasses(override val value: Int) extends AgeExtendedByOtherClasses(value) Error:(32, 12) case class AgeExtendingAgeExtendedByOtherClasses has case ancestor com.waitingforcode.types.AgeExtendedByOtherClasses, but case-to-case inheritance is prohibited. To overcome this limitation, use extractors to pattern match on non-leaf nodes. case class AgeExtendingAgeExtendedByOtherClasses(override val value: Int) extends AgeExtendedByOtherClasses(value) Error:(34, 12) value class needs to have exactly one val parameter case class AgeWith3Args(val value1: Int, val value2: Int, val value3: Int) extends AnyVal Error:(36, 46) value class may not wrap another user-defined value class case class AgeWithValueClassDependencies(val age: Age) extends AnyVal Error:(39, 7) field definition is not allowed in value class val birthdayDay: Option[String] = None
Now when we try to see what happens if we compile only valid value classes (before the comment), we can write a simple main method and call javap -c -p on the compiled class:
object ValueClassCompilationCheck { def main(args: Array[String]): Unit = { val age = new Age(10) val ageWithChecks = new AgeWithChecks(30) val ageWithUniversalTrait = new AgeWithUniversalTrait(40) val ageNotAnyVal = new AgeNotAnyVal(50) } }
The command's output is:
public void main(java.lang.String[]); Code: 0: bipush 10 2: istore_2 3: bipush 30 5: istore_3 6: bipush 40 8: istore 4 10: new #17 // class com/waitingforcode/types/AgeNotAnyVal 13: dup 14: bipush 50 16: invokespecial #20 // Method com/waitingforcode/types/AgeNotAnyVal."":(I)V 19: astore 5 21: return
As you can clearly see, the value classes are defined as simple variables allocated on stack. They're first pushed onto the operand stack and later stored in a local variable with istore_${n} instruction. A different output is generated for not value class (AgeNotAnyVal) where new operator is used to create a new object and initialize it later with invokespecial.
Operand stack
The operand stack is a LIFO (last-in-first-out) stack associated to each frame (the frame is created when the method is invoked). It consists basically on push and pop operations. Some JVM instructions send the values to the operand stack while the other ones load them in order to make some operations within.
For instance, if two ints must be added, they're first sent to the operand stack. Later they're popped by a JVM instruction, added and the sum is sent back to the operand stack from where it can be used for further operations.
Necessary allocation examples
Now let's focus on the situations where, despite of the correct value class definition, the new object is allocated on the heap:
object ValueClassAllocated { def main(args: Array[String]): Unit = { // Assign to an array val agesArray: Array[AgeArray] = Array[AgeArray](AgeArray(1)) // Use in print(...) -> considered as other type or with implemented trait that is used in a method val ageWithInterface = new AgeImplementingInterface(4) showAge(ageWithInterface) // Use in pattern matching val age = AgePatternMatching(5) age match { case AgePatternMatching(_) => println("Got age") case _ => println("Got _") } } def showAge(age: Any): Unit = { } } case class AgeArray(val value: Int) extends AnyVal case class AgePatternMatching(val value: Int) extends AnyVal class AgeImplementingInterface(val value: Int) extends AnyVal
And the bytecode instructions look like (some lines were removed for better readability):
public void main(java.lang.String[]); Code: 0: getstatic #20 // Field scala/Array$.MODULE$:Lscala/Array$; 3: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 6: iconst_1 7: anewarray #27 // class com/waitingforcode/pattern/AgeArray 12: new #27 // class com/waitingforcode/pattern/AgeArray 17: invokespecial #30 // Method com/waitingforcode/pattern/AgeArray."":(I)V 20: aastore .. 40: istore 4 42: aload_0 43: new #50 // class com/waitingforcode/pattern/AgeImplementingInterface 46: dup 47: iload 4 49: invokespecial #51 // Method com/waitingforcode/pattern/AgeImplementingInterface." ":(I)V 52: invokevirtual #55 // Method showAge:(Ljava/lang/Object;)V 55: iconst_5 .. 60: istore 6 62: new #57 // class com/waitingforcode/pattern/AgePatternMatching 66: iload 6 68: invokespecial #58 // Method com/waitingforcode/pattern/AgePatternMatching." ":(I)V
Unlike the case from the previous section, here we can observe the creations of 3 new objects allocated on the heap.
The post shows the idea of value classes in Scala. The first section presented a more global idea of the value object pattern. The second part described the rules of the value classes creation. As we could see, some of rules make that even if the class extends expected parent and takes only 1 argument in the constructor, it'll be translated to an object and allocated on the heap. Both situations were illustrated in the code samples in 2 last sections. The analysis of the bytecode shown where the stack and heap allocation occured.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects