If you are not a Member — Read for free here :

You've Built It — Now It's Time to Scale It

You've explored the internals of Spring Boot, handled async operations, used custom filters, and even spun up Docker containers in your tests.

But real-world software doesn't stop at writing code. It lives in the cloud, runs under heavy load, gets monitored, logged, deployed hundreds of times, and needs to recover from failure.

Welcome to Part 3 of this deep-dive series where we move beyond development and into deploying, scaling, monitoring, and securing Spring Boot applications in production.

If you've ever asked:

  • How do I deploy Spring Boot the right way?
  • What's the best logging strategy in microservices?
  • How do I monitor and alert on failures?
  • How can I optimize performance in real time?

This guide is for you.

1. Spring Boot in the Cloud — Stateless, Scalable, Deployable

Spring Boot was born for the cloud — but you need to prepare it for the cloud properly.

Rule #1: Make Your App Stateless

Avoid storing user sessions or application state in memory. Use:

  • Redis / Memcached for session data
  • Databases or file stores (like S3) for persistent data
  • Spring Cloud Config for dynamic configuration
server.servlet.session.persistent=true
spring.session.store-type=redis

Rule #2: Externalize Everything

Your application should never rely on local .properties in production.

Use:

  • Environment variables
  • Spring Cloud Config
  • Kubernetes ConfigMaps / Secrets

Rule #3: Readiness & Liveness Probes

When deploying on Kubernetes, configure health endpoints:

management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=always

In application.yaml:

management:
  health:
    probes:
      enabled: true

2. CI/CD with Spring Boot — The Automation You Can't Ignore

Manual deployments break things. Let's automate!

Step-by-Step CI/CD with GitHub Actions (Example)

.github/workflows/deploy.yml

name: Build and Deploy Spring Boot

on:
  push:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
      - name: Build with Maven
        run: mvn clean package -DskipTests
      - name: Docker Build
        run: docker build -t myapp:latest .
      - name: Push to DockerHub
        run: |
          docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
          docker tag myapp:latest mydockeruser/myapp:latest
          docker push mydockeruser/myapp:latest

Deploy to:

  • Kubernetes (via Helm/Manifests)
  • AWS ECS / EKS
  • Google Cloud Run / App Engine

3. Observability: Tracing, Metrics & Logs

You can't fix what you can't see.

A. Metrics with Micrometer + Prometheus + Grafana

Spring Boot uses Micrometer under the hood.

management.endpoints.web.exposure.include=*
management.metrics.export.prometheus.enabled=true

Access metrics at /actuator/prometheus.

Visualize with Grafana, and you get dashboards like:

  • JVM memory usage
  • HTTP request latency
  • DB connections
  • Custom business metrics

Custom Metric Example:

@Autowired
MeterRegistry registry;

@PostConstruct
public void init() {
    registry.counter("my_custom_counter").increment();
}

B. Distributed Tracing with Spring Cloud Sleuth + Zipkin

Add dependencies:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

This will:

  • Auto-tag logs with trace & span IDs
  • Send traces to Zipkin or OpenTelemetry backend

C. Centralized Logging with ELK or Loki

Avoid logging to local files. Stream logs to:

  • Elastic Stack (ELK)
  • Grafana Loki
  • AWS CloudWatch Logs

Recommended logging format:

logging.pattern.level=%5p [${spring.application.name:},%X{X-B3-TraceId}]

Or use JSON structured logging for better parsing.

4. Circuit Breakers with Spring Cloud Resilience4j

To prevent cascading failures when external services go down, use Circuit Breakers.

Add dependency:

<dependency>
  <groupId>io.github.resilience4j</groupId>
  <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

Then use:

@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
public String callExternalAPI() {
    return restTemplate.getForObject("/api", String.class);
}

public String fallback(Exception e) {
    return "Default response";
}

You can combine this with:

  • Retries
  • Rate Limiters
  • Bulkheads

Resilience4j dashboard also available via actuator endpoints.

5. Securing APIs: OAuth2 + Rate Limiting + CORS

OAuth2 with Spring Security

Add dependencies:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Configure JWT:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com/

Rate Limiting with Bucket4j or Redis

@Bucket(name = "apiBucket", capacity = 100, refillTokens = 10, refillPeriod = Duration.ofSeconds(1))
public ResponseEntity<?> getProducts() {
   // logic
}

Protects from abusive clients and DDoS-style attacks.

CORS and Security Headers

@Override
protected void configure(HttpSecurity http) {
    http.cors().and().headers()
        .xssProtection().and()
        .frameOptions().deny();
}

6. Optimize Performance for Real-World Traffic

Enable HTTP/2

Just add this in application.properties:

server.http2.enabled=true

Use Connection Pooling with HikariCP

Already default in Spring Boot. But tune it:

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000

Enable Response Compression

server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html

7. Graceful Rollbacks and Feature Toggles (Advanced Deployment Strategy)

Use LaunchDarkly, FF4J, or a home-grown solution to control features at runtime.

if (featureManager.isActive("new_checkout_flow")) {
    return newFlow();
} else {
    return oldFlow();
}

This allows safe releases, A/B testing, and rollback without redeploying.

Read my other articles :

Thank You for Reading!

✅ "Follow me on Medium to never miss an update!"

If you found this content helpful, feel free to show your support with 👏 claps! 😊 Your encouragement keeps me motivated to share more insights