Skill
Community

java-conventions

Install
1
Install the plugin
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-java

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

This skill should be used when working on Java or Kotlin projects, writing Java code, configuring Spring Boot, or reviewing Java code.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

Java Code Style and Idiomatic Patterns

This skill defines comprehensive conventions for writing modern Java 21+ code following the Google Java Style Guide, Spring Boot best practices, and community-standard idioms.

Modern Java Records

Use Records for DTOs and Value Objects

Prefer Java records over traditional POJOs for immutable data carriers. Records provide equals, hashCode, toString, and accessors automatically.

// CORRECT: Record for a DTO
public record UserResponse(
    Long id,
    String username,
    String email,
    Instant createdAt
) {}
// WRONG: Verbose POJO for simple data carrier
public class UserResponse {
    private final Long id;
    private final String username;
    private final String email;
    private final Instant createdAt;

    public UserResponse(Long id, String username, String email, Instant createdAt) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.createdAt = createdAt;
    }

    public Long getId() { return id; }
    public String getUsername() { return username; }
    public String getEmail() { return email; }
    public Instant getCreatedAt() { return createdAt; }

    @Override
    public boolean equals(Object o) { /* boilerplate */ }

    @Override
    public int hashCode() { /* boilerplate */ }

    @Override
    public String toString() { /* boilerplate */ }
}

Records with Compact Constructors for Validation

Use compact constructors to add validation logic to records.

// CORRECT: Compact constructor with validation
public record OrderItem(
    String productId,
    int quantity,
    BigDecimal unitPrice
) {
    public OrderItem {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        Objects.requireNonNull(productId, "productId must not be null");
        Objects.requireNonNull(unitPrice, "unitPrice must not be null");
    }

    public BigDecimal totalPrice() {
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
}
// WRONG: Using a canonical constructor when compact will do
public record OrderItem(String productId, int quantity, BigDecimal unitPrice) {
    public OrderItem(String productId, int quantity, BigDecimal unitPrice) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }
}

When Not to Use Records

Do not use records for JPA entities or mutable objects that need setters.

// CORRECT: JPA entity remains a class (records cannot be entities)
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String username;

    // JPA requires no-arg constructor
    protected User() {}

    public User(String username) {
        this.username = username;
    }

    // getters and setters required by JPA
}
// WRONG: Record as JPA entity (will not work)
@Entity
public record User(Long id, String username) {}

Sealed Interfaces and Classes

Use Sealed Interfaces for Restricted Type Hierarchies

Sealed interfaces restrict which classes can implement them, enabling exhaustive pattern matching.

// CORRECT: Sealed interface with permitted subtypes
public sealed interface PaymentResult
    permits PaymentResult.Success, PaymentResult.Declined, PaymentResult.Error {

    record Success(String transactionId, Instant processedAt) implements PaymentResult {}
    record Declined(String reason, String code) implements PaymentResult {}
    record Error(Exception cause) implements PaymentResult {}
}
// WRONG: Open interface that should be sealed
public interface PaymentResult {}

public class Success implements PaymentResult {
    private final String transactionId;
    // ...
}

public class Declined implements PaymentResult {
    private final String reason;
    // ...
}

Sealed Interfaces with Pattern Matching Switch

Combine sealed interfaces with switch expressions for exhaustive handling.

// CORRECT: Exhaustive pattern matching on sealed interface
public String describeResult(PaymentResult result) {
    return switch (result) {
        case PaymentResult.Success s ->
            "Payment processed: " + s.transactionId();
        case PaymentResult.Declined d ->
            "Payment declined: " + d.reason() + " (" + d.code() + ")";
        case PaymentResult.Error e ->
            "Payment error: " + e.cause().getMessage();
    };
}
// WRONG: instanceof chain instead of pattern matching
public String describeResult(PaymentResult result) {
    if (result instanceof PaymentResult.Success) {
        PaymentResult.Success s = (PaymentResult.Success) result;
        return "Payment processed: " + s.transactionId();
    } else if (result instanceof PaymentResult.Declined) {
        PaymentResult.Declined d = (PaymentResult.Declined) result;
        return "Payment declined: " + d.reason();
    } else {
        return "Unknown result";
    }
}

