Javadoc Best Practices for Spring Boot - 5-Minute Professional Guide 2025

You’ve written clean code. Your tests pass. Your PR is ready. Then comes the review comment: “Please add Javadoc.”

Sound familiar?

Most developers know they should document their code, but Javadoc often feels like an afterthought—time-consuming, repetitive, and unclear about what actually matters.

Here’s the truth: Good Javadoc isn’t about documenting everything. It’s about documenting the right things, efficiently.

This guide distills professional Javadoc practices into 5 minutes of focused learning. You’ll master the essential tags, learn production-ready patterns used in enterprise Spring Boot projects, and set up IntelliJ templates that write documentation for you.


Why Javadoc Matters in Professional Development

The Real Cost of Poor Documentation

// ❌ Six months later, even YOU won't remember what this does
public Optional extract(Request req) {
    String id = req.getId();
    if (id != null && id.contains("-")) {
        return Optional.of(id.substring(0, id.lastIndexOf("-")));
    }
    return Optional.empty();
}

// ✅ Future you (and your team) will thank you
/**
 * Extracts the order ID from the request by removing the timestamp suffix.
 * 
 * Request ID format: {@code {orderId}-{timestampInNs}}
 * 
 * Example: {@code "ORDER123-1234567890"} → {@code "ORDER123"}
 *
 * @param req the request containing the ID to parse
 * @return the order ID, or empty if null/invalid
 */
public Optional extract(Request req) {
    // Same implementation, but now self-documenting
}

Impact in real projects:

  • Onboarding time: New developers understand code 3x faster with good Javadoc
  • Code reviews: Reviewers spend less time asking “what does this do?”
  • Maintenance: Future changes require less reverse-engineering
  • API clarity: Public methods become self-explanatory

According to the Google Java Style Guide, every public class and public/protected method should have Javadoc—but quality matters more than quantity.

The 80/20 Rule of Javadoc: What Actually Matters

You don’t need to memorize 30+ Javadoc tags. Focus on these 5 essential tags that cover 90% of professional documentation needs:

The Essential Five

graph TD
    A[Method to Document] --> B{What needs explaining?}
    B -->|Input| C[@param]
    B -->|Output| D[@return]
    B -->|Errors| E[@throws]
    B -->|Related code| F[@see]
    B -->|Deprecated| G[@deprecated]
    
    style C fill:#90EE90
    style D fill:#90EE90
    style E fill:#FFB6C1
    style F fill:#87CEEB
    style G fill:#FFD700

1. @param - Document Method Parameters

/**
 * Creates a new user account with validation.
 *
 * @param email the user's email address (must be unique and valid format)
 * @param password the user's password (minimum 8 characters required)
 * @param role the user's role (ADMIN, USER, or GUEST)
 */
public User createUser(String email, String password, UserRole role) {
    // implementation
}

Pro tip: Don’t just repeat the parameter name—explain constraints, format requirements, or special meanings.

2. @return - Document Return Values

/**
 * Finds a user by their unique identifier.
 *
 * @param userId the unique user identifier
 * @return the user object if found, or {@code Optional.empty()} if not found
 */
public Optional<User> findById(String userId) {
    // implementation
}

Common mistake: Writing “returns a User” when it’s obvious. Instead, explain when it returns different values (null, empty, error states).

3. @throws - Document Exceptions

/**
 * Processes a payment transaction.
 *
 * @param payment the payment details to process
 * @return the transaction result with confirmation ID
 * @throws IllegalArgumentException if payment amount is negative or zero
 * @throws PaymentDeclinedException if the payment is rejected by the gateway
 * @throws ServiceUnavailableException if the payment service is temporarily down
 */
public PaymentResult processPayment(Payment payment) 
        throws PaymentDeclinedException, ServiceUnavailableException {
    // implementation
}

Critical: Document why each exception is thrown, not just what exception it is.

/**
 * Validates user credentials and generates an authentication token.
 *
 * @param username the username to authenticate
 * @param password the password to verify
 * @return an authentication token if credentials are valid
 * @throws AuthenticationException if credentials are invalid
 * @see TokenService#generateToken(User)
 * @see #refreshToken(String)
 */
public String authenticate(String username, String password) {
    // implementation
}

Pro tip: Use @see to create navigable documentation—help developers discover related functionality.

5. @deprecated - Mark Obsolete Code

