Top 10 Java 21 Best Practices with Complete Examples

Java 21 introduces powerful new features that improve performance, readability, and concurrency management. These updates help developers write better and more efficient code.

In this article, we will explore 10 best practices in Java 21 and provide complete examples to help you understand them better.

1️⃣ Use Virtual Threads for High-Performance Concurrency

Why?

Traditional threads in Java are heavyweight because they map directly to OS threads. Virtual Threads in Java 21 are lightweight, enabling the JVM to manage millions of threads efficiently.

Best Practice: Use Virtual Threads when handling many concurrent tasks, such as handling HTTP requests or database queries.

🔹 Complete Example: Virtual Threads in Action

import java.util.concurrent.Executors;

public class VirtualThreadExample {
    public static void main(String[] args) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 5; i++) {
                executor.submit(() -> {
                    System.out.println("Executing: " + Thread.currentThread());
                });
            }
        } // Executor closes automatically
    }
}

💡 Use Virtual Threads when working with large numbers of independent tasks.

2️⃣ Use StringTemplate for Cleaner String Formatting

Why?

Concatenating strings using + or String.format() can be error-prone and hard to read. Java 21 introduces StringTemplate, making string formatting safer and more readable.

Best Practice: Use StringTemplate instead of String.format().

🔹 Complete Example: Using StringTemplate for Formatting

import static java.lang.StringTemplate.STR;

public class StringTemplateExample {
    public static void main(String[] args) {
        String name = "Amit";
        int age = 30;

        // ✅ Java 21 String Template
        String message = STR."Hello, my name is \{name} and I am \{age} years old.";
        System.out.println(message);
    }
}

💡 This reduces the chances of formatting errors and makes code more readable.

3️⃣ Use Sequenced Collections for Ordered Data Structures

Why?

Before Java 21, collections like List, Set, and Map had inconsistent order-handling methods. Sequenced Collections ensure predictable ordering.

Best Practice: Use Sequenced Collections when working with ordered data.

🔹 Complete Example: Using SequencedSet for Ordered Elements

import java.util.SequencedSet;
import java.util.LinkedHashSet;

public class SequencedCollectionExample {
    public static void main(String[] args) {
        SequencedSet<String> cities = new LinkedHashSet<>();
        cities.add("Delhi");
        cities.add("Mumbai");
        cities.add("Bangalore");

        // ✅ Retrieves first and last elements
        System.out.println("First: " + cities.getFirst());
        System.out.println("Last: " + cities.getLast());
    }
}

💡 Use SequencedCollection when you need ordered elements with easy first/last access.

4️⃣ Use Pattern Matching in switch for Cleaner Code

Why?

Instead of writing multiple instanceof checks and explicit casts, Java 21 allows Pattern Matching in switch statements.

Best Practice: Use Pattern Matching in switch for type-safe, readable code.

🔹 Complete Example: Using Pattern Matching in switch

public class PatternMatchingExample {
    static void process(Object obj) {
        switch (obj) {
            case String s -> System.out.println("String: " + s);
            case Integer i -> System.out.println("Integer: " + i);
            case null -> System.out.println("Null value");
            default -> System.out.println("Unknown type");
        }
    }

    public static void main(String[] args) {
        process("Hello");
        process(42);
        process(null);
    }
}

💡 This makes switch statements more concise and eliminates redundant casting.

5️⃣ Use record for Simple Data Structures

Why?

Creating POJOs (Plain Old Java Objects) for simple data often requires boilerplate code. record simplifies this by auto-generating constructors, getters, equals, hashcode, and toString().

Best Practice: Use record for immutable data structures.

🔹 Complete Example: Defining and Using a record

public record User(String name, int age) {}

public class RecordExample {
    public static void main(String[] args) {
        User user = new User("Amit", 30);

        // ✅ Access fields without getters
        System.out.println(user.name());
        System.out.println(user.age());
    }
}

💡 Records are immutable and ideal for DTOs (Data Transfer Objects).

6️⃣ Use ScopedValues for Thread-Safe Context Management

Why?

Java 21 introduces ScopedValue as a faster alternative to ThreadLocal for passing context between threads.

Best Practice: Use ScopedValue to store request-related information efficiently.

🔹 Complete Example: Using ScopedValue in a Multi-Threaded Environment