Pattern Matching

Use Pattern Matching for instanceof

Use pattern matching with instanceof to eliminate explicit casts.

// CORRECT: Pattern matching instanceof
public double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t -> 0.5 * t.base() * t.height();
    };
}
// WRONG: Traditional instanceof with cast
public double calculateArea(Shape shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        return r.width() * r.height();
    }
    throw new IllegalArgumentException("Unknown shape");
}

Guarded Patterns in Switch

Use guarded patterns with when clauses for conditional matching.

// CORRECT: Guarded pattern matching
public String classifyTemperature(Number temp) {
    return switch (temp) {
        case Integer i when i < 0 -> "freezing";
        case Integer i when i < 15 -> "cold";
        case Integer i when i < 25 -> "comfortable";
        case Integer i -> "hot";
        case Double d when d < 0.0 -> "freezing";
        case Double d -> "measured: " + d;
        default -> "unknown type";
    };
}

Switch Expressions

Prefer Switch Expressions Over Switch Statements

Use switch expressions to return values directly and ensure exhaustiveness.

// CORRECT: Switch expression with arrow syntax
public BigDecimal applyDiscount(CustomerTier tier, BigDecimal price) {
    BigDecimal discount = switch (tier) {
        case BRONZE -> new BigDecimal("0.05");
        case SILVER -> new BigDecimal("0.10");
        case GOLD -> new BigDecimal("0.15");
        case PLATINUM -> new BigDecimal("0.20");
    };
    return price.multiply(BigDecimal.ONE.subtract(discount));
}
// WRONG: Traditional switch statement with fall-through risk
public BigDecimal applyDiscount(CustomerTier tier, BigDecimal price) {
    BigDecimal discount;
    switch (tier) {
        case BRONZE:
            discount = new BigDecimal("0.05");
            break;
        case SILVER:
            discount = new BigDecimal("0.10");
            break;
        default:
            discount = BigDecimal.ZERO;
            break;  // Easy to forget break
    }
    return price.multiply(BigDecimal.ONE.subtract(discount));
}

Virtual Threads

Use Virtual Threads for I/O-Bound Work

Use virtual threads (Project Loom) for I/O-bound operations instead of platform threads.

// CORRECT: Virtual thread executor for I/O-bound tasks
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<UserProfile>> futures = userIds.stream()
        .map(id -> executor.submit(() -> fetchUserProfile(id)))
        .toList();

    List<UserProfile> profiles = futures.stream()
        .map(f -> {
            try {
                return f.get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        })
        .toList();
}
// WRONG: Fixed thread pool for I/O-bound work (wastes platform threads)
ExecutorService executor = Executors.newFixedThreadPool(100);
try {
    List<Future<UserProfile>> futures = userIds.stream()
        .map(id -> executor.submit(() -> fetchUserProfile(id)))
        .toList();
    // ...
} finally {
    executor.shutdown();
}

Virtual Threads in Spring Boot

Configure Spring Boot to use virtual threads for request handling.

// CORRECT: Enable virtual threads in Spring Boot 3.2+
@Configuration
public class VirtualThreadConfig {

    @Bean
    public TomcatProtocolHandlerCustomizer<?> virtualThreadCustomizer() {
        return protocolHandler ->
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }
}
# Or via application.yml in Spring Boot 3.2+
spring:
  threads:
    virtual:
      enabled: true

Local Variable Type Inference

Use var for Local Variables with Clear Types

Use var when the type is obvious from the right-hand side. Never use var when it reduces readability.

// CORRECT: var with obvious types
var users = new ArrayList<User>();
var response = restTemplate.getForObject(url, UserResponse.class);
var config = loadConfiguration();
var entry = Map.entry("key", "value");

// CORRECT: var in try-with-resources
try (var reader = new BufferedReader(new FileReader(path))) {
    // ...
}