/**
 * Processes an order using the legacy system.
 *
 * @param order the order to process
 * @return the processing result
 * @deprecated Use {@link #processOrderV2(Order)} instead.
 *             This method will be removed in version 3.0.
 * @see #processOrderV2(Order)
 */
@Deprecated
public OrderResult processOrder(Order order) {
    // implementation
}

Critical: Always tell developers what to use instead and when the deprecated method will be removed.


Inline Formatting: Making Your Javadoc Readable

Good formatting makes documentation scannable and maintainable.

Essential Inline Tags

{@code} - For Code Examples

/**
 * Validates that the email follows the pattern {@code username@domain.extension}.
 * 
 * Examples:
 * 
 *   {@code "user@example.com"} - Valid
 *   {@code "user@domain"} - Invalid (missing extension)
 *   {@code "@example.com"} - Invalid (missing username)
 * 
 *
 * @param email the email address to validate
 * @return true if valid, false otherwise
 */
public boolean isValidEmail(String email) {
    // implementation
}

Why {@code} instead of <code>?

  • Automatically escapes HTML characters (no need to worry about <, >, &)
  • Cleaner in source code
  • Works correctly with generics: {@code List<String>}
/**
 * Processes a payment using the configured payment gateway.
 * 
 * The payment is validated before processing using {@link PaymentValidator#validate(Payment)}.
 * Upon success, a confirmation email is sent via {@link EmailService#sendPaymentConfirmation(User, Payment)}.
 *
 * @param payment the payment to process
 * @return the payment result with transaction ID
 * @throws InvalidPaymentException if payment validation fails
 * @see PaymentGateway#charge(Payment)
 * @see #refundPayment(String)
 */
public PaymentResult processPayment(Payment payment) {
    // implementation
}

Pro tip: Use {@link} to create clickable links in IDEs—helps code navigation.

{@value} - For Constant Values

/**
 * Service for rate limiting API requests.
 * 
 * Default rate limit is {@value #DEFAULT_RATE_LIMIT} requests per minute.
 * This can be overridden via {@code rate.limit.max-requests} property.
 *
 * @author Imad ALILAT
 * @since 1.0.0
 */
@Service
public class RateLimitService {
    
    /**
     * Default maximum requests per minute.
     */
    public static final int DEFAULT_RATE_LIMIT = 100;
    
    // implementation
}

HTML Formatting for Structure

Lists - The Most Useful Format

/**
 * Processes a bulk order with the following validations:
 * <ul>
 *   <li>All items must be in stock</li>
 *   <li>Total price must not exceed user's credit limit</li>
 *   <li>Shipping address must be valid</li>
 * </ul>
 * Processing steps:
 * <ol>
 *   <li>Validate inventory availability</li>
 *   <li>Reserve items in inventory</li>
 *   <li>Process payment</li>
 *   <li>Schedule shipment</li>
 *   <li>Send confirmation email</li>
 * </ol>
 *
 * @param order the order to process with line items and shipping details
 * @return the order result with tracking information
 * @throws InsufficientInventoryException if any item is out of stock
 * @throws PaymentDeclinedException if payment processing fails
 */
public OrderResult processBulkOrder(Order order) {
    // implementation
}

When to use which list:

  • <ul> (unordered) - For features, constraints, or requirements (no specific order)
  • <ol> (ordered) - For steps in a process or sequential operations

Paragraphs for Readability

/**
 * Authenticates a user and generates a JWT token.
 * <p>
 * This method performs the following security checks:
 * <ul>
 *   <li>Validates username exists</li>
 *   <li>Verifies password hash matches</li>
 *   <li>Checks if account is active</li>
 *   <li>Validates no active account locks</li>
 * </ul>
 * <p>
 * On successful authentication, a JWT token is generated with:
 * <ul>
 *   <li>User ID and email as claims</li>
 *   <li>User roles for authorization</li>
 *   <li>Expiration time of {@value #TOKEN_EXPIRY_HOURS} hours</li>
 * </ul>
 * <p>
 * <b>Security Note:</b> Failed login attempts are logged and may trigger
 * account lockout after {@value #MAX_LOGIN_ATTEMPTS} consecutive failures.
 *
 * @param username the username (email) to authenticate
 * @param password the plaintext password to verify
 * @return a JWT token valid for {@value #TOKEN_EXPIRY_HOURS} hours
 * @throws AuthenticationException if credentials are invalid
 * @throws AccountLockedException if the account is temporarily locked
 * @see #refreshToken(String)
 */
public String authenticate(String username, String password) {
    // implementation
}

