irahardianto / java-idioms
Modern Java (17/21+ LTS) idioms: records, sealed classes, pattern matching switch, Optional hygiene, streams, and JUnit 5 testing. Use when writing, refactoring, or reviewing Java core logic, domain models, or libraries. Pair with spring-boot-idioms for Spring services.
Java Idioms and Patterns
Java rewards clarity, type safety, and robust ecosystem tooling. Modern Java (17+ LTS) favors records, sealed classes, and pattern matching. Idiomatic Java = clean, readable, framework-aware.
Scope: Java coding idioms. Test naming: .agents/rules/testing-strategy.md. Logging:
@.agents/skills/logging-implementation/SKILL.md.
Modern Java Features (17+ LTS)
Records for immutable data carriers:
`java
// ✅ Concise, immutable, auto-generated equals/hashCode/toString
public record CreateTaskRequest(String title, Priority priority) {}// ❌ Verbose boilerplate POJO
public class CreateTaskRequest { /* getters, setters, equals, hashCode... */ }`Sealed classes for constrained hierarchies:
`java
public sealed interface TaskResult permits Success, Failure, Pending {}
public record Success(Task task) implements TaskResult {}
public record Failure(String reason) implements TaskResult {}
public record Pending(String taskId) implements TaskResult {}`Pattern matching with
switch:`java
return switch (result) {
case Success(var task) -> ResponseEntity.ok(task);
case Failure(var reason) -> ResponseEntity.badRequest().body(reason);
case Pending(var id) -> ResponseEntity.accepted().body(id);
};`Text blocks for queries and templates:
`java
String query = """
SELECT t.id, t.title, t.priority
FROM tasks t
WHERE t.user_id = ?
ORDER BY t.created_at DESC
""";`Virtual threads (21+) for I/O-bound work:
`java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> fetchUser(userId));
executor.submit(() -> fetchTasks(userId));
}`
Error Handling
Domain exception hierarchies — never raw
Exception:`java
public abstract class DomainException extends RuntimeException {
protected DomainException(String message) { super(message); }
}public class NotFoundException extends DomainException {
private final String resource;
private final String resourceId;
public NotFoundException(String resource, String resourceId) {
super(String.format("%s '%s' not found", resource, resourceId));
this.resource = resource;
this.resourceId = resourceId;
}
}`- Never catch
Exceptionbroadly — catch specific exceptions. Never swallow exceptions silently. Optionalfor nullable returns — never for parameters:`java
// ✅ Return type
public Optional<Task> findById(String id) { ... }// ❌ Parameter — use overloading or @Nullable instead
public void process(Optional<String> filter) { ... }`
Interfaces and DI
Program to interfaces, inject via constructor:
`java
// ✅ Interface in consumer package
public interface TaskStorage {
Task getById(String id);
void save(Task task);
}// ✅ Constructor injection (Spring auto-wires)
@Service
public class TaskService {
private final TaskStorage storage;
public TaskService(TaskStorage storage) { this.storage = storage; }
}`- Prefer constructor injection over
@Autowiredfield injection. No field injection — ever.
Naming
- PascalCase for classes, interfaces, enums, records.
- camelCase for methods, fields, local variables.
- UPPERSNAKECASE for constants (
static final). - No Hungarian notation.
TaskServicenotITaskService.userIdnotstrUserId. - Package names: lowercase, no underscores.
com.example.tasknotcom.example.task_management.
Testing
Test naming, pyramid: .agents/rules/testing-strategy.md. Java-specific tooling below.
JUnit 5 + AssertJ:
`java
@Test
void calculateDiscountreturnsZerowhenNoItems() {
var result = calculator.calculateDiscount(List.of(), coupon);
assertThat(result).isEqualTo(0.0);
}`@ParameterizedTestfor table-driven tests:`java
@ParameterizedTest
@CsvSource({"low,1", "medium,5", "high,10"})
void priorityScore_mapsCorrectly(String priority, int expected) {
assertThat(Priority.score(priority)).isEqualTo(expected);
}`Mockito for mocking — never PowerMock:
`java
@ExtendWith(MockitoExtension.class)
class TaskServiceTest {
@Mock TaskStorage storage;
@InjectMocks TaskService service;
}`- TestContainers for integration tests — real DB, no in-memory substitutes for critical paths.
Formatting and Static Analysis
Must pass zero warnings/errors before commit. See .agents/rules/code-idioms-and-conventions.md.
| Tool | Purpose | Command |
|---|---|---|
google-java-format | Canonical formatting | google-java-format --replace src/**/*.java |
SpotBugs | Bug detection | mvn spotbugs:check or gradle spotbugsMain |
Error Prone | Compile-time bug detection | Compiler plugin |
Checkstyle | Style enforcement | mvn checkstyle:check |
SonarQube | Comprehensive analysis | CI integration |
OWASP Dependency-Check | CVE scanning | mvn dependency-check:check |
Related
- Code Idioms and Conventions .agents/rules/code-idioms-and-conventions.md
- Testing Strategy .agents/rules/testing-strategy.md
- Error Handling Principles .agents/rules/error-handling-principles.md
- Dependency Management Principles @.agents/rules/dependency-management-principles.md
- Logging Implementation @.agents/skills/logging-implementation/SKILL.md