// CORRECT: var in enhanced for loops
for (var user : users) {
    process(user);
}
// WRONG: var hides the type and reduces readability
var result = service.process(data);  // What type is result?
var x = calculate(a, b, c);         // Meaningless variable name with var

// WRONG: var with literals where type matters
var count = 0;       // Is this int, long, Integer?
var amount = 3.14;   // Is this float or double?

// WRONG: var with diamond operator (type is lost)
var list = new ArrayList<>();  // ArrayList<Object>, not useful

Never Use var for Fields or Method Parameters

var is only for local variables, not for fields, method parameters, or return types.

// CORRECT: Explicit types for fields and parameters
public class OrderService {
    private final OrderRepository repository;

    public OrderResponse processOrder(OrderRequest request) {
        var order = repository.save(request.toOrder());  // var OK for local
        return OrderResponse.from(order);
    }
}
// WRONG: var cannot be used here (compilation error)
public class OrderService {
    private var repository;  // Compilation error
    public var processOrder(var request) { }  // Compilation error
}

Optional Patterns

Return Optional Instead of Null

Use Optional for return types where absence is a valid outcome.

// CORRECT: Return Optional for nullable results
public Optional<User> findByEmail(String email) {
    return Optional.ofNullable(
        jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE email = ?",
            userRowMapper,
            email
        )
    );
}

// CORRECT: Chain Optional operations
public String getUserDisplayName(Long userId) {
    return userRepository.findById(userId)
        .map(User::displayName)
        .orElse("Anonymous");
}
// WRONG: Return null when Optional is appropriate
public User findByEmail(String email) {
    // Callers must remember to check for null
    return jdbcTemplate.queryForObject(
        "SELECT * FROM users WHERE email = ?",
        userRowMapper,
        email
    );
}

Never Use Optional for Fields or Parameters

Optional should only be used as a return type, never as a field type or method parameter.

// CORRECT: Use Optional only as return type
public class UserService {
    public Optional<User> findUser(Long id) {
        return repository.findById(id);
    }
}
// WRONG: Optional as field
public class User {
    private Optional<String> middleName;  // Don't do this
}

// WRONG: Optional as parameter
public void updateUser(Optional<String> name) {  // Don't do this
    // ...
}

Use orElseThrow for Required Values

Use orElseThrow with a meaningful exception for required values.