Pro tip: Use <p> to separate logical sections—makes long documentation easier to scan.

Emphasis for Important Information

/**
 * Deletes a user account permanently.
 * <p>
 * <b>WARNING:</b> This operation is irreversible. All user data, including:
 * <ul>
 *   <li>Profile information</li>
 *   <li>Order history</li>
 *   <li>Saved preferences</li>
 * </ul>
 * will be permanently deleted.
 * <p>
 * <i>Note:</i> Consider using {@link #deactivateUser(String)} instead for
 * reversible account suspension.
 *
 * @param userId the ID of the user to delete
 * @throws UserNotFoundException if user doesn't exist
 * @throws IllegalStateException if user has pending orders
 * @see #deactivateUser(String)
 */
@Transactional
public void deleteUser(String userId) {
    // implementation
}

Use sparingly:

  • <b>bold</b> - For warnings or critical information
  • <i>italic</i> - For notes or less critical emphasis

Javadoc for Spring Boot: Layer-by-Layer Patterns

Spring Boot applications follow a layered architecture. Each layer has specific documentation needs:

Controller Layer Documentation

Controllers are your API’s front door—document the contract clearly:

/**
 * REST controller for managing user accounts.
 * 
 * Base path: {@code /api/v1/users}
 * 
 * All endpoints require authentication except user registration.
 *
 * @author Imad ALILAT
 * @since 1.0.0
 */
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * Creates a new user account.
     * 
     * Validates email uniqueness and password strength before creation.
     *
     * @param request the user creation request containing email, password, and name
     * @return the created user with generated ID and creation timestamp
     * @throws BadRequestException if email already exists or password is too weak
     * @throws ServiceException if user creation fails due to database error
     */
    @PostMapping
    public ResponseEntity createUser(@RequestBody @Valid CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(user));
    }

    /**
     * Retrieves a user by their unique identifier.
     *
     * @param userId the unique user identifier (UUID format)
     * @return the user details if found
     * @throws NotFoundException if no user exists with the given ID
     * @throws UnauthorizedException if the authenticated user lacks permission
     */
    @GetMapping("/{userId}")
    public ResponseEntity getUser(@PathVariable String userId) {
        User user = userService.findById(userId);
        return ResponseEntity.ok(toResponse(user));
    }
}

Key points for controllers:

  • Document the API contract (paths, methods, auth requirements)
  • Explain HTTP status codes implicitly through exception documentation
  • Link to request/response DTOs when helpful
  • Mention validation rules that affect the API

Service Layer Documentation

Services contain business logic—focus on the what and why, not the how:

/**
 * Service for managing user accounts and authentication.
 * <p>
 * Responsibilities:
 * <ul>
 *   <li>User registration with email verification</li>
 *   <li>Password management and reset</li>
 *   <li>User profile updates</li>
 *   <li>Account deactivation and deletion</li>
 * </ul>
 *
 * @author Imad ALILAT
 * @since 1.0.0
 * @see UserRepository
 * @see EmailService
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;
    private final PasswordEncoder passwordEncoder;

    /**
     * Creates a new user account with the specified details.
     * <p>
     * Process:
     * <ol>
     *   <li>Validates email uniqueness</li>
     *   <li>Hashes the password</li>
     *   <li>Saves user to database</li>
     *   <li>Sends welcome email</li>
     * </ol>
     * <p>
     * <i>Note:</i> The user account is created in PENDING status until email verification.
     *
     * @param request the user creation request with email, password, and optional name
     * @return the created user with generated ID, PENDING status, and creation timestamp
     * @throws EmailAlreadyExistsException if a user with this email already exists
     * @throws IllegalArgumentException if password doesn't meet security requirements (min 8 chars, 1 uppercase, 1 number)
     * @see #verifyEmail(String)
     */
    @Transactional
    public User createUser(CreateUserRequest request) {
        log.info("Creating user with email: {}", request.getEmail());
        
        validateEmailUnique(request.getEmail());
        
        User user = User.builder()
            .email(request.getEmail())
            .password(passwordEncoder.encode(request.getPassword()))
            .name(request.getName())
            .status(UserStatus.PENDING)
            .createdAt(LocalDateTime.now())
            .build();
        
        User savedUser = userRepository.save(user);
        emailService.sendWelcomeEmail(savedUser.getEmail());
        
        log.info("User created successfully with ID: {}", savedUser.getId());
        return savedUser;
    }

    /**
     * Finds a user by their unique identifier.
     *
     * @param userId the unique user identifier (UUID format)
     * @return the user entity with all fields populated
     * @throws UserNotFoundException if no user exists with the given ID
     * @throws IllegalArgumentException if userId is null or empty
     */
    public User findById(String userId) {
        if (userId == null || userId.isBlank()) {
            throw new IllegalArgumentException("User ID cannot be null or empty");
        }
        
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId));
    }
}

