Skip to content

Java – Classes & Methods

This page covers how to structure, declare, and format Java classes and methods at Cygnus Dynamics. The standards are grounded in Oracle's official Java Code Conventions and extended with modern Java practices.

File Organisation

Each Java source file must contain exactly one public class or interface. The file name must exactly match the public class name, including case.

// File: OrderService.java
public class OrderService { ... }   // ✅ File name matches class name

// ❌ Never put two public classes in one file

Source File Structure

Every Java source file must follow this order:

1. Package declaration
2. Import statements (blank line between groups)
3. Class/Interface declaration
4. Static constants and fields
5. Instance fields
6. Constructors
7. Public methods
8. Protected methods
9. Private methods
10. Inner classes (if any)

Import grouping order (separated by blank lines):

// 1. Java standard library
import java.util.List;
import java.util.Map;

// 2. Third-party libraries
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

// 3. Internal Cygnus Dynamics imports
import com.cygnusdynamics.order.domain.Order;
import com.cygnusdynamics.order.repository.OrderRepository;

No wildcard imports

Never use wildcard imports (import java.util.*). They pollute the namespace, make class origin ambiguous, and cause issues when the same class name exists in multiple packages. IntelliJ IDEA can be configured to expand wildcards automatically on save.

Class Declarations

Structure and Formatting

Opening braces go on the same line as the class declaration (K&R style). No space between the method/class name and the parenthesis.

// ✅ Correct structure
public class OrderService {

    private static final int MAX_ORDER_ITEMS = 100;

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    public Order getOrderById(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }
}

Access Modifiers

Always declare the most restrictive access level that still satisfies the requirement. Default to private; open up only when necessary.

Modifier When to Use
private Default for all fields and implementation methods
protected Only when a subclass genuinely needs access
public Only for the API surface — constructors, service methods, DTO accessors
package-private (no modifier) For internal collaborators within the same package
// ✅ Correct access control
public class UserService {

    private final UserRepository userRepository;   // private field — always

    public User getUserById(Long id) { ... }       // public — part of the API

    private User buildUserFromRequest(CreateUserRequest req) { ... } // private helper
}

// ❌ Overly permissive
public class UserService {
    public UserRepository userRepository;          // field should never be public
    public User buildUserFromRequest(...) { ... }  // internal helper — should be private
}

Immutability First

Declare fields final wherever possible. Immutable objects are inherently thread-safe and much easier to reason about.

// ✅ Immutable dependencies — the service cannot accidentally reassign them
public class OrderService {
    private final OrderRepository orderRepository;
    private final Clock clock;

    public OrderService(OrderRepository orderRepository, Clock clock) {
        this.orderRepository = orderRepository;
        this.clock = clock;
    }
}

Method Declarations

One Statement Per Line

Each statement must appear on its own line. Never write multiple statements on a single line.

// ✅ Correct
int itemCount = order.getItems().size();
boolean isEligible = itemCount > 0 && order.getTotal().compareTo(BigDecimal.ZERO) > 0;

// ❌ Multiple statements on one line
int itemCount = order.getItems().size(); boolean isEligible = itemCount > 0;

Method Length

Methods should be short and focused. Aim for under 30 lines. If a method is longer, it is likely doing more than one thing and should be decomposed.

public void handleCheckout(Long userId, List<CartItem> items) {
    // validate user
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException(userId));
    if (!user.isActive()) throw new UserInactiveException(userId);

    // validate items
    if (items == null || items.isEmpty()) throw new EmptyCartException();
    for (CartItem item : items) {
        Product p = productRepository.findById(item.getProductId())
            .orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
        if (p.getStock() < item.getQuantity()) throw new InsufficientStockException(p);
    }

    // calculate total
    BigDecimal total = items.stream()
        .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);

    // create order
    Order order = new Order(user, items, total, LocalDateTime.now());
    orderRepository.save(order);

    // send confirmation
    emailService.sendOrderConfirmation(user.getEmail(), order);
}
public Order handleCheckout(Long userId, List<CartItem> items) {
    User user = validateUser(userId);
    validateCartItems(items);
    BigDecimal total = calculateTotal(items);
    Order order = createOrder(user, items, total);
    notificationService.sendOrderConfirmation(order);
    return order;
}

