The Shallow Immutability Trap: Hardening Java 21 Records for Virtual Threads
Virtual threads allow us to spawn millions of concurrent tasks with trivial overhead, but teams are quietly sabotaging their services by assuming records provide deep immutability. Java records only make component references final; passing mutable collections through them into massive concurrent fan-outs guarantees silent data corruption.
Why Most Developers Get This Wrong
-
Confusing shallow field immutability with deep thread safety: A
record UserPayload(List<String> roles)prevents reassignment ofroles, but any virtual thread can still callroles.add()concurrently without triggering a compile-time warning. -
Blinded by structured concurrency: Launching thousands of subtasks via
StructuredTaskScopewhile passing around unhardened records leads to unsynchronized, cross-thread mutations on shared heap structures. - Trusting generated accessors: Standard record accessors return direct object references, completely bypassing encapsulation and leaking internal mutable state to unpredictable caller threads.
The Right Way
Enforce strict, deep immutability at construction time so records remain genuinely read-only across arbitrary virtual thread boundaries.
- Use compact canonical constructors to replace incoming collection references with frozen copies via
List.copyOf(),Set.copyOf(), orMap.copyOf(). - Reject or defensively copy mutable components (such as
Date, arrays, or custom domain POJOs) before the instance finishes instantiation. - Avoid exposing raw, mutable third-party objects inside records—wrap them in unmodifiable adapters or domain primitives.
Want to go deeper? javalld.com — machine coding interview problems with working Java code and full execution traces.
Show Me The Code
Here is how to properly harden a record against concurrent mutation across millions of virtual threads:
public record OrderBatch(String batchId, List<UUID> orderIds, Instant createdAt) {
// Compact canonical constructor enforces deep immutability
public OrderBatch {
Objects.requireNonNull(batchId, "batchId cannot be null");
Objects.requireNonNull(createdAt, "createdAt cannot be null");
// List.copyOf handles null-checking of elements and guarantees an unmodifiable list
orderIds = List.copyOf(orderIds);
}
}
If an external virtual thread attempts orderBatch.orderIds().add(newId), it immediately fails fast with an UnsupportedOperationException instead of corrupting shared memory.
Key Takeaways
- Records guarantee shallow immutability only;
finalreferences to mutable objects are not thread-safe. -
List.copyOf()inside a compact constructor provides instant, fail-fast protection against concurrent modification bugs. - As virtual thread counts scale into the millions, defensive copying at record boundaries is mandatory for state integrity.
Top comments (0)