Document Code Effectively

Problem

Poorly documented code forces developers to read implementation details to understand what code does, why it exists, and how to use it. Over-documentation clutters code with obvious comments that add no value. Missing documentation leaves critical context unrecorded, making maintenance difficult.

This guide shows how to document Java code effectively.

Javadoc Basics

Documenting Classes

/**
 * Manages user accounts and authentication.
 *
 * <p>This service handles user registration, login, password reset,
 * and account management operations. All operations are transactional
 * and log security events.
 *
 * <p>Example usage:
 * <pre>{@code
 * UserService service = new UserService(userRepository);
 * User user = service.register("alice@example.com", "password123");
 * boolean authenticated = service.authenticate("alice@example.com", "password123");
 * }</pre>
 *
 * @author Alice Johnson
 * @version 1.0
 * @since 2025-01-01
 */
public class UserService {
  // Implementation...
}

Documenting Methods

/**
 * Registers a new user with the specified email and password.
 *
 * <p>Creates a new user account, hashes the password using bcrypt,
 * and sends a verification email. The user account is initially
 * inactive until email verification completes.
 *
 * @param email    the user's email address (must be unique and valid)
 * @param password the user's password (minimum 8 characters)
 * @return the newly created user with generated ID
 * @throws IllegalArgumentException if email is invalid or password too short
 * @throws DuplicateEmailException  if email already registered
 * @throws EmailSendException       if verification email fails to send
 */
public User register(String email, String password) {
  validateEmail(email);
  validatePassword(password);

  User user = new User(email, hashPassword(password));
  user = userRepository.save(user);

  emailService.sendVerificationEmail(user);

  return user;
}

Key elements:

  • Summary: First sentence describes what method does
  • Details: Additional paragraphs explain behavior and constraints
  • @param: Describes each parameter and constraints
  • @return: Describes return value
  • @throws: Documents each exception type and when it’s thrown

Documenting Fields

public class Order {

  /**
   * Unique identifier for this order.
   * Generated automatically on creation.
   */
  private final String id;

  /**
   * Total price including tax and shipping.
   * Calculated when order is placed.
   */
  private BigDecimal totalPrice;

  /**
   * Maximum number of retry attempts for payment processing.
   * Default is 3 attempts.
   */
  private static final int MAX_RETRY_ATTEMPTS = 3;

  /**
   * Items in this order.
   * May be empty but never null.
   */
  private final List<OrderItem> items = new ArrayList<>();
}

When to Write Comments

Comments that Add Value

// ✅ Explain WHY, not WHAT
public BigDecimal calculateDiscount(Order order) {
  // Apply 10% discount to premium customers to match competitor pricing
  if (order.getCustomer().isPremium()) {
    return order.getTotal().multiply(new BigDecimal("0.10"));
  }

  // New customers get 5% discount to encourage first purchase
  if (order.getCustomer().isNew()) {
    return order.getTotal().multiply(new BigDecimal("0.05"));
  }

  return BigDecimal.ZERO;
}

// ✅ Clarify complex logic
public boolean isValidEmail(String email) {
  // Email must have exactly one @ symbol with content before and after
  // RFC 5322 allows more complex patterns, but we enforce stricter validation
  String[] parts = email.split("@");
  return parts.length == 2 &&
         !parts[0].isEmpty() &&
         !parts[1].isEmpty() &&
         parts[1].contains(".");
}

// ✅ Warn about gotchas
public void processOrders(List<Order> orders) {
  // Note: This modifies the input list! Caller should pass a copy if needed.
  orders.removeIf(order -> !order.isValid());

  for (Order order : orders) {
    processOrder(order);
  }
}

// ✅ TODO comments for future work
// TODO: Add retry logic for transient network failures
// TODO: Optimize this query - currently scans entire table
// FIXME: Race condition when multiple threads access shared state

Comments to Avoid

// ❌ Stating the obvious
// Get the user name
String name = user.getName();

// ❌ Redundant comments
/**
 * Sets the age.
 * @param age the age
 */
public void setAge(int age) {
  this.age = age;
}

