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:
- Client logs in with credentials via
/api/v1/auth/login. - Auth Service issues signed JWT containing user ID, roles, and expiration time.
- 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