Logging and Exception Handling in Spring Boot: Best Practices for Microservices

Page content

In distributed systems, logging and exception handling are not just debugging tools β€” they are core parts of system reliability and observability.

Poor logging makes production issues hard to diagnose, while improper exception handling often leads to hidden failures, inconsistent states, and frustrated users.

In this article, we’ll go through practical Spring Boot–based best practices for:

  • What to log at each log level
  • How to structure logs in microservices
  • How to handle exceptions cleanly
  • Common mistakes to avoid

Why Logging Matters More in Microservices

In monolithic applications, a stack trace is often enough.

In microservices:

  • A single request may travel through 5–10 services
  • Failures happen across network boundaries
  • You rarely have a full picture in one place

Good logging helps you:

βœ… Trace requests across services
βœ… Understand failures quickly
βœ… Monitor system health
βœ… Support distributed debugging


Log Levels and What to Log

πŸ”΅ DEBUG – Technical Details for Troubleshooting

Use DEBUG for:

  • SQL queries (if needed)
  • Payloads (carefully!)
  • Internal decision logic

Never enable DEBUG in production by default.


🟒 INFO – Business Flow & Important Events

Use INFO to describe normal application behavior.

Good examples:

  • Service startup and shutdown
  • Important business actions
  • External system calls (success)
log.info("Order {} successfully created for user {}", orderId, userId);

Avoid:

❌ Logging every method call
❌ Logging internal loops


🟑 WARN – Unexpected but Recoverable Situations

Use WARN when something is not ideal but the system can continue.

Examples:

  • Fallback triggered
  • Invalid input handled gracefully
  • External service timeout with retry
log.warn("Payment service timeout, retrying for order {}", orderId);

πŸ”΄ ERROR – Failures That Break the Operation

Use ERROR when:

  • A request cannot be processed
  • Data consistency might be affected
  • An unexpected exception occurs
log.error("Failed to process order {}", orderId, exception);

Always include:

βœ” meaningful context
βœ” exception stack trace


Avoid These Common Logging Mistakes

❌ Logging sensitive data (passwords, tokens, personal info)
❌ Logging huge payloads
❌ Using only ERROR level
❌ Logging without context

Bad:

log.error("Something went wrong");

Good:

log.error("Failed to create order {} for user {}", orderId, userId, exception);

Avoid Unnecessary Logging Overhead

Logging is not free β€” it consumes CPU time, allocates memory, and may trigger expensive string formatting even if the log level is disabled. To reduce unnecessary resource usage, it is a good practice to guard log statements with level checks such as isDebugEnabled() or isWarnEnabled() when the log message construction is non-trivial. This ensures that costly operations are only executed when the log level is actually active.

if (logger.isDebugEnabled()) {
    logger.debug(
        "Calculated price for order {} with items {} and discounts {}",
        orderId,
        calculateItems(order),
        calculateDiscounts(order)
    );
}

Without the isDebugEnabled() check, the helper methods may still be executed, wasting resources even though the debug log is never written.


Centralized Logging and Correlation IDs

In microservices, logs should be:

  • Centralized (ELK, OpenSearch, etc.)
  • Correlated per request

Example: Adding a Correlation ID with MDC

@Component
public class CorrelationIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws IOException, ServletException {

        String correlationId = UUID.randomUUID().toString();
        MDC.put("correlationId", correlationId);

        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

Then configure logging pattern:

logging.pattern.level=%5p [${correlationId}]

This allows tracing a single request across services.


Clean Exception Handling with @ControllerAdvice

Instead of handling exceptions everywhere:

Use centralized exception handling.

Example:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex) {
        log.warn("Resource not found: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneric(Exception ex) {
        log.error("Unexpected error occurred", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body("Internal server error");
    }
}

Benefits:

βœ… Consistent error responses
βœ… Clean controllers
βœ… Centralized logging


Don’t Mix Logging and Control Flow

A common anti-pattern:

try {
    service.process();
} catch (Exception e) {
    log.error("Error occurred", e);
}

And then continuing as if nothing happened.

Instead:

  • Log
  • Then rethrow or handle properly
catch (Exception e) {
    log.error("Failed to process order {}", orderId, e);
    throw e;
}

Logging vs Throwing Exceptions

A good rule of thumb:

πŸ‘‰ Log where you have the most context
πŸ‘‰ Throw exceptions upward for proper handling

Avoid logging the same exception multiple times across layers.


Microservices-Specific Best Practices

βœ” Use structured logs (JSON if possible)
βœ” Always include request identifiers
βœ” Log external calls and failures
βœ” Monitor error rates


Final Thoughts

Good logging and exception handling:

  • Improve system reliability
  • Reduce debugging time
  • Make distributed systems manageable

In microservices, they are not optional β€” they are fundamental architecture elements.

In upcoming articles, we’ll dive deeper into:

  • Observability tools (tracing, metrics)
  • Event-driven error handling
  • Resilience patterns

Key Takeaways:

  • Use log levels properly
  • Add meaningful context
  • Centralize logs
  • Handle exceptions consistently
  • Design logging with distributed systems in mind