| name | java-best-practices |
| description | Use this skill when working with Java code, Optional handling, CompletableFuture, records, sealed classes, or virtual threads. |
| version | 1.0.0 |
Java Best Practices
Guidance for writing type-safe, concurrent, and modern Java code targeting Java 17+ and Java 21 LTS. Covers null safety, concurrency patterns, and modern language features.
Core Principles
- Null safety first: Use Optional for return values, @Nullable/@NonNull for parameters
- Immutability preferred: Use records for data carriers, final fields where possible
- Explicit error handling: Use checked exceptions sparingly, prefer Result patterns
- Modern features: Leverage records, sealed classes, and pattern matching
- Virtual threads for IO: Use virtual threads (Java 21) for IO-bound operations
Type Safety
Use Optional for Return Values
public Optional<User> findById(String id) {
User user = userRepository.findById(id);
return Optional.ofNullable(user);
}
public User findById(String id) {
return userRepository.findById(id);
}
Never Use Optional as Parameter or Field
public void processUser(Optional<User> user) { ... }
public void processUser(@Nullable User user) { ... }
public void processUser(User user) { ... }
private Optional<String> middleName;
@Nullable
private String middleName;
Use Null Safety Annotations
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
public @NonNull User createUser(@NonNull String name, @Nullable String email) {
Objects.requireNonNull(name, "name cannot be null");
return new User(name, email);
}
Defensive Coding with Objects.requireNonNull
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
public UserService(UserRepository repository, EmailService emailService) {
this.repository = Objects.requireNonNull(repository, "repository cannot be null");
this.emailService = Objects.requireNonNull(emailService, "emailService cannot be null");
}
}
Null Handling
Optional Transformation with map/flatMap
String city = findUserById(id)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
Optional<Order> latestOrder = findUserById(id)
.flatMap(User::getLatestOrder);
Prefer orElseGet for Expensive Defaults
User user = findUserById(id)
.orElseGet(() -> userService.createDefaultUser());
User user = findUserById(id)
.orElse(userService.createDefaultUser());
Use orElseThrow for Required Values
User user = findUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
User user = findUserById(id)
.orElseThrow();
Avoid Optional.get() Without Check
User user = findUserById(id).get();
User user = findUserById(id)
.orElseThrow(() -> new IllegalStateException("Expected user to exist"));
Optional<User> userOpt = findUserById(id);
if (userOpt.isPresent()) {
User user = userOpt.get();
}
findUserById(id).ifPresent(user -> {
});
Filter with Optional
Optional<String> activeUserEmail = findUserById(id)
.filter(User::isActive)
.map(User::getEmail);
Optional<String> activeUserEmail = findUserById(id)
.flatMap(user -> user.isActive()
? Optional.of(user.getEmail())
: Optional.empty());
Optional in Streams
List<User> users = userIds.stream()
.map(this::findUserById)
.flatMap(Optional::stream)
.toList();
List<User> users = userIds.stream()
.map(this::findUserById)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
Concurrency
CompletableFuture Basics
CompletableFuture<User> future = CompletableFuture.supplyAsync(() -> {
return userRepository.findById(id);
});
CompletableFuture<String> emailFuture = future
.thenApply(User::getEmail)
.thenApply(String::toLowerCase);
CompletableFuture<UserProfile> profile = CompletableFuture
.allOf(userFuture, ordersFuture, preferencesFuture)
.thenApply(v -> new UserProfile(
userFuture.join(),
ordersFuture.join(),
preferencesFuture.join()
));
CompletableFuture Error Handling
CompletableFuture<User> userFuture = fetchUserAsync(id)
.exceptionally(ex -> {
log.error("Failed to fetch user: {}", id, ex);
return User.anonymous();
});
CompletableFuture<User> userFuture = fetchUserAsync(id)
.handle((user, ex) -> {
if (ex != null) {
log.warn("Fetch failed, using cache", ex);
return userCache.get(id);
}
return user;
});
fetchUserAsync(id)
.whenComplete((user, ex) -> {
if (ex != null) {
metrics.incrementFailure();
} else {
metrics.incrementSuccess();
}
});
Parallel Execution with CompletableFuture
public CompletableFuture<DashboardData> loadDashboard(String userId) {
CompletableFuture<User> userFuture = fetchUserAsync(userId);
CompletableFuture<List<Order>> ordersFuture = fetchOrdersAsync(userId);
CompletableFuture<List<Notification>> notificationsFuture = fetchNotificationsAsync(userId);
return CompletableFuture.allOf(userFuture, ordersFuture, notificationsFuture)
.thenApply(v -> new DashboardData(
userFuture.join(),
ordersFuture.join(),
notificationsFuture.join()
));
}
CompletableFuture<String> fastest = CompletableFuture.anyOf(
fetchFromPrimary(),
fetchFromSecondary(),
fetchFromCache()
).thenApply(result -> (String) result);
Virtual Threads (Java 21)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = urls.stream()
.map(url -> executor.submit(() -> fetchUrl(url)))
.toList();
List<String> results = new ArrayList<>();
for (Future<String> future : futures) {
results.add(future.get());
}
}
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new UserWithOrders(userTask.get(), ordersTask.get());
}
When to Use Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Response>> responses = requests.stream()
.map(req -> executor.submit(() -> httpClient.send(req)))
.toList();
}
ForkJoinPool.commonPool().submit(() -> {
});
ExecutorService Patterns
ExecutorService executor = new ThreadPoolExecutor(
4,
8,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
} finally {
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
try (var executor = Executors.newFixedThreadPool(4)) {
}
Thread Safety Patterns
public record User(String id, String name, String email) {}
private final ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();
private final AtomicInteger counter = new AtomicInteger(0);
private final AtomicReference<Config> config = new AtomicReference<>(defaultConfig);
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public User getUser(String id) {
lock.readLock().lock();
try {
return userCache.get(id);
} finally {
lock.readLock().unlock();
}
}
public void updateUser(User user) {
lock.writeLock().lock();
try {
userCache.put(user.id(), user);
} finally {
lock.writeLock().unlock();
}
}
Async Error Handling Patterns
public sealed interface AsyncResult<T> {
record Success<T>(T value) implements AsyncResult<T> {}
record Failure<T>(Throwable error) implements AsyncResult<T> {}
}
public CompletableFuture<AsyncResult<User>> fetchUserSafe(String id) {
return fetchUserAsync(id)
.<AsyncResult<User>>thenApply(AsyncResult.Success::new)
.exceptionally(AsyncResult.Failure::new);
}
CompletableFuture<User> userFuture = fetchUserAsync(id)
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
return User.anonymous();
}
throw new CompletionException(ex);
});
public <T> CompletableFuture<T> withRetry(
Supplier<CompletableFuture<T>> operation,
int maxRetries,
Duration initialDelay) {
return operation.get().exceptionallyCompose(ex -> {
if (maxRetries <= 0) {
return CompletableFuture.failedFuture(ex);
}
return CompletableFuture
.delayedExecutor(initialDelay.toMillis(), TimeUnit.MILLISECONDS)
.execute(() -> {});
});
}
Modern Java Features
Records (Java 17+)
public record User(String id, String name, String email) {}
public record User(String id, String name, String email) {
public User {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(name, "name cannot be null");
if (email != null && !email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
public record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
}
public record Point(int x, int y) {
public static Point origin() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
}
When to Use Records
public record UserDTO(String id, String name, String email) {}
public record Money(BigDecimal amount, Currency currency) {}
public record ApiResponse<T>(T data, int status, String message) {}
public record DatabaseConfig(String host, int port, String database) {}
public record CacheKey(String userId, String resourceType) {}
Sealed Classes (Java 17+)
public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
}
public record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
public record Triangle(double base, double height) implements Shape {
@Override
public double area() {
return 0.5 * base * height;
}
}
Sealed Classes for Result Types
public sealed interface Result<T>
permits Result.Success, Result.Failure {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error, Throwable cause) implements Result<T> {
public Failure(String error) {
this(error, null);
}
}
default T getOrThrow() {
return switch (this) {
case Success<T> s -> s.value();
case Failure<T> f -> throw new RuntimeException(f.error(), f.cause());
};
}
default T getOrElse(T defaultValue) {
return switch (this) {
case Success<T> s -> s.value();
case Failure<T> f -> defaultValue;
};
}
}
Pattern Matching for instanceof (Java 17+)
public String describe(Object obj) {
if (obj instanceof String s) {
return "String of length " + s.length();
}
if (obj instanceof Integer i) {
return "Integer: " + i;
}
if (obj instanceof List<?> list && !list.isEmpty()) {
return "Non-empty list with " + list.size() + " elements";
}
return "Unknown: " + obj;
}
public String describeOld(Object obj) {
if (obj instanceof String) {
String s = (String) obj;
return "String of length " + s.length();
}
}
Pattern Matching in Switch (Java 21+)
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
public String categorize(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "Large circle";
case Circle c -> "Small circle";
case Rectangle r when r.width() == r.height() -> "Square";
case Rectangle r -> "Rectangle";
case Triangle t -> "Triangle";
};
}
public String process(String input) {
return switch (input) {
case null -> "Input is null";
case String s when s.isBlank() -> "Input is blank";
case String s -> "Input: " + s;
};
}
Switch Expressions (Java 17+)
public String getDayType(DayOfWeek day) {
return switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
case SATURDAY, SUNDAY -> "Weekend";
};
}
public int calculate(Operation op, int a, int b) {
return switch (op) {
case ADD -> a + b;
case SUBTRACT -> a - b;
case MULTIPLY -> a * b;
case DIVIDE -> {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
yield a / b;
}
};
}
Quick Reference: Modern Features Checklist
Quick Reference: Concurrency Checklist
Quick Reference: Type Safety Checklist