Building a Production-Ready Weather API with Spring Boot: A Technical Deep Dive

I am a developer from....
Introduction
In this technical deep dive, I'll walk you through building a robust, production-ready weather API using Spring Boot. This project demonstrates modern Java development practices, microservices architecture, caching strategies, comprehensive testing, and containerization.
Project Overview
Our weather application is a RESTful API that fetches weather data from external APIs, implements intelligent caching, and provides a seamless developer experience. Here's what makes this project stand out:
Spring Boot 3.x with Java 21
Redis caching for performance optimization
Docker containerization for consistent deployment
Comprehensive testing with TestContainers
Exception handling and error management
Production-ready configuration management
Technical Architecture
1. Layered Architecture Design
We implemented a clean, layered architecture following Spring Boot best practices:
Controller Layer (REST API)
↓
Service Layer (Business Logic)
↓
Client Layer (External API Integration)
↓
Model Layer (Data Transfer Objects)
Key Benefits:
Separation of Concerns: Each layer has a specific responsibility
Testability: Easy to unit test each layer independently
Maintainability: Changes in one layer don't affect others
Scalability: Can easily add new features or modify existing ones
2. Spring Boot Configuration Management
We implemented environment-specific configuration using Spring Boot's profile system:
# application.properties (default)
spring.data.redis.host=redis
spring.data.redis.port=6379
spring.data.redis.client-type=jedis
# application-docker.properties
spring.data.redis.host=redis
spring.data.redis.port=6379
spring.data.redis.client-type=jedis
spring.redis.timeout=60000
# application-local.properties
spring.data.redis.host=localhost
spring.data.redis.port=6379
This approach allows seamless deployment across different environments without code changes.
3. Redis Caching Strategy
We implemented a sophisticated caching strategy using Redis:
@Cacheable(value = "weather", key = "#cityName")
public WeatherResponse getWeatherData(String cityName) {
// Fetch from external API
return weatherApiClient.getWeatherData(cityName);
}
Caching Benefits:
Performance: Reduces API calls to external services
Cost Optimization: Minimizes external API usage
Reliability: Provides fallback data during external API outages
Scalability: Reduces load on external services
Problems We Faced and Solutions Implemented
Problem 1: Redis Connection Issues in Docker
Issue: When containerizing the application, Redis connections failed with connection refused errors.
Root Cause:
Incorrect Redis host configuration (
localhostvsredis)Missing Redis client type configuration
Serialization issues with Spring Boot 3.x
Solution:
# application-docker.properties
spring.data.redis.host=redis
spring.data.redis.port=6379
spring.data.redis.client-type=jedis
spring.redis.timeout=60000
Key Learning: Always use service names in Docker Compose networks, not localhost.
Problem 2: Comprehensive Testing Strategy
Issue: Needed to test all aspects of the application including caching, error handling, and edge cases.
Solution: Implemented a multi-layered testing approach:
Unit Tests: Testing individual components in isolation
Integration Tests: Testing component interactions
Parameterized Tests: Testing multiple scenarios efficiently
TestContainers: Using real Redis for integration tests
@ParameterizedTest
@ValueSource(strings = {"London", "New York", "Tokyo", "", " ", "A" * 100})
void testWeatherDataWithVariousCityNames(String cityName) {
// Test implementation
}
Problem 3: Test Environment Setup
Issue: Tests required external Redis instance, making CI/CD complex and tests non-portable.
Solution: Implemented TestContainers for self-contained testing:
@Testcontainers
class WeatherServiceTest {
@Container
@SuppressWarnings("resource")
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", redis::getFirstMappedPort);
}
}
Benefits:
Tests run anywhere (CI/CD, local development)
No external dependencies
Consistent test environment
Fast test execution
Problem 4: JUnit Platform Version Conflicts
Issue: NoSuchMethodError due to version mismatches between JUnit Platform and Maven Surefire plugin.
Solution: Let Spring Boot manage dependency versions:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Removed explicit JUnit Platform dependencies to avoid conflicts.
Problem 5: Cache Behavior Testing
Issue: Testing cache behavior required understanding of Spring's caching mechanism and timing.
Solution: Implemented proper cache testing with clear expectations:
@Test
void testCachingBehavior() {
// First call - should hit external API
WeatherResponse response1 = weatherService.getWeatherData("London");
assertEquals("api", response1.getSource());
// Second call - should hit cache
WeatherResponse response2 = weatherService.getWeatherData("London");
assertEquals("cache", response2.getSource());
}
Advanced Technical Features
1. Exception Handling Strategy
We implemented a comprehensive exception handling strategy:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleCityNotFound(CityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("City not found", ex.getMessage()));
}
@ExceptionHandler(WeatherApiException.class)
public ResponseEntity<ErrorResponse> handleWeatherApiError(WeatherApiException ex) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("Weather service unavailable", ex.getMessage()));
}
}
Benefits:
Consistent error responses
Proper HTTP status codes
Detailed error information for debugging
Graceful degradation
2. External API Integration
We implemented robust external API integration with proper error handling:
@Component
public class WeatherApiClient {
public WeatherResponse getWeatherData(String cityName) {
try {
String url = buildWeatherApiUrl(cityName);
ResponseEntity<WeatherResponse> response = restTemplate.getForEntity(url, WeatherResponse.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
response.getBody().setSource("api");
return response.getBody();
}
throw new WeatherApiException("Invalid response from weather API");
} catch (HttpClientErrorException.NotFound e) {
throw new CityNotFoundException("City not found: " + cityName);
} catch (Exception e) {
throw new WeatherApiException("Error fetching weather data: " + e.getMessage());
}
}
}
3. Docker Containerization
We implemented multi-stage Docker builds for optimal image size:
FROM openjdk:21-jdk-slim as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
FROM openjdk:21-jre-slim
COPY --from=build /workspace/app/target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Benefits:
Smaller production images
Faster deployment
Better security (minimal attack surface)
4. Docker Compose Orchestration
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- redis
environment:
- SPRING_PROFILES_ACTIVE=docker
networks:
- weather-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- weather-network
networks:
weather-network:
driver: bridge
Performance Optimizations
1. Redis Configuration
# Docker environment
spring.data.redis.host=redis
spring.data.redis.port=6379
spring.data.redis.client-type=jedis
spring.redis.timeout=60000
# Local environment
spring.data.redis.host=localhost
spring.data.redis.port=6379
2. Caching Strategy
Cache Key Strategy: Using city name as cache key
Cache Eviction: Spring Boot's default cache eviction
Cache Source Tracking: Tracking whether data comes from cache or API
3. API Response Optimization
Timeout Configuration: Proper timeout settings for external API calls
Error Handling: Robust error handling for external API failures
Response Caching: Intelligent caching to reduce external API calls
Security Considerations
1. API Key Management
External API keys stored in environment variables
No hardcoded secrets in code
Secure key rotation process
2. Input Validation
public WeatherResponse getWeatherData(String cityName) {
if (cityName == null || cityName.trim().isEmpty()) {
throw new IllegalArgumentException("City name cannot be null or empty");
}
// Implementation
}
3. Error Information Disclosure
Generic error messages for production
Detailed error messages for development
No sensitive information in error responses
Testing Strategy
1. Test Categories
Unit Tests: Individual component testing
Integration Tests: Component interaction testing
Parameterized Tests: Multiple scenario testing
Cache Tests: Caching behavior verification
2. Test Coverage
Normal Cases: Standard functionality
Edge Cases: Empty strings, null values, special characters
Error Cases: API failures, network issues
Concurrency: Multiple simultaneous requests
3. Test Data Management
TestContainers: Real Redis for integration tests
Mock Data: Consistent test data
Cleanup: Proper test isolation
Deployment Strategy
1. Environment Configuration
Development: Local Redis, detailed logging
Docker: Containerized Redis, optimized for containers
Production: Production Redis, minimal logging
2. Health Checks
Spring Boot provides built-in health check endpoints through Spring Boot Actuator. To enable health checks, add the actuator dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Then configure the health endpoint in application.properties:
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=always
The health endpoint will be available at /actuator/health and provides information about application status, Redis connectivity, and other dependencies.
3. Monitoring and Logging
Spring Boot Actuator: Built-in monitoring and health checks
Application Metrics: Spring Boot's default metrics
Health Checks: Kubernetes-ready health endpoints via Actuator
Key Learnings and Best Practices
1. Configuration Management
Use Spring profiles for environment-specific configuration
Externalize configuration from code
Use environment variables for sensitive data
2. Caching Strategy
Cache at the service layer, not controller layer
Use meaningful cache keys
Implement proper cache eviction strategies
3. Testing Approach
Test behavior, not implementation
Use real dependencies when possible
Implement comprehensive error case testing
4. Containerization
Use multi-stage builds for smaller images
Implement proper health checks
Use service discovery in container networks
5. Error Handling
Implement global exception handling
Use appropriate HTTP status codes
Provide meaningful error messages
Conclusion
This weather API project demonstrates modern Spring Boot development practices and showcases essential skills for backend developers. The project covers:
Architecture Design: Clean, layered architecture
Performance Optimization: Redis caching and timeout configuration
Testing Strategy: Comprehensive testing with TestContainers
Containerization: Docker and Docker Compose
Error Handling: Robust exception management
Configuration Management: Environment-specific configuration
The problems we faced and solved provide valuable lessons for real-world development:
Docker networking issues → Use service names in container networks
Testing complexity → Use TestContainers for integration testing
Dependency conflicts → Let Spring Boot manage versions
Cache testing challenges → Understand caching behavior thoroughly
Environment configuration → Use Spring profiles effectively
This project has been a fulfilling learning journey. I started with the goal of building something simple, but ended up solving some real-world problems that gave me deeper insights into backend development, caching, containerization, and testing.
Resources
📚 Full documentation and setup instructions are available in the /docs folder of the GitLab repository.



