Logging and Exception Handling in Spring Boot: Best Practices for Microservices
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