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

Spring Boot Testing Strategy – Context Management & Perf Secrets (Part 2)

Every second saved in test execution multiplies across your entire team and CI/CD pipeline. A 10x improvement in test performance can save hours of developer time daily—transforming a 10-minute test suite into a 1-minute feedback loop.

Read more

Spring Boot Testing Strategy – Ultimate Cheatsheet 2025 (Part 1)

Most Spring Boot testing articles bury you in theory. This one is built for action: a battle-tested reference you can keep open while coding.

Read more