// ❌ Outdated comments (worse than no comments!)
// Calculate discount (code now calculates tax instead!)
public BigDecimal calculateTax(Order order) {
  return order.getTotal().multiply(TAX_RATE);
}

// ❌ Commented-out code (use version control instead)
// public void oldMethod() {
//   // old implementation...
// }

Why it matters: Good comments explain intent, rationale, and context that code can’t express. Bad comments add noise without value. Self-documenting code reduces the need for comments.

Self-Documenting Code

Descriptive Naming

// ❌ Unclear names requiring comments
// Get all users created after the specified date
public List<User> getUsers(Date d) {
  // Filter users by creation date
  return users.stream()
    .filter(u -> u.getCreated().after(d))
    .collect(Collectors.toList());
}

// ✅ Clear names - comments unnecessary
public List<User> findUsersCreatedAfter(Date creationDate) {
  return users.stream()
    .filter(user -> user.getCreatedAt().isAfter(creationDate))
    .collect(Collectors.toList());
}

Extract Methods to Name Logic

// ❌ Complex logic requiring explanation
public void processOrder(Order order) {
  // Validate order has all required fields
  if (order.getCustomerId() == null ||
      order.getItems().isEmpty() ||
      order.getTotal().compareTo(BigDecimal.ZERO) <= 0) {
    throw new ValidationException("Invalid order");
  }

  // Check if customer is eligible for discount
  if (order.getCustomer().getOrderCount() > 10 &&
      order.getTotal().compareTo(new BigDecimal("100")) > 0) {
    applyDiscount(order);
  }

  processPayment(order);
}

// ✅ Extract methods with descriptive names
public void processOrder(Order order) {
  validateOrder(order);

  if (isEligibleForDiscount(order)) {
    applyDiscount(order);
  }

  processPayment(order);
}

private void validateOrder(Order order) {
  if (order.getCustomerId() == null) {
    throw new ValidationException("Customer ID required");
  }
  if (order.getItems().isEmpty()) {
    throw new ValidationException("Order must have at least one item");
  }
  if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) {
    throw new ValidationException("Order total must be positive");
  }
}

private boolean isEligibleForDiscount(Order order) {
  return order.getCustomer().getOrderCount() > 10 &&
         order.getTotal().compareTo(new BigDecimal("100")) > 0;
}

Package Documentation

package-info.java

/**
 * User management and authentication services.
 *
 * <p>This package provides:
 * <ul>
 *   <li>User registration and authentication</li>
 *   <li>Password hashing and validation</li>
 *   <li>Email verification</li>
 *   <li>Password reset functionality</li>
 * </ul>
 *
 * <p>Core classes:
 * <ul>
 *   <li>{@link UserService} - Main entry point for user operations</li>
 *   <li>{@link AuthenticationService} - Handles login and session management</li>
 *   <li>{@link PasswordHasher} - Secure password hashing with bcrypt</li>
 * </ul>
 *
 * <p>Example usage:
 * <pre>{@code
 * UserService userService = new UserService(userRepository);
 * User user = userService.register("alice@example.com", "password123");
 *
 * AuthenticationService authService = new AuthenticationService(userService);
 * Session session = authService.authenticate("alice@example.com", "password123");
 * }</pre>
 *
 * @since 1.0
 */
package com.example.user;

API Documentation

Public vs Internal Documentation

/**
 * Public API class with detailed documentation for external users.
 *
 * <p>This class is part of the public API and its behavior is guaranteed
 * to remain stable across minor version updates. Breaking changes will
 * only occur in major version updates.
 */
public class PublicService {

  /**
   * Processes the specified order.
   *
   * <p>This method is thread-safe and can be called concurrently.
   *
   * @param order the order to process (must not be null)
   * @return result containing order ID and status
   * @throws NullPointerException if order is null
   * @throws ValidationException  if order is invalid
   */
  public OrderResult process(Order order) {
    return internalProcess(order);
  }

  /**
   * Internal implementation details.
   * Not documented as extensively because it's not public API.
   */
  private OrderResult internalProcess(Order order) {
    // Implementation...
  }
}

Documenting Assumptions and Constraints