Key points for services:

  • Document business rules and validation logic
  • Explain transaction boundaries (use of @Transactional)
  • List processing steps for complex operations
  • Link related services and repositories

Repository Layer Documentation

Repositories handle data access—document custom queries and special behaviors:

/**
 * Repository for {@link User} entity persistence operations.
 * 
 * Provides CRUD operations and custom queries for user management.
 *
 * @author Imad ALILAT
 * @since 1.0.0
 */
public interface UserRepository extends JpaRepository {

    /**
     * Finds a user by their email address.
     * 
     * Email comparison is case-insensitive.
     *
     * @param email the email address to search for (case-insensitive)
     * @return an optional containing the user if found, or empty if not found
     */
    Optional findByEmailIgnoreCase(String email);

    /**
     * Finds all users with the specified status.
     * 
     * Results are ordered by creation date (newest first).
     *
     * @param status the user status to filter by (ACTIVE, PENDING, SUSPENDED, DELETED)
     * @return a list of users with the given status, or empty list if none found
     */
    List findByStatusOrderByCreatedAtDesc(UserStatus status);

    /**
     * Checks if a user exists with the given email address.
     * 
     * This is more efficient than {@link #findByEmailIgnoreCase(String)} when you only
     * need to check existence without loading the entity.
     *
     * @param email the email address to check (case-insensitive)
     * @return true if a user exists with this email, false otherwise
     */
    boolean existsByEmailIgnoreCase(String email);

    /**
     * Finds all users created within the specified date range.
     * 
     * The query includes users created on the start date and excludes the end date
     * (i.e., {@code createdAt >= startDate AND createdAt < endDate}).
     *
     * @param startDate the start of the date range (inclusive)
     * @param endDate the end of the date range (exclusive)
     * @return a list of users created in the date range, ordered by creation date
     */
    @Query("SELECT u FROM User u WHERE u.createdAt >= :startDate AND u.createdAt < :endDate ORDER BY u.createdAt")
    List findUsersCreatedBetween(@Param("startDate") LocalDateTime startDate, 
                                       @Param("endDate") LocalDateTime endDate);
}

Key points for repositories:

  • Document query behavior (case-sensitivity, ordering, null handling)
  • Explain custom queries and their performance characteristics
  • Note any database-specific behavior
  • Mention alternatives when appropriate (e.g., exists vs find)

Common Javadoc Mistakes and How to Avoid Them

Mistake #1: Stating the Obvious (click to expand)
// ❌ BAD: Repeats what the code already says
/**
 * Gets the user name.
 *
 * @return the user name
 */
public String getUserName() {
    return userName;
}

// ✅ GOOD: Omit trivial getters or add useful context
/**
 * Gets the user's display name.
 * 
 * Returns the full name if available, otherwise returns the email username.
 *
 * @return the display name (never null or empty)
 */
public String getUserName() {
    return fullName != null ? fullName : email.split("@")[0];
}

Rule: If the Javadoc doesn’t add information beyond the method signature, skip it or add valuable context.

Mistake #2: Forgetting to Escape HTML Characters (click to expand)
// ❌ BAD: Will break HTML rendering
/**
 * Checks if value is in range [min, max].
 * Returns List of errors.
 */
public boolean inRange(int value, int min, int max) {
    // implementation
}

// ✅ GOOD: Use {@code} to avoid HTML issues
/**
 * Checks if value is in the range {@code [min, max]} (inclusive).
 * 
 * Returns a {@code List} of validation errors if any.
 *
 * @param value the value to check
 * @param min the minimum allowed value (inclusive)
 * @param max the maximum allowed value (inclusive)
 * @return true if value is within range, false otherwise
 */
public boolean inRange(int value, int min, int max) {
    // implementation
}

Common characters that need escaping:

  • < and > - Use {@code} tag
  • & - Use {@code} or HTML entity &amp;
  • @ - Escape as {@literal @} if not a Javadoc tag
Mistake #3: Not Closing HTML Tags (click to expand)
// ❌ BAD: Unclosed tags break formatting
/**
 * Supported payment types:
 * 
 *   Credit card
 *   Debit card
 *   PayPal
 */
