Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min read
Building a Production-Ready Weather API with Spring Boot: A Technical Deep Dive
N

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 (localhost vs redis)

  • 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:

  1. Unit Tests: Testing individual components in isolation

  2. Integration Tests: Testing component interactions

  3. Parameterized Tests: Testing multiple scenarios efficiently

  4. 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:

  1. Docker networking issues → Use service names in container networks

  2. Testing complexity → Use TestContainers for integration testing

  3. Dependency conflicts → Let Spring Boot manage versions

  4. Cache testing challenges → Understand caching behavior thoroughly

  5. 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.