Lesson 17: Spring Boot Production Monitoring and Caching
Master Spring Boot Actuator for production monitoring and implement intelligent caching strategies to build high-performance, observable applications ready for enterprise environments.
Introduction
Building production-ready applications requires two critical capabilities: comprehensive observability to understand how your application is performing, and intelligent caching to ensure it performs well under load. Spring Boot Actuator provides production-ready monitoring features that give you deep insights into application health, metrics, and operational status, while Spring's caching abstraction offers powerful performance optimization through intelligent data storage and retrieval. Think of monitoring as your application's vital signs monitor and caching as its short-term memory - monitoring tells you what's happening and whether everything is healthy, while caching helps your application respond faster by remembering frequently needed information. Together, these capabilities enable you to build applications that not only perform exceptionally well but also provide the observability needed to maintain that performance in production environments. This lesson teaches you to implement both comprehensive monitoring and effective caching strategies that work together to create resilient, high-performance applications.
Understanding Actuator
Definition
Spring Boot Actuator is a set of production-ready features that help you monitor and manage your application when it's running in production. It provides built-in endpoints for health checks, application metrics, environment details, and operational information. Actuator endpoints are automatically secured and can be customized or extended to meet specific monitoring requirements. These features give operations teams and developers the visibility they need to maintain healthy, performant applications in production environments.
Analogy
Think of Spring Boot Actuator like the comprehensive dashboard and diagnostic system in a modern car. Just as your car's dashboard shows you essential information like speed, fuel level, engine temperature, and oil pressure, Actuator provides a dashboard view of your application's vital signs. The car's diagnostic port allows mechanics to connect specialized tools to get detailed information about engine performance, sensor readings, and error codes - similarly, Actuator endpoints allow monitoring tools and operations teams to connect and get detailed insights into your application's performance, health status, and internal metrics.
Examples
Adding Actuator dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Basic Actuator configuration:
management.endpoints.web.exposure.include=health,info,metrics,caches
management.endpoint.health.show-details=always
management.endpoints.web.base-path=/actuator
Accessing health endpoint:
# GET /actuator/health
{
"status": "UP",
"components": {
"db": {"status": "UP"},
"diskSpace": {"status": "UP"},
"cacheManager": {"status": "UP"}
}
}
Health Checks
Definition
Health checks provide a way to verify that your application and its dependencies are functioning correctly. Spring Boot Actuator includes built-in health indicators for databases, disk space, and other common components, and you can create custom health indicators for business-specific dependencies. Health checks return a status (UP, DOWN, OUT_OF_SERVICE) along with detailed information about what's being checked. Load balancers and monitoring systems use these endpoints to determine if an instance should receive traffic or needs attention.
Examples
Custom health indicator for cache:
@Component
public class CacheHealthIndicator implements HealthIndicator {
@Autowired
private CacheManager cacheManager;
public Health health() {
try {
Cache userCache = cacheManager.getCache("users");
if (userCache != null) {
return Health.up()
.withDetail("cache", "users")
.withDetail("status", "available")
.build();
}
return Health.down().withDetail("cache", "unavailable").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Database and cache health check:
@Component
public class ApplicationHealthIndicator implements HealthIndicator {
public Health health() {
boolean dbHealth = checkDatabaseConnection();
boolean cacheHealth = checkCacheAvailability();
if (dbHealth && cacheHealth) {
return Health.up()
.withDetail("database", "connected")
.withDetail("cache", "available")
.build();
}
return Health.down()
.withDetail("database", dbHealth ? "connected" : "disconnected")
.withDetail("cache", cacheHealth ? "available" : "unavailable")
.build();
}
}
Application Info
Definition
The info endpoint provides metadata about your application such as version numbers, build information, Git commit details, and custom application properties. This information helps operations teams understand exactly which version is deployed, when it was built, and what features it contains. You can customize the info endpoint to include cache configuration details, monitoring settings, and other operational information.
Examples
Application info with cache details:
info.app.name=E-commerce Platform
info.app.description=Product catalog with caching and monitoring
info.app.version=@project.version@
info.features.caching=enabled
info.features.monitoring=enabled
Custom info contributor with cache information:
@Component
public class CacheInfoContributor implements InfoContributor {
@Autowired
private CacheManager cacheManager;
public void contribute(Info.Builder builder) {
Collection cacheNames = cacheManager.getCacheNames();
builder.withDetail("caching", Map.of(
"provider", cacheManager.getClass().getSimpleName(),
"caches", cacheNames,
"count", cacheNames.size()
));
}
}
Metrics Monitoring
Definition
Spring Boot Actuator provides comprehensive metrics about your application's performance, including JVM statistics, HTTP request metrics, database connection pool stats, and cache performance metrics. Built on Micrometer, it supports various monitoring systems like Prometheus, Grafana, and New Relic. Metrics help you understand application behavior over time, identify performance bottlenecks, track cache effectiveness, and set up alerting for abnormal conditions.
Examples
Cache metrics monitoring:
@Component
public class CacheMetricsCollector {
private final MeterRegistry meterRegistry;
private final CacheManager cacheManager;
public CacheMetricsCollector(MeterRegistry meterRegistry, CacheManager cacheManager) {
this.meterRegistry = meterRegistry;
this.cacheManager = cacheManager;
// Register cache size gauges
cacheManager.getCacheNames().forEach(cacheName -> {
Gauge.builder("cache.size")
.tag("cache", cacheName)
.register(meterRegistry, this, metrics -> getCacheSize(cacheName));
});
}
private double getCacheSize(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
return cache != null ? cache.getNativeCache().size() : 0;
}
}
Custom business metrics with caching:
@Service
public class ProductService {
private final Counter cacheHits;
private final Counter cacheMisses;
private final Timer queryTimer;
public ProductService(MeterRegistry meterRegistry) {
this.cacheHits = Counter.builder("cache.hits").tag("cache", "products").register(meterRegistry);
this.cacheMisses = Counter.builder("cache.misses").tag("cache", "products").register(meterRegistry);
this.queryTimer = Timer.builder("products.query.time").register(meterRegistry);
}
@Cacheable("products")
public Product findProduct(Long id) {
Timer.Sample sample = Timer.start();
try {
return productRepository.findById(id);
} finally {
sample.stop(queryTimer);
}
}
}
Custom Endpoints
Definition
Custom Actuator endpoints allow you to expose application-specific operational information and controls. You can create endpoints for cache management, performance diagnostics, or business metrics. Custom endpoints follow the same security and exposure patterns as built-in endpoints, ensuring consistent access control and discoverability.
Examples
Cache management endpoint:
@Endpoint(id = "cache-management")
@Component
public class CacheManagementEndpoint {
@Autowired
private CacheManager cacheManager;
@ReadOperation
public Map getCacheInfo() {
Map cacheInfo = new HashMap<>();
cacheManager.getCacheNames().forEach(name -> {
Cache cache = cacheManager.getCache(name);
cacheInfo.put(name, Map.of(
"size", cache.getNativeCache().size(),
"type", cache.getClass().getSimpleName()
));
});
return cacheInfo;
}
@WriteOperation
public void clearCache(@Selector String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
}
Performance monitoring endpoint:
@WebEndpoint(id = "performance")
@Component
public class PerformanceEndpoint {
@ReadOperation
public WebEndpointResponse
Understanding Caching
Definition
Caching is a performance optimization technique that stores copies of frequently accessed data in fast-access storage to avoid repeated expensive operations. When data is requested, the cache is checked first; if found (cache hit), the cached data is returned immediately. If not found (cache miss), the expensive operation is performed, and the result is stored in the cache for future requests. Effective caching can reduce database load, improve response times, and enhance overall application scalability.
Analogy
Think of caching like the strategy a busy chef uses in a restaurant kitchen. Instead of going to the main storage room every time they need common ingredients like salt, pepper, or olive oil, the chef keeps frequently used items on the counter within arm's reach. When an order comes in for a dish that needs salt, the chef grabs it from the counter (cache hit) rather than walking to the storage room (database query). Occasionally, an ingredient runs out and the chef must restock from the main storage (cache miss), but this happens much less frequently than if they fetched every ingredient from storage for every dish.
Examples
Without caching - repeated database queries:
public User getUser(Long id) {
return userRepository.findById(id); // Database query every time
}
With caching - store results in memory:
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id); // Query once, cache result
}
Cache performance comparison:
// Without cache: 200ms database query every time
// With cache: 200ms first time, 2ms subsequent requests
// 100x performance improvement for cached data!
Spring Cache Abstraction
Definition
Spring's cache abstraction provides a unified programming model for caching that works with different cache providers without changing your code. The abstraction uses annotations to declare caching behavior, automatically handling cache operations like storing, retrieving, and evicting data. You can switch between cache providers (from simple HashMap to Redis clusters) by changing configuration, not code.
Examples
Enable caching in Spring Boot:
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Cache configuration with monitoring:
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("users", "products");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()); // Enable statistics for monitoring
return manager;
}
}
Cache Annotations
Definition
Spring provides several cache annotations that declaratively define caching behavior. @Cacheable caches method results, @CacheEvict removes cached data, @CachePut updates cache entries, and @Caching combines multiple cache operations. These annotations use SpEL (Spring Expression Language) for dynamic cache keys and conditions.
Examples
@Cacheable - cache method results:
@Cacheable(value = "users", key = "#id")
public User findUser(Long id) {
return userRepository.findById(id);
}
@CacheEvict - remove cached data:
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
@CachePut - update cache:
@CachePut(value = "users", key = "#result.id")
public User createUser(User user) {
return userRepository.save(user);
}
Conditional caching with monitoring:
@Cacheable(value = "products", condition = "#result != null", unless = "#result.price > 1000")
public Product findProduct(Long id) {
Product product = productRepository.findById(id);
// Log cache operation for monitoring
logger.info("Product {} cached: {}", id, product != null);
return product;
}
Cache Providers
Definition
Cache providers are the underlying storage mechanisms that actually hold cached data. Spring Boot supports various providers: ConcurrentHashMap for simple in-memory caching, Caffeine for high-performance local caching with advanced features, Redis for distributed caching across multiple servers, and Hazelcast for in-memory data grids. Each provider has different characteristics regarding performance, memory management, persistence, and distribution capabilities.
Examples
Caffeine high-performance cache with monitoring:
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats() // Enable statistics
.removalListener((key, value, cause) -> {
logger.info("Cache entry removed: key={}, cause={}", key, cause);
}));
return manager;
}
Redis distributed cache configuration:
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware() // Support for monitoring transactions
.build();
}
Cache Configuration
Definition
Cache configuration defines how caches behave regarding size limits, expiration policies, eviction strategies, and serialization. Configuration includes setting maximum cache sizes, time-based expiration (TTL), access-based expiration, and cache warming strategies. Proper configuration ensures optimal memory usage, prevents cache growth from consuming all available memory, and maintains data freshness appropriate for your application's requirements.
Examples
Environment-specific cache configuration:
# application-dev.properties
spring.cache.caffeine.spec=maximumSize=100,expireAfterWrite=5m
# application-prod.properties
spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=30m
management.metrics.cache.instrument=true
Multiple cache configurations:
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
// Short-lived user sessions
manager.registerCustomCache("user-sessions",
Caffeine.newBuilder()
.expireAfterAccess(30, TimeUnit.MINUTES)
.recordStats()
.build());
// Long-lived reference data
manager.registerCustomCache("reference-data",
Caffeine.newBuilder()
.expireAfterWrite(4, TimeUnit.HOURS)
.maximumSize(5000)
.recordStats()
.build());
return manager;
}
Monitoring & Caching Integration
Definition
Integrating monitoring with caching provides comprehensive visibility into cache performance, helping optimize cache strategies and troubleshoot performance issues. This includes tracking cache hit rates, monitoring eviction patterns, measuring cache-related response times, and alerting on cache health issues. Proper integration ensures that caching improvements are measurable and cache-related problems are quickly identified.
Examples
Cache metrics endpoint:
@RestController
public class CacheMonitoringController {
@Autowired
private CacheManager cacheManager;
@GetMapping("/actuator/cache-stats")
public Map getCacheStatistics() {
Map stats = new HashMap<>();
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
com.github.benmanes.caffeine.cache.stats.CacheStats cacheStats =
((com.github.benmanes.caffeine.cache.Cache, ?>) cache.getNativeCache()).stats();
stats.put(cacheName, Map.of(
"hitRate", cacheStats.hitRate(),
"missCount", cacheStats.missCount(),
"evictionCount", cacheStats.evictionCount(),
"size", ((com.github.benmanes.caffeine.cache.Cache, ?>) cache.getNativeCache()).estimatedSize()
));
}
});
return stats;
}
}
Cache health indicator:
@Component
public class CacheHealthIndicator implements HealthIndicator {
@Autowired
private CacheManager cacheManager;
@Override
public Health health() {
Health.Builder builder = Health.up();
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
try {
// Test cache operation
cache.get("health-check", () -> "OK");
builder.withDetail(cacheName, "healthy");
} catch (Exception e) {
builder.down().withDetail(cacheName, "unhealthy: " + e.getMessage());
}
});
return builder.build();
}
}
Cache warming with monitoring:
@Component
public class CacheWarmupService {
private final Logger logger = LoggerFactory.getLogger(CacheWarmupService.class);
private final MeterRegistry meterRegistry;
@EventListener(ApplicationReadyEvent.class)
public void warmupCaches() {
Timer.Sample sample = Timer.start(meterRegistry);
try {
// Warm up popular products
List popularProducts = productRepository.findTop100ByOrderBySalesDesc();
popularProducts.forEach(product ->
cacheManager.getCache("products").put(product.getId(), product));
logger.info("Cache warmed with {} products", popularProducts.size());
meterRegistry.counter("cache.warmup.success").increment();
} catch (Exception e) {
logger.error("Cache warmup failed", e);
meterRegistry.counter("cache.warmup.failure").increment();
} finally {
sample.stop(Timer.builder("cache.warmup.duration").register(meterRegistry));
}
}
}
Production Best Practices
Definition
Production best practices for monitoring and caching involve securing endpoints, optimizing cache strategies, implementing proper alerting, and ensuring observability doesn't impact performance. This includes protecting sensitive monitoring endpoints, choosing appropriate cache eviction policies, setting up meaningful alerts, and monitoring the monitoring systems themselves to ensure reliability.
Examples
Production monitoring security:
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
return http.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health")).permitAll()
.requestMatchers(EndpointRequest.to("cache-stats")).hasRole("ADMIN")
.anyRequest().hasRole("ACTUATOR"))
.build();
}
}
Cache monitoring alerts:
@Component
public class CacheAlertingService {
private final MeterRegistry meterRegistry;
@EventListener
@Async
public void onCacheEviction(CacheEvictEvent event) {
meterRegistry.counter("cache.evictions", "cache", event.getCacheName()).increment();
// Alert if eviction rate is too high
if (isEvictionRateHigh(event.getCacheName())) {
alertingService.sendAlert(
"High cache eviction rate detected for cache: " + event.getCacheName()
);
}
}
@Scheduled(fixedRate = 60000) // Every minute
public void checkCacheHealth() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
double hitRate = getCacheHitRate(cache);
if (hitRate < 0.8) { // Alert if hit rate drops below 80%
alertingService.sendAlert(
String.format("Low cache hit rate (%.2f) for cache: %s", hitRate, cacheName)
);
}
});
}
}
Production cache configuration:
# application-prod.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,cache-stats
endpoint:
health:
show-details: when-authorized
metrics:
cache:
instrument: true
export:
prometheus:
enabled: true
spring:
cache:
caffeine:
spec: maximumSize=50000,expireAfterWrite=1h,recordStats=true
Summary
You've now mastered both Spring Boot Actuator monitoring and caching strategies, creating a powerful combination for building high-performance, observable applications. Actuator provides the comprehensive monitoring capabilities needed to understand application health and performance, while Spring's caching abstraction offers the tools to optimize that performance through intelligent data storage and retrieval. The integration of monitoring and caching creates a virtuous cycle: monitoring helps you identify what to cache and how well your caching strategies are working, while caching improves the performance metrics you're monitoring. You've learned to implement health checks, track meaningful metrics, create custom endpoints, configure various cache providers, and follow production best practices that ensure both performance and observability. These skills enable you to build applications that not only perform exceptionally well but also provide the visibility needed to maintain and improve that performance over time. Next, you'll explore comprehensive performance optimization techniques that build upon these monitoring and caching foundations.
Programming Challenge
Challenge: Comprehensive Monitoring and Caching E-commerce Platform
Task: Build a Spring Boot e-commerce application with comprehensive monitoring capabilities and intelligent multi-level caching that demonstrates the integration of observability and performance optimization.
Requirements:
- Create core entities and services:
Product
,Category
,Order
,User
entities- Service layer with business logic for product catalog and order processing
- REST endpoints for product search, user management, and order operations
- Implement comprehensive monitoring:
- Custom health indicators for database, cache, and external services
- Business metrics tracking (orders per minute, popular products, revenue)
- Custom Actuator endpoints for business dashboard and cache management
- Application events monitoring for startup, shutdown, and critical operations
- Design intelligent caching strategy:
- L1 Cache: Individual products (1-hour TTL, high hit rate expected)
- L2 Cache: Product categories and search results (30-min TTL)
- L3 Cache: User profiles and preferences (4-hour TTL)
- L4 Cache: Popular products with cache warming at startup
- Integrate monitoring with caching:
- Cache hit/miss rate monitoring and alerting
- Cache size and eviction pattern tracking
- Performance comparison metrics (with/without cache)
- Cache health indicators and automatic failover strategies
- Implement production features:
- Structured logging with correlation IDs for request tracing
- Security configuration for monitoring endpoints
- Environment-specific cache and monitoring configurations
- Graceful degradation when cache is unavailable
- Add management and operations capabilities:
- Administrative endpoints for cache clearing and statistics
- Scheduled cache refresh for critical data
- Performance alerting based on response times and error rates
- Prometheus metrics export for external monitoring systems
Bonus features:
- Distributed caching with Redis and monitoring across multiple instances
- Cache warming based on user behavior patterns and popular search terms
- Dynamic cache configuration adjustment based on monitoring data
- Circuit breaker pattern with cache fallback and monitoring integration
- Custom cache eviction listeners with monitoring and alerting
- A/B testing framework for cache strategies with performance monitoring
Learning Goals: Practice integrating comprehensive monitoring with intelligent caching strategies, implementing production-ready observability features, creating meaningful business metrics, and building systems that provide both excellent performance and operational visibility for enterprise environments.