public enum PaymentType {
    // implementation
}

// ✅ GOOD: Always close tags
/**
 * Supported payment types:
 * 
 *   Credit card
 *   Debit card
 *   PayPal
 * 
 *
 * @since 1.0.0
 */
public enum PaymentType {
    // implementation
}

Pro tip: Most IDEs will highlight unclosed tags—pay attention to warnings!

Mistake #4: Using @param for Fields (click to expand)
// ❌ BAD: @param only works for methods
/**
 * The user's email address.
 *
 * @param email the email address  // WRONG!
 */
private String email;

// ✅ GOOD: Just describe the field directly
/**
 * The user's email address (must be unique and validated).
 * 
 * Format: {@code username@domain.extension}
 */
private String email;

Rule: Use @param, @return, and @throws only for methods and constructors, not fields.

Mistake #5: Documenting Implementation Details (click to expand)
// ❌ BAD: Exposes implementation (might change)
/**
 * Finds users by email.
 * 
 * Uses a HashMap internally for O(1) lookup performance.
 * Queries the database using Hibernate Criteria API.
 *
 * @param email the email to search
 * @return the user if found
 */
public Optional findByEmail(String email) {
    // implementation
}

// ✅ GOOD: Documents behavior, not implementation
/**
 * Finds a user by their email address.
 * 
 * Email comparison is case-insensitive.
 *
 * @param email the email to search (case-insensitive)
 * @return an optional containing the user if found, or empty otherwise
 */
public Optional findByEmail(String email) {
    // implementation
}

Rule: Document what the method does and why, not how it does it. Implementation details belong in code comments, not Javadoc.


IntelliJ IDEA: Automate Your Javadoc with Live Templates

Stop typing the same Javadoc structure repeatedly. IntelliJ’s live templates let you create documentation in seconds.

Setting Up Live Templates

Path: Settings → Editor → Live Templates → Create new template group “Javadoc”

Template #1: Method Javadoc (Shortcut: `jdoc`) (click to expand)
/**
 * $DESCRIPTION$
 *
 * $DETAILS$
 *
 * @param $PARAM$ $PARAM_DESC$
 * @return $RETURN_DESC$
 * @throws $EXCEPTION$ $EXCEPTION_DESC$
 */

Usage:

  1. Type jdoc above any method
  2. Press Tab
  3. Fill in the placeholders
  4. Press Tab to move between fields

Context: Java → Declaration

Template #2: Class Javadoc (Shortcut: `jclass`) (click to expand)
/**
 * $DESCRIPTION$
 *
 * $RESPONSIBILITIES$
 *
 * @author $USER$
 * @since $VERSION$
 * @see $RELATED_CLASS$
 */

Variables:

  • $USER$ → Expression: user()
  • $VERSION$ → Expression: "1.0.0" (or your versioning scheme)
Template #3: Constructor Javadoc (Shortcut: `jctor`) (click to expand)
/**
 * Creates a new $CLASS_NAME$ with the specified $PARAM$.
 *
 * @param $PARAM$ $PARAM_DESC$
 * @throws $EXCEPTION$ if $EXCEPTION_CONDITION$
 */

Variables:

  • $CLASS_NAME$ → Expression: className()
Template #4: Spring Controller Endpoint (Shortcut: `jendpoint`) (click to expand)
/**
 * $HTTP_METHOD$ $DESCRIPTION$
 *
 * Endpoint: {@code $HTTP_METHOD$ $ENDPOINT_PATH$}
 *
 * @param $PARAM$ $PARAM_DESC$
 * @return $RETURN_DESC$
 * @throws $EXCEPTION$ if $EXCEPTION_CONDITION$
 */
Template #5: Deprecated Method (Shortcut: `jdeprecated`) (click to expand)
/**
 * $DESCRIPTION$
 *
 * @param $PARAM$ $PARAM_DESC$
 * @return $RETURN_DESC$
 * @deprecated Use {@link #$REPLACEMENT_METHOD$} instead.
 *             This method will be removed in version $REMOVAL_VERSION$.
 * @see #$REPLACEMENT_METHOD$
 */
@Deprecated
Template #6: Exception Documentation (Shortcut: `jexception`) (click to expand)
/**
 * Thrown when $CONDITION$.
 *
 * This exception indicates $EXPLANATION$.
 *
 * @author $USER$
 * @since $VERSION$
 * @see $RELATED_EXCEPTION$
 */

