DEV Community

Machine coding Master
Machine coding Master

Posted on

The Shallow Immutability Trap: Hardening Java 21 Records for Virtual Threads

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 of roles, but any virtual thread can still call roles.add() concurrently without triggering a compile-time warning.
  • Blinded by structured concurrency: Launching thousands of subtasks via StructuredTaskScope while 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(), or Map.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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; final references 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)