Javadoc Best Practices for Spring Boot - 5-Minute Professional Guide 2025
Table of Contents
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.
4. @see - Link Related Code
/**
* 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>}
{@link} - For Cross-References
/**
* 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.,
existsvsfind)
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&@- 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:
- Type
jdocabove any method - Press
Tab - Fill in the placeholders
- Press
Tabto 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:
- Place cursor on method
- Press
Alt+Insert(Windows/Linux) orCmd+N(Mac) - Select “Generate JavaDoc”
- IntelliJ creates basic structure from signature
Fix Javadoc warnings:
- Press
Alt+Enteron warning - IntelliJ suggests fixes (missing @param, @return, etc.)
Format Javadoc:
- Select Javadoc comment
- Press
Ctrl+Alt+L(Windows/Linux) orCmd+Option+L(Mac) - 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
- Focus on the “Why” and “What” - Don’t document how (that’s what code is for)
- Use the 80/20 Rule - Master 5 essential tags (
@param,@return,@throws,@see,@deprecated) - Make It Scannable - Use
<ul>,<ol>,<p>, and<b>for structure - Leverage {@code} and {@link} - For examples and navigation
- 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:javadocto 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
JavadocStylechecks - 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
Resources & Links
Essential Documentation
- Oracle Javadoc Guide - Official reference
- Google Java Style Guide - Javadoc - Industry best practices
- IntelliJ IDEA Live Templates - Template creation guide
Companion Content
Last updated: November 2025 | Java 21+ | Spring Boot 3.x compatible
Happy Documenting! 🚀