// CORRECT: orElseThrow with descriptive exception
public User getUser(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
// WRONG: get() without check (throws NoSuchElementException)
public User getUser(Long id) {
    return userRepository.findById(id).get();  // Unsafe!
}

// WRONG: isPresent/get pattern
public User getUser(Long id) {
    Optional<User> opt = userRepository.findById(id);
    if (opt.isPresent()) {
        return opt.get();  // Use map/orElse/orElseThrow instead
    }
    throw new UserNotFoundException("Not found");
}

Stream API Best Practices

Prefer Streams for Collection Transformations

Use Stream API for declarative collection processing.

// CORRECT: Stream for filtering and mapping
public List<UserSummary> getActiveUserSummaries() {
    return userRepository.findAll().stream()
        .filter(User::isActive)
        .map(user -> new UserSummary(user.id(), user.displayName()))
        .sorted(Comparator.comparing(UserSummary::displayName))
        .toList();
}
// WRONG: Imperative loop for simple transformations
public List<UserSummary> getActiveUserSummaries() {
    List<User> users = userRepository.findAll();
    List<UserSummary> result = new ArrayList<>();
    for (User user : users) {
        if (user.isActive()) {
            result.add(new UserSummary(user.id(), user.displayName()));
        }
    }
    Collections.sort(result, Comparator.comparing(UserSummary::displayName));
    return result;
}

Use Collectors.toUnmodifiableList for Immutable Results

Prefer toList() (Java 16+) or Collectors.toUnmodifiableList() for immutable results.

// CORRECT: Immutable list result (Java 16+)
List<String> names = users.stream()
    .map(User::name)
    .toList();  // Returns unmodifiable list

// CORRECT: Immutable map result
Map<Long, User> userById = users.stream()
    .collect(Collectors.toUnmodifiableMap(User::id, Function.identity()));
// WRONG: Mutable list when immutability is preferred
List<String> names = users.stream()
    .map(User::name)
    .collect(Collectors.toList());  // Mutable list, use toList() instead

Avoid Complex Stream Chains

Keep stream chains readable. Extract complex operations into methods.

// CORRECT: Break complex chains into named steps
public OrderReport generateReport(List<Order> orders) {
    var completedOrders = filterCompleted(orders);
    var revenueByProduct = calculateRevenueByProduct(completedOrders);
    var topProducts = findTopProducts(revenueByProduct, 10);
    return new OrderReport(topProducts, revenueByProduct);
}

private List<Order> filterCompleted(List<Order> orders) {
    return orders.stream()
        .filter(o -> o.status() == OrderStatus.COMPLETED)
        .toList();
}
// WRONG: Overly long stream chain
public OrderReport generateReport(List<Order> orders) {
    return orders.stream()
        .filter(o -> o.status() == OrderStatus.COMPLETED)
        .flatMap(o -> o.items().stream())
        .collect(Collectors.groupingBy(
            OrderItem::productId,
            Collectors.reducing(BigDecimal.ZERO, OrderItem::total, BigDecimal::add)
        ))
        .entrySet().stream()
        .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
        .limit(10)
        // ... too much in one chain
        .toList();
}

Text Blocks

Use Text Blocks for Multi-Line Strings

Use text blocks for SQL queries, JSON templates, and other multi-line strings.

// CORRECT: Text block for SQL
String query = """
    SELECT u.id, u.username, u.email
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE o.status = 'ACTIVE'
    ORDER BY u.username
    """;

// CORRECT: Text block for JSON templates
String jsonTemplate = """
    {
        "name": "%s",
        "email": "%s",
        "role": "USER"
    }
    """.formatted(name, email);
// WRONG: String concatenation for multi-line content
String query = "SELECT u.id, u.username, u.email " +
    "FROM users u " +
    "JOIN orders o ON u.id = o.user_id " +
    "WHERE o.status = 'ACTIVE' " +
    "ORDER BY u.username";

Immutable Collections

Prefer Immutable Collections

Use unmodifiable collection factories and avoid exposing mutable internal state.

// CORRECT: Immutable collection factories
List<String> roles = List.of("ADMIN", "USER", "GUEST");
Set<String> permissions = Set.of("READ", "WRITE", "DELETE");
Map<String, Integer> limits = Map.of(
    "FREE", 100,
    "PRO", 1000,
    "ENTERPRISE", 10000
);

// CORRECT: Defensive copy in constructor
public class Config {
    private final List<String> servers;

    public Config(List<String> servers) {
        this.servers = List.copyOf(servers);  // Defensive copy
    }

    public List<String> servers() {
        return servers;  // Already unmodifiable
    }
}
// WRONG: Exposing mutable internal state
public class Config {
    private final List<String> servers;

    public Config(List<String> servers) {
        this.servers = servers;  // Caller can modify the list
    }

    public List<String> getServers() {
        return servers;  // Exposes mutable reference
    }
}

Logging with SLF4J

Use SLF4J with Parameterized Messages

Always use SLF4J for logging with parameterized messages instead of string concatenation.

// CORRECT: SLF4J parameterized logging
private static final Logger log = LoggerFactory.getLogger(UserService.class);

public User createUser(CreateUserRequest request) {
    log.info("Creating user with email={}", request.email());
    try {
        User user = userRepository.save(request.toUser());
        log.info("User created successfully: id={}, email={}", user.id(), user.email());
        return user;
    } catch (DataIntegrityViolationException e) {
        log.warn("Duplicate email detected: email={}", request.email());
        throw new DuplicateEmailException(request.email(), e);
    }
}
// WRONG: String concatenation in logging
log.info("Creating user with email=" + request.email());  // Always evaluates

// WRONG: Using System.out for logging
System.out.println("User created: " + user.id());

// WRONG: Wrong log level
log.debug("CRITICAL: Payment failed for order=" + orderId);  // Use error, not debug

Structured Logging

Use structured logging with key-value pairs for machine-parseable logs.

// CORRECT: Structured logging with MDC
import org.slf4j.MDC;

public OrderResponse processOrder(OrderRequest request) {
    MDC.put("orderId", request.orderId());
    MDC.put("userId", request.userId());
    try {
        log.info("Processing order: amount={}, items={}",
            request.totalAmount(), request.items().size());
        // process...
        log.info("Order processed successfully");
        return response;
    } finally {
        MDC.clear();
    }
}

Spring Dependency Injection

Use Constructor Injection

Always use constructor injection. Never use field injection with @Autowired.

// CORRECT: Constructor injection (single constructor, @Autowired optional)
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;

    public OrderService(
            OrderRepository orderRepository,
            PaymentGateway paymentGateway,
            NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
    }
}
// WRONG: Field injection
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private PaymentGateway paymentGateway;

    @Autowired
    private NotificationService notificationService;
}