/**
 * Caches user data in memory for fast lookup.
 *
 * <p><strong>Thread Safety:</strong> This class is thread-safe.
 * All operations are synchronized.
 *
 * <p><strong>Memory:</strong> Cache holds maximum 10,000 users.
 * Oldest entries evicted when limit reached (LRU policy).
 *
 * <p><strong>Expiration:</strong> Cached entries expire after 1 hour.
 * Stale data removed on next access.
 *
 * <p><strong>Null Handling:</strong> This class does not accept null
 * values. All methods throw {@link NullPointerException} if passed null.
 */
public class UserCache {
  // Implementation...
}

Code Examples in Documentation

Effective Examples

/**
 * Validates credit card numbers using the Luhn algorithm.
 *
 * <p>Example usage:
 * <pre>{@code
 * CreditCardValidator validator = new CreditCardValidator();
 *
 * // Valid card number
 * boolean valid = validator.validate("4532015112830366");
 * // Returns: true
 *
 * // Invalid card number
 * boolean invalid = validator.validate("1234567890123456");
 * // Returns: false
 *
 * // Handle invalid format
 * try {
 *   validator.validate("not-a-number");
 * } catch (IllegalArgumentException e) {
 *   // Handle error
 * }
 * }</pre>
 *
 * @see <a href="https://en.wikipedia.org/wiki/Luhn_algorithm">Luhn Algorithm</a>
 */
public class CreditCardValidator {
  // Implementation...
}

README Documentation

Project README Structure

Brief description of what the application does and why it exists.

## Features

- User authentication and authorization
- Order processing and payment
- Real-time notifications
- Analytics dashboard

## Prerequisites

- Java 17 or higher
- PostgreSQL 14 or higher
- Maven 3.8 or higher

## Installation

```bash
git clone https://github.com/example/myapp.git
cd myapp

mvn clean install

mvn test
```

Configuration

Create application.properties in src/main/resources:

database.url=jdbc:postgresql://localhost:5432/mydb
database.username=dbuser
database.password=dbpass

Usage

// Example code showing how to use the main API
UserService service = new UserService();
User user = service.register("alice@example.com", "password123");

Architecture

Brief overview of system architecture, major components, and how they interact.

Contributing

Guidelines for contributors.

License

MIT License - see LICENSE file for details.


## Documentation Generation

### Generating Javadoc

```bash
mvn javadoc:javadoc

javadoc -d docs/api -sourcepath src/main/java com.example.user

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <version>3.5.0</version>
  <configuration>
    <show>public</show>
    <nohelp>true</nohelp>
  </configuration>
</plugin>

Summary

Effective documentation explains intent, constraints, and usage without stating the obvious. Javadoc documents public APIs comprehensively with descriptions, parameter details, return values, and exceptions. First sentence becomes summary, additional paragraphs provide details, and code examples show usage patterns.

Comments should explain why code exists and why it’s written a particular way, not what the code does. Explain business logic rationale, document assumptions, warn about gotchas, and mark areas needing future work. Avoid obvious comments that duplicate what code already says clearly.

Self-documenting code reduces comment needs through descriptive names and well-structured logic. Extract complex conditionals into methods with clear names. Choose variable and method names that express intent. Good code structure communicates what and how, comments explain why.

Package-level documentation through package-info.java explains package purpose, lists key classes, and provides usage examples. This gives overview before developers dive into individual classes. Document public APIs thoroughly, internal implementations lightly.

Document assumptions and constraints explicitly - thread safety, memory limits, null handling, performance characteristics. Future maintainers need this context. Include code examples showing common usage patterns and edge cases.

README files serve different audiences than API documentation. README targets users setting up and running the application. Include installation steps, configuration examples, feature list, and architectural overview. Keep it updated as the project evolves.

Generate Javadoc regularly to ensure documentation stays current. Configure build tools to fail on Javadoc warnings, catching documentation issues early. Good documentation becomes part of code quality standards, not an afterthought.

Balance comprehensive documentation with avoiding over-documentation. Public APIs need thorough documentation. Internal implementation details need less. Trust developers to read code when appropriate. Document what’s not obvious from the code itself.

Related Content

Last updated