import java.lang.ScopedValue;

public class ScopedValueExample {
    static final ScopedValue<String> USERNAME = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(USERNAME, "Amit").run(() -> {
            System.out.println("Current User: " + USERNAME.get());
        });
    }
}

💡 Use ScopedValue instead of ThreadLocal to avoid memory leaks.

7️⃣ Use Record Patterns for Object Destructuring

Why?

Instead of manually extracting values from records using getters, Java 21 allows Record Patterns for destructuring objects.

Best Practice: Use record patterns for cleaner object decomposition.

🔹 Complete Example: Using Record Patterns

record Point(int x, int y) {}

public class RecordPatternExample {
    static void printPoint(Point p) {
        switch (p) {
            case Point(int x, int y) -> System.out.println("X: " + x + ", Y: " + y);
        }
    }

    public static void main(String[] args) {
        printPoint(new Point(5, 10));
    }
}

💡 This simplifies object pattern matching in switch cases.

8️⃣ Use Structured Concurrency for Managing Threads

Why?

Handling multiple threads manually is error-prone. Structured Concurrency makes it easier to manage and join tasks.

Best Practice: Use StructuredTaskScope to manage dependent tasks.

🔹 Complete Example: Using Structured Concurrency

import java.util.concurrent.*;

public class StructuredConcurrencyExample {
    public static void main(String[] args) throws InterruptedException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<String> task1 = scope.fork(() -> "Task 1 Completed");
            Future<String> task2 = scope.fork(() -> "Task 2 Completed");

            scope.join();
            System.out.println(task1.resultNow());
            System.out.println(task2.resultNow());
        }
    }
}

💡 Use this when managing parallel tasks that depend on each other.

9️⃣ Use Foreign Function & Memory API for Native Code Interoperability

Why?

Interacting with native code in Java required JNI (Java Native Interface), which was complex and error-prone. Java 21 introduces the Foreign Function & Memory API, allowing direct interaction with native memory without JNI.

Best Practice: Use the Foreign Function & Memory API for high-performance native interop.

🔹 Complete Example: Allocating and Accessing Native Memory

import java.lang.foreign.*;
import java.nio.charset.StandardCharsets;

public class ForeignMemoryExample {
    public static void main(String[] args) {
        try (Arena arena = Arena.openConfined()) { // Confined Arena for memory management
            MemorySegment segment = arena.allocate(100); // Allocate 100 bytes
            
            // Store a string in native memory
            String message = "Hello from Java 21!";
            segment.asByteBuffer().put(message.getBytes(StandardCharsets.UTF_8));

            // Read back the message
            byte[] bytes = new byte[message.length()];
            segment.asByteBuffer().get(bytes);
            System.out.println(new String(bytes, StandardCharsets.UTF_8));
        } // Memory is automatically freed when arena closes
    }
}

💡 This is useful for high-performance applications that interact with C libraries.

🔟 Use String.repeat() Instead of Loops for Repeating Text

Why?

Repeating strings in Java used to require manual loops or StringBuilder. Java 11 introduced repeat(), which eliminates unnecessary iterations.

Best Practice: Use .repeat() instead of manually looping.

🔹 Complete Example: Generating Repeated Strings

public class RepeatExample {
    public static void main(String[] args) {
        // ❌ Old way: Using loops
        String stars = "";
        for (int i = 0; i < 5; i++) {
            stars += "*";
        }
        System.out.println(stars); // Output: *****

        // ✅ New way: Using repeat()
        System.out.println("*".repeat(5)); // Output: *****
    }
}

💡 Great for formatting text-based UI components, logs, and output formatting.

✅ Summary: Top 10 Java 21 Best Practices

Use Virtual Threads for scalable concurrency.
Use StringTemplate for cleaner string formatting.
Use SequencedCollection for ordered data.
Use Pattern Matching in switch for cleaner code.
Use record for immutable data structures.
Use ScopedValue instead of ThreadLocal.
Use Record Patterns for object destructuring.
Use Structured Concurrency for better thread management.
Use Foreign Function & Memory API for native interop.
Use .repeat() for simple string repetition.


Java 21 simplifies coding, improves performance, and enhances concurrency. These best practices will help you write faster, safer, and cleaner Java applications.

💬 Which Java 21 feature are you excited to use? Let me know! 🚀🔥

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare