Metadata
| Field | Value |
|---|---|
| Type | context |
| Applies to | java, maven, gradle, junit, spring, jakarta, quarkus, mockito, testcontainers, hibernate, jpa |
| File extensions | .java |
Java Coding Standards
Core Principles
- Simplicity: Simple, understandable code
- Readability: Readability over cleverness
- Maintainability: Code that’s easy to maintain
- Testability: Code that’s easy to test
- SOLID: Follow SOLID principles for object-oriented design
- DRY: Don’t Repeat Yourself - but don’t overdo it
General Rules
- Early Returns: Use early returns to avoid nesting
- Descriptive Names: Meaningful names for classes, methods, and variables
- Minimal Changes: Only change relevant code parts
- No Over-Engineering: No unnecessary complexity
- Immutability: Prefer immutable objects where possible
- Minimal Comments: Code should be self-explanatory. No redundant comments!
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | UserService, OrderRepository |
| Interfaces | PascalCase | UserRepository, PaymentProcessor |
| Methods | camelCase | getUserById, calculateTotal |
| Variables | camelCase | firstName, totalAmount |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, DEFAULT_TIMEOUT |
| Packages | lowercase.dot.separated | com.example.service, com.example.repository |
| Test Classes | ClassNameTest | UserServiceTest, OrderRepositoryTest |
| Test Methods | descriptive_snake_case or camelCase | shouldReturnUserWhenIdExists |
Project Structure
Maven Project
myproject/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/myapp/
│ │ │ ├── Application.java # Main entry point
│ │ │ ├── config/
│ │ │ │ └── AppConfig.java # Configuration
│ │ │ ├── domain/
│ │ │ │ └── User.java # Domain models
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java # Data access
│ │ │ ├── service/
│ │ │ │ └── UserService.java # Business logic
│ │ │ └── controller/
│ │ │ └── UserController.java # REST endpoints
│ │ └── resources/
│ │ ├── application.properties
│ │ └── application-dev.properties
│ └── test/
│ ├── java/
│ │ └── com/example/myapp/
│ │ ├── service/
│ │ │ └── UserServiceTest.java
│ │ └── repository/
│ │ └── UserRepositoryTest.java
│ └── resources/
│ └── application-test.properties
└── README.mdGradle Project
myproject/
├── build.gradle or build.gradle.kts
├── settings.gradle or settings.gradle.kts
├── src/
│ ├── main/
│ │ └── java/... # Same structure as Maven
│ └── test/
│ └── java/... # Same structure as Maven
└── README.mdModern Java Features
Recommended: Use the latest LTS for new projects (currently Java 21 or Java 25).
Java 17 Features
Records (Immutable Data)
// Replace verbose POJOs with records
public record User(String id, String name, String email) {
// Compact constructor for validation
public User {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
}
// Custom methods allowed
public String displayName() {
return name.toUpperCase();
}
}
// Usage
var user = new User("1", "John Doe", "john@example.com");
System.out.println(user.name()); // Auto-generated accessorSealed Classes (Restricted Hierarchies)
// Define closed set of subclasses
public sealed interface Result<T>
permits Success, Failure {
}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String error) implements Result<T> {}
// Pattern matching exhaustiveness
public <T> void handleResult(Result<T> result) {
switch (result) {
case Success<T> s -> System.out.println("Success: " + s.value());
case Failure<T> f -> System.out.println("Error: " + f.error());
// No default needed - compiler knows all cases
}
}Pattern Matching (instanceof)
// Old way
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
// Modern way - pattern matching
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
// Pattern matching in switch
public String formatValue(Object obj) {
return switch (obj) {
case Integer i -> "Number: " + i;
case String s -> "Text: " + s;
case null -> "null";
default -> "Unknown: " + obj;
};
}Text Blocks (Multi-line Strings)
// Old way
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30\n" +
"}";
// Modern way - text block
String json = """
{
"name": "John",
"age": 30
}
""";Switch Expressions
// Old switch statement
String result;
switch (day) {
case MONDAY:
case FRIDAY:
result = "Work";
break;
case SATURDAY:
case SUNDAY:
result = "Weekend";
break;
default:
result = "Unknown";
}
// Modern switch expression
String result = switch (day) {
case MONDAY, FRIDAY -> "Work";
case SATURDAY, SUNDAY -> "Weekend";
default -> "Unknown";
};Java 21 Features
Virtual Threads
// Traditional platform threads - expensive, limited scalability
try (var executor = Executors.newFixedThreadPool(100)) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> fetchData());
}
}
// Virtual threads - lightweight, millions possible
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> fetchData());
}
}
// Start virtual thread directly
Thread.startVirtualThread(() -> {
// Task code
});
// Structured concurrency (preview in Java 21)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser(id));
Future<List<Order>> orders = scope.fork(() -> fetchOrders(id));
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Throw if any failed
return new UserDetails(user.resultNow(), orders.resultNow());
}Sequenced Collections
// New interfaces: SequencedCollection, SequencedSet, SequencedMap
// Get first and last elements
List<String> list = List.of("a", "b", "c");
String first = list.getFirst(); // "a"
String last = list.getLast(); // "c"
// Reversed view (not a copy!)
List<String> reversed = list.reversed();
// Works with Set
LinkedHashSet<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
set.addFirst("z"); // z, a, b, c
set.addLast("x"); // z, a, b, c, x
// Works with Map
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.putFirst("first", 1);
map.putLast("last", 99);Pattern Matching for switch (finalized)
// Pattern matching with null handling
String formatted = switch (obj) {
case null -> "null";
case Integer i -> "Number: " + i;
case String s -> "Text: " + s;
case List<?> list -> "List of " + list.size() + " items";
default -> "Unknown";
};
// Guard patterns
String category = switch (value) {
case Integer i when i < 0 -> "Negative";
case Integer i when i == 0 -> "Zero";
case Integer i -> "Positive";
default -> "Not a number";
};Record Patterns (finalized)
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
// Deconstruct records in patterns
static void printPoint(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println("x: " + x + ", y: " + y);
}
}
// Nested deconstruction
static void printCircle(Object obj) {
if (obj instanceof Circle(Point(int x, int y), int r)) {
System.out.println("Circle at (" + x + ", " + y + ") with radius " + r);
}
}
// In switch
static String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
case Circle(Point(int x, int y), int r) ->
"Circle at (" + x + ", " + y + ") radius " + r;
default -> "Unknown shape";
};
}Java 25 Features
Flexible Main Methods
// No longer need public static void main(String[] args)
// Simple main - for beginners and scripts
void main() {
System.out.println("Hello World");
}
// With arguments (if needed)
void main(String[] args) {
System.out.println("Args: " + Arrays.toString(args));
}
// Instance main (access to instance fields/methods)
class App {
private String message = "Hello";
void main() {
System.out.println(message); // Access instance field
greet(); // Call instance method
}
void greet() {
System.out.println("Welcome");
}
}Scoped Values (Alternative to ThreadLocal)
// ThreadLocal (old way) - must be cleaned up manually
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
// Scoped Values (new way) - automatically cleaned up
public static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// Set scoped value (automatically reverted when block exits)
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// Value is available here
User currentUser = CURRENT_USER.get();
processRequest(currentUser);
// Value automatically cleared when block exits
});
// Inherited by virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
executor.submit(() -> {
// Virtual thread inherits scoped value
User u = CURRENT_USER.get();
handleTask(u);
});
});
}Primitive Pattern Matching (Preview)
// Pattern matching now works with primitives
static String classify(int value) {
return switch (value) {
case 0 -> "zero";
case int i when i > 0 -> "positive";
case int i when i < 0 -> "negative";
};
}
// Type patterns for primitives
Object obj = 42;
if (obj instanceof int i) {
System.out.println("Integer: " + i);
}Gatherers (Custom Stream Operations)
// Create custom intermediate stream operations
// Built-in gatherers
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowFixed(2)) // [[1,2], [3,4], [5]]
.toList();
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(2)) // [[1,2], [2,3], [3,4], [4,5]]
.toList();
// Custom gatherer example (simplified)
var sumAndCount = Gatherer.of(
() -> new long[2], // [sum, count]
(state, element, downstream) -> {
state[0] += element;
state[1]++;
return true;
},
(state, downstream) -> {
downstream.push(state[0] / (double) state[1]);
}
);
double average = Stream.of(1, 2, 3, 4, 5)
.gather(sumAndCount)
.findFirst()
.orElse(0.0);Code Organization
Visibility Modifiers
// Use the most restrictive visibility possible
public class UserService {
private final UserRepository repository; // private - internal state
public UserService(UserRepository repository) { // public - API
this.repository = repository;
}
public User findById(String id) { // public - API
return validateAndFetch(id);
}
private User validateAndFetch(String id) { // private - internal logic
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("ID cannot be blank");
}
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}SOLID Principles
Single Responsibility Principle
// BAD - class does too much
public class UserService {
public void createUser(User user) { /* ... */ }
public void sendEmail(String to, String message) { /* ... */ }
public void logActivity(String activity) { /* ... */ }
}
// GOOD - single responsibility
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
private final AuditService auditService;
public void createUser(User user) {
repository.save(user);
emailService.sendWelcomeEmail(user);
auditService.logUserCreation(user.id());
}
}Dependency Inversion
// GOOD - depend on abstractions, not implementations
public class UserService {
private final UserRepository repository; // interface, not concrete class
public UserService(UserRepository repository) {
this.repository = repository;
}
}Exception Handling
Checked vs Unchecked
// Checked exceptions for recoverable errors (use sparingly)
public class UserNotFoundException extends Exception {
public UserNotFoundException(String userId) {
super("User not found: " + userId);
}
}
// Unchecked exceptions for programming errors (preferred)
public class InvalidUserIdException extends RuntimeException {
public InvalidUserIdException(String userId) {
super("Invalid user ID: " + userId);
}
}Try-with-Resources
// Automatic resource management
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
// reader automatically closed
} catch (IOException e) {
throw new UncheckedIOException(e);
}
// Multiple resources
try (var inputStream = new FileInputStream("in.txt");
var outputStream = new FileOutputStream("out.txt")) {
// Both automatically closed in reverse order
}Custom Exception Hierarchies
// Base exception for domain
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
}
// Specific exceptions
public class UserNotFoundException extends DomainException {
public UserNotFoundException(String userId) {
super("User not found: " + userId);
}
}
public class InvalidEmailException extends DomainException {
public InvalidEmailException(String email) {
super("Invalid email: " + email);
}
}Collections & Streams API
When to Use Which Collection
// List - ordered, allows duplicates
List<String> names = new ArrayList<>();
// Set - no duplicates
Set<String> uniqueNames = new HashSet<>();
// Map - key-value pairs
Map<String, User> userById = new HashMap<>();
// Prefer List.of(), Set.of(), Map.of() for immutable collections
List<String> immutableList = List.of("a", "b", "c");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("a", 1, "b", 2);Stream Best Practices
// Filter and map
List<String> activeUserNames = users.stream()
.filter(User::isActive)
.map(User::name)
.toList(); // Java 16+, or .collect(Collectors.toList())
// Find first
Optional<User> firstAdmin = users.stream()
.filter(User::isAdmin)
.findFirst();
// Reduce
int totalAge = users.stream()
.mapToInt(User::age)
.sum();
// Group by
Map<String, List<User>> usersByRole = users.stream()
.collect(Collectors.groupingBy(User::role));
// Don't reuse streams (they're one-time use)
// BAD
var stream = users.stream();
stream.filter(...).toList();
stream.map(...).toList(); // IllegalStateException
// GOOD
users.stream().filter(...).toList();
users.stream().map(...).toList();Optional & Null Handling
When to Use Optional
// GOOD - Optional for return values (absence is expected)
public Optional<User> findUserById(String id) {
return repository.findById(id);
}
// BAD - don't use Optional for parameters or fields
public void processUser(Optional<User> user) { /* avoid */ }
private Optional<User> currentUser; /* avoid */
// GOOD - use null for parameters if optional
public void processUser(User user) {
if (user != null) {
// process
}
}Optional Best Practices
// Chain operations
String userName = userService.findUserById(id)
.map(User::name)
.map(String::toUpperCase)
.orElse("UNKNOWN");
// Throw exception if absent
User user = userService.findUserById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// Execute action if present
userService.findUserById(id)
.ifPresent(user -> emailService.sendWelcome(user));
// Don't use Optional.get() without checking
// BAD
Optional<User> maybeUser = findUser(id);
User user = maybeUser.get(); // NoSuchElementException if empty
// GOOD
User user = maybeUser.orElseThrow(); // More explicitNull Safety
// Use Objects.requireNonNull for validation
public User(String id, String name) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.name = Objects.requireNonNull(name, "name cannot be null");
}
// Prefer Objects utilities
String result = Objects.requireNonNullElse(value, "default");
boolean equals = Objects.equals(a, b); // null-safe equalsTesting Fundamentals
JUnit 5 Basics
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
private UserService service;
private UserRepository repository;
@BeforeEach
void setUp() {
repository = new InMemoryUserRepository();
service = new UserService(repository);
}
@Test
void shouldReturnUserWhenIdExists() {
// Given
var user = new User("1", "John", "john@example.com");
repository.save(user);
// When
var result = service.findById("1");
// Then
assertTrue(result.isPresent());
assertEquals("John", result.get().name());
}
@Test
void shouldThrowWhenIdIsNull() {
assertThrows(IllegalArgumentException.class,
() -> service.findById(null));
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t"})
void shouldThrowWhenIdIsBlank(String id) {
assertThrows(IllegalArgumentException.class,
() -> service.findById(id));
}
}Mockito for Mocking
import org.mockito.*;
import static org.mockito.Mockito.*;
class UserServiceTest {
@Mock
private UserRepository repository;
@InjectMocks
private UserService service;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldCallRepositoryWhenFindingUser() {
// Given
var user = new User("1", "John", "john@example.com");
when(repository.findById("1")).thenReturn(Optional.of(user));
// When
var result = service.findById("1");
// Then
verify(repository).findById("1");
assertTrue(result.isPresent());
assertEquals("John", result.get().name());
}
}Test Naming
// Descriptive names - what scenario, what expected
@Test
void shouldReturnEmptyWhenUserNotFound() { }
@Test
void shouldThrowExceptionWhenEmailIsInvalid() { }
@Test
void shouldCalculateDiscountWhenUserIsVip() { }Build Tool Awareness
Maven Dependencies (pom.xml)
<dependencies>
<!-- Core dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Gradle Dependencies (build.gradle.kts)
dependencies {
// Core dependencies
implementation("org.springframework.boot:spring-boot-starter-web")
// Test dependencies
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core")
}Project Conventions
// Maven standard directory layout
src/main/java - Production code
src/main/resources - Configuration files
src/test/java - Test code
src/test/resources - Test configuration
// Gradle uses same layout
// Package structure matches directory structure
// com.example.myapp.service -> src/main/java/com/example/myapp/service/Recommended Tooling
| Tool | Purpose |
|---|---|
maven or gradle | Build automation |
junit-jupiter | Testing framework (JUnit 5) |
mockito | Mocking framework |
checkstyle | Code style checking |
spotbugs | Static bug detection |
jacoco | Code coverage |
maven-enforcer | Dependency management rules |
testcontainers | Integration testing with Docker |
Production Best Practices
- Immutability - Prefer records and final fields, reduces bugs
- Dependency Injection - Constructor injection over field injection
- Fail Fast - Validate inputs immediately, throw exceptions early
- Explicit over Implicit - Clear code over clever code
- Resource Management - Always use try-with-resources for I/O
- Optional for Return Types - Signal absence without null
- Stream API - Use streams for collection operations, but don’t overuse
- Modern Java - Use records, sealed classes, pattern matching
- Minimal Checked Exceptions - Prefer unchecked for most cases
- Constructor Validation - Validate in constructor or compact constructor (records)
- Descriptive Names - Method names should explain intent
- Single Responsibility - Classes and methods should do one thing
- Package by Feature - Not by layer (service/repository/controller in same package)
- Logging - Use SLF4J, structured logging, appropriate levels
- Configuration - Externalize via application.properties, environment variables
Comments - Less is More
// BAD - redundant comment
// Get user from repository
User user = repository.findById(id);
// GOOD - self-explanatory code, no comment needed
User user = repository.findById(id);
// GOOD - comment explains WHY (not obvious)
// Rate limit: API allows max 100 requests per minute per client
rateLimiter.acquire();
// GOOD - comment documents constraint
/**
* Processes payment. Amount must be positive.
* @throws IllegalArgumentException if amount <= 0
*/
public void processPayment(BigDecimal amount) { }References
- Java Language Specification: https://docs.oracle.com/javase/specs/
- Effective Java by Joshua Bloch
- Clean Code by Robert C. Martin