| name | java-guide |
| description | Java language guardrails, patterns, and best practices for AI-assisted development.
Use when working with Java files (.java), pom.xml, build.gradle, or when the user mentions Java.
Provides modern Java patterns (records, sealed classes, streams), testing standards,
and Spring conventions specific to this project's coding standards.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"language","language":"java","extensions":".java"} |
Java Guide
Applies to: Java 17+, Spring Boot, Maven/Gradle, Enterprise Applications
Core Principles
- Immutability by Default: Prefer records,
final fields, and unmodifiable collections
- Explicit Over Implicit: Clear type declarations, no raw types, no unchecked casts
- Fail Fast: Validate inputs at boundaries, use
Objects.requireNonNull liberally
- Composition Over Inheritance: Favor delegation and interfaces over deep class hierarchies
- Standard Library First: Use
java.util, java.time, java.nio before adding dependencies
Guardrails
Version & Dependencies
- Use Java 17+ (LTS) with preview features disabled in production
- Manage dependencies with Maven (
pom.xml) or Gradle (build.gradle.kts)
- Pin dependency versions explicitly (no dynamic versions like
1.+)
- Run
mvn dependency:analyze or gradle dependencies to detect unused/undeclared deps
- Check for vulnerabilities:
mvn org.owasp:dependency-check-maven:check
Code Style
- Follow Google Java Style Guide
- Classes:
PascalCase | Methods/fields: camelCase | Constants: UPPER_SNAKE_CASE
- Packages:
com.company.project.module (lowercase, no underscores)
- One top-level class per file (name matches filename)
- Use
var for local variables only when the type is obvious from the right side
- No wildcard imports (
import java.util.* is forbidden)
Records & Sealed Classes
- Use
record for immutable data carriers instead of manual POJOs
- Use
sealed classes/interfaces for restricted type hierarchies
- Add compact constructors for validation in records
- Sealed classes enable exhaustive
switch expressions with pattern matching
public record UserId(String value) {
public UserId {
Objects.requireNonNull(value, "UserId must not be null");
if (value.isBlank()) {
throw new IllegalArgumentException("UserId must not be blank");
}
}
}
public sealed interface PaymentResult
permits PaymentResult.Success, PaymentResult.Declined, PaymentResult.Error {
record Success(String transactionId, BigDecimal amount) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record Error(Exception cause) implements PaymentResult {}
}
Streams & Optional
- Use streams for transformations, not for side effects
- Never call
Optional.get() -- use orElseThrow(), orElse(), map(), flatMap()
- Do not use
Optional as a method parameter or field type (only as a return type)
- Avoid parallel streams unless measured to be faster (overhead is real)
- Prefer
toList() (Java 16+) over collect(Collectors.toList())
List<String> activeEmails = users.stream()
.filter(User::isActive)
.map(User::email)
.toList();
String displayName = userRepository.findById(id)
.map(User::displayName)
.orElseThrow(() -> new UserNotFoundException(id));
Resource Management
- Always use try-with-resources for
AutoCloseable types
- Never rely on
finalize() (deprecated and unreliable)
- Close resources in reverse order of acquisition
try (var connection = dataSource.getConnection();
var statement = connection.prepareStatement(sql);
var resultSet = statement.executeQuery()) {
while (resultSet.next()) {
results.add(mapRow(resultSet));
}
}
Project Structure
myproject/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/com/company/project/
│ │ │ ├── Application.java # Entry point
│ │ │ ├── config/ # Configuration classes
│ │ │ ├── controller/ # REST controllers / API layer
│ │ │ ├── service/ # Business logic
│ │ │ ├── repository/ # Data access
│ │ │ ├── model/ # Domain entities and records
│ │ │ ├── dto/ # Data transfer objects (records)
│ │ │ └── exception/ # Custom exceptions
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/migration/ # Flyway/Liquibase migrations
│ └── test/java/com/company/project/ # Mirrors main structure
└── target/ # Build output (gitignored)
- Packages map to bounded contexts (not technical layers at the top)
- Test structure mirrors source structure
- Keep
resources/ flat; use db/migration/ for schema changes
Key Patterns
Sealed Classes with Pattern Matching
public static double area(Shape shape) {
return switch (shape) {
case Shape.Circle c -> Math.PI * c.radius() * c.radius();
case Shape.Rectangle r -> r.width() * r.height();
case Shape.Triangle t -> 0.5 * t.base() * t.height();
};
}
Immutable Collections
List<String> roles = List.of("ADMIN", "USER", "GUEST");
Map<String, String> config = Map.ofEntries(
Map.entry("host", "localhost"),
Map.entry("port", "8080"));
List<Item> snapshot = List.copyOf(mutableList);
Text Blocks
String query = """
SELECT u.id, u.email, u.created_at
FROM users u
WHERE u.active = true
ORDER BY u.created_at DESC
LIMIT ?
""";
Optional Chaining
User user = cache.findUser(id)
.or(() -> database.findUser(id))
.or(() -> remoteService.fetchUser(id))
.orElseThrow(() -> new UserNotFoundException(id));
userRepository.findById(userId)
.ifPresentOrElse(
u -> log.info("Found user: {}", u.name()),
() -> log.warn("User {} not found", userId));
Testing
Standards
- Test files mirror source under
src/test/java
- Test class names:
ClassNameTest (not TestClassName)
- Test methods:
@Test void shouldDescribeBehavior()
- Use
@DisplayName for complex scenarios, @Nested to group related tests
- Coverage target: >80% for business logic, >60% overall
- Prefer AssertJ over JUnit assertions for readability
Basic Test Structure
class UserServiceTest {
private UserRepository userRepository;
private UserService userService;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
userService = new UserService(userRepository);
}
@Test
void shouldReturnUserWhenFound() {
var expected = new User("1", "alice@example.com", "Alice");
when(userRepository.findById("1")).thenReturn(Optional.of(expected));
User result = userService.getUser("1");
assertThat(result).isEqualTo(expected);
verify(userRepository).findById("1");
}
@Test
void shouldThrowWhenUserNotFound() {
when(userRepository.findById("999")).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getUser("999"))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");
}
}
Parameterized Tests
@ParameterizedTest
@CsvSource({
"alice@example.com, true",
"bob@test.org, true",
"invalid-email, false",
"'', false",
})
void shouldValidateEmail(String email, boolean expected) {
assertThat(EmailValidator.isValid(email)).isEqualTo(expected);
}
@ParameterizedTest
@MethodSource("provideMoneyAdditions")
void shouldAddMoneySameCurrency(Money a, Money b, Money expected) {
assertThat(a.add(b)).isEqualTo(expected);
}
static Stream<Arguments> provideMoneyAdditions() {
var usd = Currency.getInstance("USD");
return Stream.of(
Arguments.of(new Money(BigDecimal.ONE, usd),
new Money(BigDecimal.TEN, usd),
new Money(new BigDecimal("11"), usd))
);
}
Tooling
Maven Commands
mvn clean install
mvn test
mvn verify
mvn dependency:tree
mvn dependency:analyze
mvn spotbugs:check
mvn checkstyle:check
Gradle Commands
./gradlew build
./gradlew test
./gradlew check
./gradlew dependencies
./gradlew jacocoTestReport
References
For detailed patterns and examples, see:
External References