Quick IntelliJ Tips

Generate Javadoc quickly:

  1. Place cursor on method
  2. Press Alt + Insert (Windows/Linux) or Cmd + N (Mac)
  3. Select “Generate JavaDoc”
  4. IntelliJ creates basic structure from signature

Fix Javadoc warnings:

  1. Press Alt + Enter on warning
  2. IntelliJ suggests fixes (missing @param, @return, etc.)

Format Javadoc:

  1. Select Javadoc comment
  2. Press Ctrl + Alt + L (Windows/Linux) or Cmd + Option + L (Mac)
  3. IntelliJ reformats according to code style

Real-World Example: Complete Class Documentation

Here’s a production-ready example showing all concepts together:

Complete PaymentService Class Documentation Example (click to expand)
package com.devadvisor.payment;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service for processing payment transactions through external payment gateways.
 * <p>
 * This service:
 * <ul>
 *   <li>Validates payment details before processing</li>
 *   <li>Integrates with multiple payment gateways (Stripe, PayPal)</li>
 *   <li>Handles payment failures with automatic retry logic</li>
 *   <li>Sends payment confirmations via email</li>
 * </ul>
 * <p>
 * <b>Transaction Handling:</b> All public methods are transactional and will
 * rollback on any exception.
 * <p>
 * <b>Rate Limiting:</b> Payment processing is subject to rate limiting of
 * {@value #MAX_PAYMENTS_PER_MINUTE} payments per minute per user.
 *
 * @author Imad ALILAT
 * @since 1.0.0
 * @see PaymentGateway
 * @see PaymentValidator
 * @see EmailService
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final PaymentGateway paymentGateway;
    private final PaymentValidator paymentValidator;
    private final EmailService emailService;
    private final RateLimitService rateLimitService;

    /**
     * Maximum payments allowed per minute per user.
     */
    public static final int MAX_PAYMENTS_PER_MINUTE = 10;

    /**
     * Maximum retry attempts for failed payments.
     */
    private static final int MAX_RETRY_ATTEMPTS = 3;

    /**
     * Processes a payment transaction with automatic retry on transient failures.
     * <p>
     * The payment processing follows these steps:
     * <ol>
     *   <li>Rate limit check for the user</li>
     *   <li>Validation of payment details (amount, card number, CVV)</li>
     *   <li>External gateway processing (Stripe/PayPal)</li>
     *   <li>Database persistence of transaction</li>
     *   <li>Confirmation email to user</li>
     * </ol>
     * <p>
     * <b>Retry Logic:</b> Transient failures (network issues, gateway timeouts)
     * are retried up to {@value #MAX_RETRY_ATTEMPTS} times with exponential backoff.
     * Permanent failures (card declined, insufficient funds) are not retried.
     * <p>
     * <b>Idempotency:</b> If the same payment is submitted multiple times with the
     * same idempotency key, only the first payment is processed. Subsequent requests
     * return the original transaction result.
     *
     * @param payment the payment request containing amount, card details, and idempotency key
     * @param userId the ID of the user making the payment
     * @return the payment result with transaction ID, status, and gateway response
     * @throws RateLimitExceededException if user exceeds {@value #MAX_PAYMENTS_PER_MINUTE} payments per minute
     * @throws InvalidPaymentException if payment validation fails (invalid amount, expired card, etc.)
     * @throws PaymentDeclinedException if the payment is permanently declined by the gateway
     * @throws PaymentGatewayException if the gateway is unavailable after all retry attempts
     * @throws IllegalArgumentException if payment or userId is null
     * @see #refundPayment(String, String, BigDecimal)
     * @see PaymentValidator#validate(Payment)
     */
    @Transactional
    public PaymentResult processPayment(Payment payment, String userId)
            throws RateLimitExceededException, InvalidPaymentException, PaymentDeclinedException {

        log.info("Processing payment for user: {}, amount: {}", userId, payment.getAmount());

        // Validate inputs
        if (payment == null || userId == null) {
            throw new IllegalArgumentException("Payment and userId cannot be null");
        }

        // Check rate limit
        if (!rateLimitService.allowPayment(userId)) {
            log.warn("Rate limit exceeded for user: {}", userId);
            throw new RateLimitExceededException(
                String.format("Rate limit of %d payments per minute exceeded", MAX_PAYMENTS_PER_MINUTE)
            );
        }

        // Validate payment details
        paymentValidator.validate(payment);

        // Process with retry logic
        PaymentResult result = processWithRetry(payment, userId);

        // Send confirmation
        emailService.sendPaymentConfirmation(userId, result);

        log.info("Payment processed successfully. Transaction ID: {}", result.getTransactionId());
        return result;
    }

    /**
     * Refunds a previously processed payment.
     * <p>
     * The refund must meet these conditions:
     * <ul>
     *   <li>Original payment must exist and be in COMPLETED status</li>
     *   <li>Refund amount must not exceed original payment amount</li>
     *   <li>Refund must be requested within 90 days of original payment</li>
     * </ul>
     * <p>
     * <b>Partial Refunds:</b> Multiple partial refunds are supported as long as
     * the total refunded amount doesn't exceed the original payment.
     *
     * @param transactionId the ID of the original payment transaction to refund
     * @param userId the ID of the user requesting the refund (must match original payer)
     * @param amount the amount to refund (must be positive and not exceed remaining balance)
     * @return the refund result with refund ID and updated balances
     * @throws PaymentNotFoundException if no payment exists with the given transaction ID
     * @throws UnauthorizedException if the userId doesn't match the original payer
     * @throws InvalidRefundException if refund conditions are not met
     * @throws PaymentGatewayException if the gateway refund fails
     * @see #processPayment(Payment, String)
     */
    @Transactional
    public RefundResult refundPayment(String transactionId, String userId, BigDecimal amount)
            throws PaymentNotFoundException, InvalidRefundException {

        log.info("Processing refund for transaction: {}, amount: {}", transactionId, amount);

        // Validate refund eligibility
        Payment originalPayment = validateRefundEligibility(transactionId, userId, amount);

        // Process refund with gateway
        RefundResult result = paymentGateway.refund(transactionId, amount);

        // Update database
        paymentRepository.recordRefund(transactionId, result);

        // Send confirmation
        emailService.sendRefundConfirmation(userId, result);

        log.info("Refund processed successfully. Refund ID: {}", result.getRefundId());
        return result;
    }

    /**
     * Retrieves the payment history for a specific user.
     * <p>
     * Results are ordered by payment date (newest first) and include both
     * successful and failed payments.
     * <p>
     * <i>Performance Note:</i> This method is optimized for pagination. For large
     * datasets, use {@link #getPaymentHistory(String, int, int)} instead.
     *
     * @param userId the ID of the user whose payment history to retrieve
     * @return a list of all payments for the user, ordered by date (newest first)
     * @throws IllegalArgumentException if userId is null or empty
     * @see #getPaymentHistory(String, int, int)
     */
    public List getPaymentHistory(String userId) {
        if (userId == null || userId.isBlank()) {
            throw new IllegalArgumentException("User ID cannot be null or empty");
        }

        log.debug("Retrieving payment history for user: {}", userId);
        return paymentRepository.findByUserIdOrderByCreatedAtDesc(userId);
    }

    /**
     * Processes payment with exponential backoff retry logic for transient failures.
     *
     * @param payment the payment to process
     * @param userId the user making the payment
     * @return the payment result
     * @throws PaymentDeclinedException if payment is permanently declined
     * @throws PaymentGatewayException if all retry attempts fail
     */
    private PaymentResult processWithRetry(Payment payment, String userId)
            throws PaymentDeclinedException, PaymentGatewayException {

        for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
            try {
                return paymentGateway.charge(payment);
            } catch (TransientGatewayException e) {
                log.warn("Payment attempt {} failed for user {}: {}", attempt, userId, e.getMessage());

                if (attempt == MAX_RETRY_ATTEMPTS) {
                    throw new PaymentGatewayException("Payment failed after " + MAX_RETRY_ATTEMPTS + " attempts", e);
                }

                // Exponential backoff: wait 2^attempt seconds
                try {
                    Thread.sleep((long) Math.pow(2, attempt) * 1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new PaymentGatewayException("Payment processing interrupted", ie);
                }
            }
        }

        throw new PaymentGatewayException("Unexpected retry loop termination");
    }

    /**
     * Validates that a refund request meets all eligibility requirements.
     *
     * @param transactionId the transaction to refund
     * @param userId the user requesting refund
     * @param amount the refund amount
     * @return the original payment if eligible
     * @throws PaymentNotFoundException if payment doesn't exist
     * @throws InvalidRefundException if refund conditions aren't met
     */
    private Payment validateRefundEligibility(String transactionId, String userId, BigDecimal amount)
            throws PaymentNotFoundException, InvalidRefundException {

        Payment payment = paymentRepository.findById(transactionId)
            .orElseThrow(() -> new PaymentNotFoundException("Payment not found: " + transactionId));

        if (!payment.getUserId().equals(userId)) {
            throw new UnauthorizedException("User does not own this payment");
        }

        if (payment.getStatus() != PaymentStatus.COMPLETED) {
            throw new InvalidRefundException("Can only refund completed payments");
        }

        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidRefundException("Refund amount must be positive");
        }

        if (amount.compareTo(payment.getRemainingBalance()) > 0) {
            throw new InvalidRefundException("Refund amount exceeds remaining balance");
        }

        LocalDateTime paymentDate = payment.getCreatedAt();
        LocalDateTime ninetyDaysAgo = LocalDateTime.now().minusDays(90);
        if (paymentDate.isBefore(ninetyDaysAgo)) {
            throw new InvalidRefundException("Refund window expired (90 days)");
        }

        return payment;
    }
}

