Claude-code-setup standards-java
Java coding standards for enterprise applications. Includes naming conventions, modern Java features, design patterns, and recommended tooling.
install
source · Clone the upstream repo
git clone https://github.com/b33eep/claude-code-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/b33eep/claude-code-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/standards-java" ~/.claude/skills/b33eep-claude-code-setup-standards-java && rm -rf "$T"
manifest:
skills/standards-java/SKILL.mdsource content
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 | , |
| Interfaces | PascalCase | , |
| Methods | camelCase | , |
| Variables | camelCase | , |
| Constants | UPPER_SNAKE_CASE | , |
| Packages | lowercase.dot.separated | , |
| Test Classes | ClassNameTest | , |
| Test Methods | descriptive_snake_case or camelCase | |
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 |
|---|---|
or | Build automation |
| Testing framework (JUnit 5) |
| Mocking framework |
| Code style checking |
| Static bug detection |
| Code coverage |
| Dependency management rules |
| 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