Spring Configuration Properties

Use @ConfigurationProperties Over @Value

Prefer type-safe @ConfigurationProperties over scattered @Value annotations.

// CORRECT: Type-safe configuration properties
@ConfigurationProperties(prefix = "app.payment")
public record PaymentProperties(
    String apiKey,
    String apiUrl,
    Duration timeout,
    RetryProperties retry
) {
    public record RetryProperties(
        int maxAttempts,
        Duration delay
    ) {}
}
# application.yml
app:
  payment:
    api-key: ${PAYMENT_API_KEY}
    api-url: https://api.payment.example.com
    timeout: 30s
    retry:
      max-attempts: 3
      delay: 1s
// WRONG: Scattered @Value annotations
@Service
public class PaymentService {
    @Value("${app.payment.api-key}")
    private String apiKey;

    @Value("${app.payment.api-url}")
    private String apiUrl;

    @Value("${app.payment.timeout}")
    private Duration timeout;
}

Spring @Transactional Best Practices

Apply @Transactional at the Service Layer

Place @Transactional on service methods, not on repository or controller methods.

// CORRECT: @Transactional on service methods
@Service
public class TransferService {
    private final AccountRepository accountRepository;

    public TransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public TransferResult transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId)
            .orElseThrow(() -> new AccountNotFoundException(fromId));
        Account to = accountRepository.findById(toId)
            .orElseThrow(() -> new AccountNotFoundException(toId));

        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);

        return new TransferResult(from.balance(), to.balance());
    }

    @Transactional(readOnly = true)
    public AccountBalance getBalance(Long accountId) {
        return accountRepository.findById(accountId)
            .map(Account::balance)
            .orElseThrow(() -> new AccountNotFoundException(accountId));
    }
}
// WRONG: @Transactional on controller
@RestController
public class TransferController {
    @Transactional  // Don't put transactions on controllers
    @PostMapping("/transfer")
    public TransferResult transfer(@RequestBody TransferRequest request) {
        // ...
    }
}

Understand @Transactional Proxy Gotchas

Internal method calls bypass Spring's proxy, so @Transactional will not apply.

// CORRECT: Separate transactional operations into distinct beans
@Service
public class OrderService {
    private final OrderProcessor orderProcessor;

    public OrderService(OrderProcessor orderProcessor) {
        this.orderProcessor = orderProcessor;
    }

    public void processAndNotify(Order order) {
        orderProcessor.process(order);  // Transaction applied via proxy
        notify(order);
    }
}

@Service
public class OrderProcessor {
    @Transactional
    public void process(Order order) {
        // Transactional work
    }
}
// WRONG: Self-invocation skips proxy
@Service
public class OrderService {
    @Transactional
    public void process(Order order) {
        // Transactional work
    }

    public void processAndNotify(Order order) {
        process(order);  // @Transactional NOT applied (self-invocation)
        notify(order);
    }
}

Spring Profiles

Use Profiles for Environment-Specific Configuration

Use Spring profiles to manage environment-specific configuration.