private User validateUser(Long userId) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException(userId));
    if (!user.isActive()) throw new UserInactiveException(userId);
    return user;
}

private void validateCartItems(List<CartItem> items) {
    if (items == null || items.isEmpty()) throw new EmptyCartException();
    items.forEach(this::validateStockAvailability);
}

private BigDecimal calculateTotal(List<CartItem> items) {
    return items.stream()
        .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);
}

Parameter Count

Methods should have no more than 4 parameters. When a method needs more, group related parameters into a dedicated request/value object.

// ❌ Too many parameters — hard to call correctly, easy to swap arguments
public Order createOrder(Long userId, String currency, BigDecimal total,
                         String shippingAddress, String billingAddress,
                         boolean isExpressShipping, String couponCode) { }

// ✅ Use a request object
public Order createOrder(CreateOrderRequest request) { }

public class CreateOrderRequest {
    private final Long userId;
    private final String currency;
    private final BigDecimal total;
    private final Address shippingAddress;
    private final Address billingAddress;
    private final boolean expressShipping;
    private final String couponCode;
    // constructor, getters...
}

Return Types

Never return null from a public method. Use Optional<T> for values that may be absent, and throw a typed exception when a required resource is not found.

// ✅ Correct — Optional for nullable lookup
public Optional<User> findUserByEmail(String email) {
    return userRepository.findByEmail(email);
}

// ✅ Correct — throw when the resource is required
public User getUserById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id));
}

// ❌ Never return null from a public API
public User getUserById(Long id) {
    return userRepository.findById(id).orElse(null); // forces null checks on every caller
}

Return empty collections, never null, when a list result is empty:

// ✅ Empty list — callers can iterate safely without a null check
public List<Order> findOrdersByUserId(Long userId) {
    List<Order> orders = orderRepository.findByUserId(userId);
    return orders != null ? orders : Collections.emptyList();
}

Indentation and Formatting

Indentation

Use 4 spaces per indentation level. Never use tabs — they render differently across editors.

// ✅ 4-space indentation at each level
public class OrderService {
    public Order processOrder(Order order) {
        if (order.isValid()) {
            for (OrderItem item : order.getItems()) {
                inventoryService.reserve(item);
            }
        }
        return orderRepository.save(order);
    }
}

Line Length and Wrapping

Keep lines under 120 characters. When wrapping, break after a comma or before an operator, and align the continuation with the opening expression.

// ✅ Method call wrapping — align continuation arguments
Order order = orderService.createOrder(
        userId,
        shippingAddress,
        paymentMethod,
        items);

// ✅ Chained method calls — each chain on its own line
List<OrderDto> result = orders.stream()
        .filter(order -> order.getStatus() == OrderStatus.CONFIRMED)
        .map(orderMapper::toDto)
        .sorted(Comparator.comparing(OrderDto::getCreatedAt).reversed())
        .collect(Collectors.toList());

// ✅ Boolean condition wrapping — 8-space indent to distinguish from body
if ((userIsActive && accountHasFunds)
        || (isAdminOverride && requestIsValid)) {
    processPayment(payment);
}

Blank Lines

  • Two blank lines between top-level class definitions
  • One blank line between methods within a class
  • One blank line to separate logical groups of statements within a method
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
                                                        // ← one blank line between field groups

    public Order getOrderById(Long id) { ... }
                                                        // ← one blank line between methods
    public Order createOrder(CreateOrderRequest request) { ... }
                                                        // ← one blank line between methods
    private void validateRequest(CreateOrderRequest request) { ... }
}

Statements

Control Flow

Always use braces {} for all control flow statements — even single-line bodies. This prevents the classic off-by-one error where a second statement is added without realising braces are missing.