The 5-Minute Documentation Checklist

Before you commit your code, run through this quick checklist:

For Public Methods (2 minutes)

☐ First sentence clearly states what the method does
☐ Complex logic explained with <ul> or <ol> lists
☐ All @param documented with constraints/format
☐ @return documents all possible return scenarios
☐ All @throws documented with conditions
☐ {@code} used for inline code examples
☐ {@link} used for related methods

For Classes (1 minute)

☐ Class purpose clearly stated
☐ Responsibilities listed with <ul>
☐ @author tag present
☐ @since tag with version
☐ Key dependencies linked with @see

For Complex Algorithms (2 minutes)

☐ High-level approach explained
☐ Edge cases documented
☐ Performance characteristics noted (if relevant)
☐ Example inputs/outputs provided with {@code}
☐ Related methods linked with @see

Key Takeaways & Best Practices

Essential Principles

  1. Focus on the “Why” and “What” - Don’t document how (that’s what code is for)
  2. Use the 80/20 Rule - Master 5 essential tags (@param, @return, @throws, @see, @deprecated)
  3. Make It Scannable - Use <ul>, <ol>, <p>, and <b> for structure
  4. Leverage {@code} and {@link} - For examples and navigation
  5. Automate with Templates - Let IntelliJ do the repetitive work