# application.yml (common config)
spring:
  application:
    name: my-service

# application-local.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb

# application-prod.yml
spring:
  datasource:
    url: ${DATABASE_URL}
// CORRECT: Profile-specific bean
@Configuration
public class CacheConfig {

    @Bean
    @Profile("local")
    public CacheManager localCacheManager() {
        return new ConcurrentMapCacheManager("users", "orders");
    }

    @Bean
    @Profile("prod")
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory).build();
    }
}

Try-with-Resources

Always Use Try-with-Resources for AutoCloseable

Use try-with-resources for any AutoCloseable resource to ensure proper cleanup.

// CORRECT: Try-with-resources
public List<User> readUsersFromCsv(Path csvPath) throws IOException {
    try (var reader = Files.newBufferedReader(csvPath);
         var csvParser = new CSVParser(reader, CSVFormat.DEFAULT.withFirstRecordAsHeader())) {
        return csvParser.getRecords().stream()
            .map(record -> new User(record.get("id"), record.get("name")))
            .toList();
    }
}
// WRONG: Manual resource management
public List<User> readUsersFromCsv(Path csvPath) throws IOException {
    BufferedReader reader = null;
    try {
        reader = Files.newBufferedReader(csvPath);
        // process...
    } finally {
        if (reader != null) {
            reader.close();  // Can throw and mask original exception
        }
    }
}

Lombok Conventions

Follow Project Conventions for Lombok

If the project uses Lombok, follow its established patterns. Prefer records for new projects.

// CORRECT: Lombok when project already uses it
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LegacyUser {
    private Long id;
    private String username;
    private String email;
}

// CORRECT: Lombok @Value for immutable class (pre-records codebase)
@Value
@Builder
public class LegacyUserResponse {
    Long id;
    String username;
    String email;
}
// WRONG: Mixing Lombok and records inconsistently in the same project
// Pick one approach and be consistent

// WRONG: @Data on entity without @EqualsAndHashCode exclusion
@Data
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;  // id in equals/hashCode causes Hibernate issues
}

// CORRECT: Exclude id from equals/hashCode for entities
@Getter
@Setter
@EqualsAndHashCode(exclude = "id")
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
}

Google Java Style Guide Essentials

Formatting Rules

Follow the Google Java Style Guide for consistent formatting.

// CORRECT: Google Java Style formatting
public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
    private static final int MAX_RETRY = 3;

    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public OrderResponse processOrder(OrderRequest request) {
        // 4-space indentation
        // Opening brace on same line
        // One statement per line
        if (request.items().isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one item");
        }

        return switch (request.type()) {
            case STANDARD -> processStandard(request);
            case EXPRESS -> processExpress(request);
            case BULK -> processBulk(request);
        };
    }
}

Import Ordering

Follow Google Java Style import ordering: static imports first, then regular imports, sorted alphabetically within groups.

// CORRECT: Import ordering (Google style)
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import com.example.domain.Order;
import com.example.repository.OrderRepository;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
// WRONG: Unsorted or wildcard imports
import java.util.*;
import com.example.domain.*;
import org.springframework.stereotype.Service;
import java.time.Instant;
import com.example.repository.OrderRepository;

Existing Repository Compatibility

Respect Established Patterns

When contributing to existing repositories, respect their established conventions.

// Check existing patterns before writing code:
// - Look at pom.xml/build.gradle for Java version and dependencies
// - Check for Lombok usage
// - Review existing record vs POJO patterns
// - Follow the existing logging framework
// - Match the existing test framework and patterns
// - Follow the established error handling approach
# Discover project conventions
cat pom.xml | grep java.version      # Java version
grep -r "@Value" src/                  # @Value vs @ConfigurationProperties
grep -r "lombok" pom.xml              # Lombok usage
ls src/test/                           # Test structure

This skill ensures Java code follows modern idioms, is maintainable, and leverages the latest language features available in Java 21+. Apply these rules consistently across all Java projects while respecting existing project conventions.

Stats
Stars0
Forks0
Last CommitFeb 10, 2026

Similar Skills