Back to all articles

Architecting Resilient Microservices with Spring Boot & Docker

A guide to building maintainable, fault-tolerant backend architectures using Spring Cloud Gateway, Eureka service discovery, JWT security, and Docker containers.

Hasin Ishraq
Hasin Ishraq AI / ML & Data Science Enthusiast
Wednesday, December 10, 2025 5 min read
Listen to this article
5:17
0:00

Introduction

Transitioning from a monolithic architecture to a distributed microservices ecosystem introduces operational complexity along with modular scalability. While building our peer-to-peer micro-lending platform FundBridge, we adopted a decoupled architecture to isolate payment gateways, campaign management, user identity, and notification dispatches.

flowchart TD
    Client[Web & Mobile Clients] --> Gateway[Spring Cloud API Gateway :8080]
    Gateway --> Discovery[Eureka Service Discovery :8761]
    Gateway --> Auth[Auth Service :8081\nJWT + Spring Security]
    Gateway --> Campaign[Campaign Service :8082\nPostgreSQL]
    Gateway --> Payment[Payment Service :8083\nTransactional DB]
    Payment -.-> Kafka[Apache Kafka Event Bus]
    Kafka -.-> Notif[Notification Service :8084]

1. Centralized Routing with Spring Cloud Gateway

The API Gateway acts as the single point of entry, enforcing cross-cutting concerns such as rate-limiting, CORS policies, SSL termination, and JWT authentication token validation.

@Configuration
public class GatewayRoutingConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder, JwtAuthFilter jwtFilter) {
        return builder.routes()
            .route("auth-service", r -> r.path("/api/v1/auth/**")
                .uri("lb://AUTH-SERVICE"))
            .route("campaign-service", r -> r.path("/api/v1/campaigns/**")
                .filters(f -> f.filter(jwtFilter.apply(new JwtAuthFilter.Config())))
                .uri("lb://CAMPAIGN-SERVICE"))
            .route("payment-service", r -> r.path("/api/v1/payments/**")
                .filters(f -> f.filter(jwtFilter.apply(new JwtAuthFilter.Config())))
                .uri("lb://PAYMENT-SERVICE"))
            .build();
    }
}

2. Stateless Security with JWT Authentication

In a distributed environment, session replication across service nodes degrades latency. Adopting asymmetric or signed JSON Web Tokens (JWT) allows each downstream microservice to verify claims and roles locally without querying the authentication database on every incoming request.

Token Verification Flow:

  1. Client logs in with credentials via /api/v1/auth/login.
  2. Auth Service issues signed JWT containing user ID, roles, and expiration time.
  3. API Gateway and downstream services decode and validate the token signature using the shared secret or public key.

3. Containerization and Multi-Stage Docker Builds

Optimizing Docker container images minimizes build time and reduces deployment surface area. Using multi-stage Docker builds ensures that heavy build dependencies (like the JDK and Maven plugins) are omitted from the lightweight production JRE runtime image:

# Stage 1: Build the artifact
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests

# Stage 2: Minimal runtime image
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

4. Resilience and Circuit Breaking with Resilience4j

When dependent services experience network partition or slowdown, preventing cascading failure is paramount. Integrating Resilience4j circuit breakers and fallback handlers preserves system stability:

@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processTransaction(PaymentRequest request) {
    return restTemplate.postForObject(paymentServiceUrl, request, PaymentResponse.class);
}

public PaymentResponse paymentFallback(PaymentRequest request, Throwable throwable) {
    // Return gracefully degraded response or queue for retry
    return new PaymentResponse("PENDING_RETRY", "Payment provider temporarily busy.");
}

Summary

Building production microservices requires disciplined adherence to:

  • Stateless security via validated JWTs
  • Automated service registry and dynamic client-side load balancing
  • Optimized container images with multi-stage Docker builds
  • Fault-tolerant circuit breakers to insulate critical services from transient downtime
Enjoyed this article? Share it:
Hasin Ishraq

Written by Hasin Ishraq

Final Year Computer Science Student passionate about Artificial Intelligence, Data Science, and Machine Learning at United International University.