When to Write Javadoc

✅ Always document:

  • Public APIs (classes, methods, constructors)
  • Complex business logic
  • Non-obvious behaviors
  • Edge cases and constraints
  • Validation rules
  • Exception conditions

❌ Skip documentation for:

  • Private helper methods (use code comments instead)
  • Trivial getters/setters
  • Self-explanatory methods
  • Test methods (test names should be self-documenting)

Quality Over Quantity

Bad Javadoc is worse than no Javadoc—it creates false confidence and misleads developers. A well-documented public API with clear examples beats comprehensive but vague documentation every time.


What’s Next?

Now that you’ve mastered Javadoc fundamentals, consider these advanced topics:

Generate API Documentation

  • JavaDoc HTML Generation: mvn javadoc:javadoc to create browsable docs
  • Spring REST Docs: Generate API docs from your tests
  • OpenAPI/Swagger: For REST API documentation

Code Quality Tools

  • Checkstyle: Enforce Javadoc presence with JavadocStyle checks
  • SonarQube: Track documentation coverage in CI/CD
  • IntelliJ Inspections: Enable “Missing Javadoc” warnings

Team Documentation Standards

  • Create a team Javadoc style guide
  • Share IntelliJ live templates across the team
  • Include Javadoc review in PR checklist

Essential Documentation

Companion Content


Last updated: November 2025 | Java 21+ | Spring Boot 3.x compatible

Happy Documenting! 🚀

Related Posts

Threads, @Async, @Transactional, and Virtual Threads: What Actually Happens Inside a Spring Boot Backend

A webhook fires. One HTTP request comes in.

Ten seconds later, half the app is returning 503s.

The bug is not in the webhook.

Read more

A Customer Couldn’t Upload an Invoice. The Fix Re-Taught Me XSD, JAXB, and Maven.

A Polish customer reports their invoice won’t upload.

The file looks valid. Other Polish customers upload fine.

Three days later I’ve read more of the Polish Tax Code than I’d like to admit.

Read more

What Jackson Doesn’t Deserialize for Free: The Empty Array That Took Down a Production Endpoint

A 503. Every 30 seconds, on the dot.

Sentry was empty. The application logs said the request was fine.

Then we looked at what Jackson was actually parsing.

Read more