// ✅ Always use braces — even for single statements
if (order.isEmpty()) {
    throw new EmptyOrderException();
}

for (OrderItem item : order.getItems()) {
    inventoryService.reserve(item);
}

// ❌ Never omit braces
if (order.isEmpty())
    throw new EmptyOrderException();   // adding a second line here is a silent bug

Switch Statements

Always include a default case. If a case intentionally falls through (no break), add a // falls through comment to make it explicit.

// ✅ Correct switch format
switch (order.getStatus()) {
    case PENDING:
        notifyCustomerOfPendingOrder(order);
        break;
    case CONFIRMED:
        scheduleShipment(order);
        break;
    case CANCELLED:
        processRefund(order);
        break;
    default:
        logger.warn("Unhandled order status: {}", order.getStatus());
        break;
}

Prefer switch expressions (Java 14+)

For modern Java (14+), prefer switch expressions over switch statements when returning a value. They are exhaustive by default and eliminate fall-through bugs entirely:

String label = switch (order.getStatus()) {
    case PENDING    -> "Awaiting confirmation";
    case CONFIRMED  -> "Order confirmed";
    case SHIPPED    -> "On the way";
    case DELIVERED  -> "Delivered";
    case CANCELLED  -> "Cancelled";
};

Ternary Operator

Use ternary operators only for simple, single-expression conditionals. Never nest ternary operators.

// ✅ Simple and clear
String label = order.isExpress() ? "Express" : "Standard";

// ✅ Return simplified directly from boolean expression — don't use an if/else
return order.getItems().isEmpty();
// Not:
if (order.getItems().isEmpty()) {
    return true;
} else {
    return false;
}

// ❌ Nested ternary — unreadable
String label = order.isExpress() ? "Express" : order.isSameDay() ? "Same Day" : "Standard";

Javadoc

All public and protected classes, interfaces, constructors, and methods must have a Javadoc comment. private methods may use implementation comments when the logic is non-obvious.

Class-Level Javadoc

/**
 * Manages the lifecycle of customer orders from creation through delivery.
 *
 * <p>This service is the primary entry point for all order-related operations.
 * It coordinates with {@link PaymentService} and {@link InventoryService}
 * to ensure transactional consistency across the checkout flow.
 *
 * @author Platform Team
 * @since 2.0.0
 */
@Service
public class OrderService { ... }

Method-Level Javadoc

/**
 * Retrieves an order by its unique identifier.
 *
 * @param orderId the unique identifier of the order; must not be null
 * @return the order matching the given ID
 * @throws OrderNotFoundException if no order exists with the given ID
 * @throws IllegalArgumentException if orderId is null
 */
public Order getOrderById(Long orderId) {
    if (orderId == null) throw new IllegalArgumentException("orderId must not be null");
    return orderRepository.findById(orderId)
        .orElseThrow(() -> new OrderNotFoundException(orderId));
}

Keep Javadoc up to date

A Javadoc comment that contradicts the method's actual behaviour is worse than no comment. When you change a method signature or behaviour, update the Javadoc in the same commit.

Annotations

Annotations go on their own line, immediately before the declaration they annotate.

// ✅ Correct
@Service
@Transactional(readOnly = true)
public class OrderQueryService { ... }

@Override
public String toString() { ... }

// ❌ Incorrect — annotation on same line
@Override public String toString() { ... }

Key annotations and where they belong:

Annotation Layer Notes
@Service Service class Marks a Spring service bean
@Repository Repository class Enables persistence exception translation
@RestController Controller class Combines @Controller + @ResponseBody
@Transactional Service layer Never on the controller layer
@Override Any overriding method Always include — the compiler verifies the override
@NonNull / @Nullable Parameters, return types Documents null contract explicitly

@Transactional belongs on the service layer — not the controller

Placing @Transactional on a controller method mixes concerns and can cause unexpected transaction scope issues. Transaction boundaries must be managed at the service layer.