standards-java
Scannednpx machina-cli add skill b33eep/claude-code-setup/standards-java --openclawJava 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.md
Gradle 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.md
Modern 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 accessor
Sealed 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 explicit
Null 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 equals
Testing 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
Source
git clone https://github.com/b33eep/claude-code-setup/blob/main/skills/standards-java/SKILL.mdView on GitHub Overview
Defines Java coding standards for enterprise applications, covering naming conventions, modern Java features, design patterns, and recommended tooling. The guide emphasizes simplicity, readability, maintainability, and testability, while promoting SOLID and DRY practices.
How This Skill Works
The standard codifies core principles, general rules, and naming conventions into a concrete project blueprint. It outlines Maven and Gradle project structures, encourages modern Java constructs like records and sealed types where appropriate, and enforces immutability and minimal comments to keep code clean and maintainable.
When to Use It
- Starting a new Java enterprise project (Spring, Jakarta, Quarkus) and aligning on a common structure.
- During code reviews to enforce naming, immutability, and SOLID design.
- Refactoring legacy Java code to modern patterns and clearer APIs.
- Setting up CI/CD pipelines and tooling to enforce coding standards.
- Onboarding new Java developers to a shared set of conventions and practices.
Quick Start
- Step 1: Create a Maven or Gradle project with the standard src/main/java and src/test/java layout as illustrated in the Java project structure.
- Step 2: Apply the Naming Conventions: Class/Interface names in PascalCase, methods in camelCase, constants in UPPER_SNAKE_CASE, and packages in lowercase dot notation.
- Step 3: Enable modern Java features where suitable (records for immutable data, sealed types for restricted hierarchies), enforce immutability, SOLID design, and add unit tests using JUnit and your chosen tooling.
Best Practices
- Follow SOLID principles and DRY without over-engineering.
- Use descriptive names for classes, methods, and variables.
- Prefer immutable objects; favor final declarations where possible.
- Use early returns to reduce nesting and improve readability.
- Keep comments to a minimum; aim for self-explanatory code and meaningful tests.
Example Use Cases
- UserService.java implementing business logic in the service layer.
- UserRepository.java as a data access interface.
- Application.java as the main entry point (or Spring/Jakarta boot class).
- UserServiceTest.java showing unit tests in the service package.
- UserRepositoryTest.java showing data-access tests in the repository package.