Java Conventions
Records & Data Types
- Use records for immutable data transfer objects — no manual getters
- Use sealed interfaces with records for algebraic data types
- Never return null — use
Optional.empty() for absent values
- Prefer
List.of(), Map.of() for immutable collections
// BAD: mutable POJO with null returns
public class UserDto {
private String name;
public String getName() { return name; }
public void setName(String n) { this.name = n; }
}
// GOOD: immutable record
public record UserDto(String name, String email) {}
Optional Usage
- Return
Optional<T> from methods that may not produce a result
- Never call
get() without isPresent() — use orElseThrow() or map()
- Never use Optional as a field or method parameter — only as return type
// BAD: returns null
public User findById(String id) {
return users.get(id); // might be null
}
// GOOD: returns Optional
public Optional<User> findById(String id) {
return Optional.ofNullable(users.get(id));
}
Pattern Matching
- Use pattern matching in switch for sealed type hierarchies — the compiler enforces exhaustiveness
- Use guarded patterns with
when for conditional matching: case String s when s.length() > 10
- Prefer pattern matching over instanceof chains — reduces boilerplate and eliminates explicit casts
- Use unnamed variables (
_, Java 22+) for variables you must declare but never read
Virtual Threads (Java 21+)
- Use virtual threads for I/O-bound workloads — they are cheap (millions possible) and remove the need for reactive frameworks for simple concurrency
- Create with
Thread.ofVirtual().start() or Executors.newVirtualThreadPerTaskExecutor()
- Never pool virtual threads — they are meant to be created and discarded per task; pooling negates their purpose
- Avoid pinning: do not hold synchronized blocks during I/O — use ReentrantLock instead; synchronized pins the virtual thread to its carrier
Sequenced Collections (Java 21+)
- Use SequencedCollection methods for first/last access: getFirst(), getLast(), reversed() — replaces fragmented APIs across List, Deque, SortedSet
- Prefer reversed() over manual reverse iteration — returns a reversed view without copying
Collections & Streams
- Use Streams API for collection transformations — no manual loops for map/filter
- Prefer method references over lambdas when they improve clarity
- Use
toList() (Java 16+) instead of Collectors.toList() — less boilerplate and returns unmodifiable list
- Avoid side effects inside stream operations — side effects make streams unpredictable with parallel execution
Dependency Injection
- Use constructor injection — never field injection with
@Autowired; field injection hides dependencies and breaks testability
- Mark injected fields as
final — enforce immutability
- Keep constructors simple — no business logic in constructors
- Use
@Qualifier when multiple beans of the same type exist
Database & Security
- Use prepared statements for all database queries — prevent SQL injection
- Load secrets from environment variables or a secret manager — never hardcode
- Validate all user input at controller boundaries
- Use parameterized queries in JPA with
@Query or Criteria API
Testing
- Use JUnit 5 with
@ParameterizedTest for data-driven tests
- Use
@Nested classes to group related test cases
- Mock external dependencies with Mockito — never mock the class under test
- Name tests descriptively:
shouldReturnEmpty_whenUserNotFound
- Use Gradle with Kotlin DSL (build.gradle.kts) for new projects — type-safe, better IDE support than Groovy DSL
- Use version catalogs (libs.versions.toml) for centralized dependency version management
- Pin the Gradle wrapper version in gradle-wrapper.properties — ensures reproducible builds
Native Image (GraalVM)
- Consider GraalVM native image for cloud-native microservices — sub-second startup, reduced memory footprint
- Avoid reflection in hot paths — use compile-time DI (Micronaut, Quarkus) or Spring AOT processing
- Test with native image in CI — runtime behavior can differ from JVM mode
- Spring Boot 3.x includes built-in AOT support — use
spring-boot:process-aot for native compatibility
Error Handling
- Define custom exception hierarchies for domain errors
- Use
try-with-resources for all closeable resources — guarantees cleanup even when exceptions are thrown
- Log exceptions with structured context — never swallow silently
- Return appropriate HTTP status codes from exception handlers