Skip to content

Scalable Java Full Stack Architecture & Resilient API Design

1. Backend API: Intermittent Failures and Inconsistent JSON

Section titled “1. Backend API: Intermittent Failures and Inconsistent JSON”

A backend API starts returning intermittent failures and inconsistent JSON structures.

How would you design:

  • Error handling
  • Retries
  • Fallbacks
  • Circuit breakers
  • User notifications

What information should be logged on both frontend and backend?


For a Java Spring Boot + React full-stack application, handle intermittent API failures using a combination of:

  • Standardized error responses
  • Controlled retries
  • Circuit breakers
  • Business-appropriate fallbacks
  • Centralized user notifications
  • Structured logging
  • Correlation/trace IDs

The most important principle is to make failures predictable and observable rather than allowing every service to return a different error format.


The backend should expose a consistent error contract even when the underlying exception differs.

Example:

{
"timestamp": "2026-08-11T21:20:30Z",
"status": 503,
"code": "CUSTOMER_SERVICE_UNAVAILABLE",
"message": "Customer service is temporarily unavailable",
"traceId": "abc-123"
}

In Spring Boot, use:

  • @RestControllerAdvice
  • @ExceptionHandler
  • Custom business exceptions
  • Standard HTTP status codes
  • Validation error handling
  • A common ErrorResponse DTO

Example:

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(
"RESOURCE_NOT_FOUND",
ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"INTERNAL_ERROR",
"Something went wrong"));
}
}

The React application should not have to understand dozens of backend exception formats.

Instead:

Backend exceptions
|
v
Global Exception Handler
|
v
Standard ErrorResponse
|
v
React

If a downstream API returns inconsistent JSON, don’t expose that structure directly to React.

Use:

  1. Response validation
  2. Mapping to stable DTOs
  3. Schema validation where appropriate
  4. Controlled handling of malformed responses
flowchart LR
    A[React] --> B[API Gateway]
    B --> C[Spring Boot Service]
    C --> D[Downstream API]
    D --> E{Valid Response?}
    E -->|Yes| F[Map to Stable DTO]
    E -->|No| G[Validation Error]
    F --> H[Standard API Response]
    G --> I[Retry / Fallback / Error]
    I --> H
    H --> A

This prevents a downstream API’s inconsistent contract from becoming a frontend problem.


Do not retry every failure.

Usually transient failures such as:

  • Network timeout
  • Connection reset
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • Business validation failures

Use:

  • Limited retry count, typically 2–3 attempts
  • Exponential backoff
  • Jitter
  • Timeouts
  • Retry only appropriate/idempotent operations

Example:

sequenceDiagram
    participant C as Client Service
    participant D as Downstream API

    C->>D: Attempt 1
    D-->>C: Timeout
    Note over C: Wait 200 ms

    C->>D: Attempt 2
    D-->>C: 503
    Note over C: Wait 500 ms

    C->>D: Attempt 3
    D-->>C: Failure
    Note over C: Fallback / Error

For non-idempotent operations such as payment or order creation, retries can create duplicate operations.

Use idempotency keys when retrying such operations.


Retries alone are not enough.

If a downstream service is continuously failing, repeated retries can create a retry storm and make the outage worse.

Use a Circuit Breaker, for example with Resilience4j.

stateDiagram-v2
    [*] --> CLOSED

    CLOSED --> OPEN: Failure threshold exceeded
    OPEN --> HALF_OPEN: Wait duration elapsed
    HALF_OPEN --> CLOSED: Test requests succeed
    HALF_OPEN --> OPEN: Test request fails
@CircuitBreaker(
name = "customerService",
fallbackMethod = "customerFallback"
)
public Customer getCustomer(Long id) {
return customerClient.getCustomer(id);
}

Configure:

  • Failure-rate threshold
  • Sliding window
  • Minimum number of calls
  • Open-state wait duration
  • Number of permitted half-open calls

Fallback behavior should depend on the business capability.

If recommendations fail:

flowchart LR
    A[Recommendation Request] --> B[Recommendation Service]
    B -->|Failure| C[Circuit Breaker]
    C --> D[Fallback]
    D --> E[Popular Products / Cached Data]
    E --> F[User]

Possible fallback strategies:

  • Cached response
  • Default response
  • Read-only mode
  • Queue operation asynchronously
  • Graceful degradation
  • User-friendly error

For payment or order processing, don’t silently return fake/default data.

Instead, clearly indicate that the operation could not be completed.


The user should see a business-friendly message, not a Java exception.

503 Service Unavailable - SocketTimeoutException
We're temporarily unable to load your orders.
Please try again.

React can centralize error handling using an Axios interceptor.

axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 503) {
showNotification(
"Service is temporarily unavailable. Please try again."
);
}
return Promise.reject(error);
}
);
Error User experience
Network failure Check your connection
401 Redirect to login
403 You don’t have permission
404 Resource not found
429 Too many requests, try later
500 Something went wrong
503 Service temporarily unavailable

2. Logging and Observability for API Failures

Section titled “2. Logging and Observability for API Failures”

Use structured JSON logging and correlate frontend and backend logs using a trace/correlation ID.

Useful information:

  • API URL or logical API name
  • HTTP method
  • Status code
  • Timestamp
  • Request duration
  • Trace/correlation ID
  • Error type
  • Component/page
  • Retry attempt
  • User action that triggered the request
  • Browser/device information where appropriate

Example:

{
"level": "ERROR",
"api": "/api/orders",
"method": "GET",
"status": 503,
"durationMs": 3200,
"traceId": "abc-123",
"errorCode": "ORDER_SERVICE_UNAVAILABLE",
"page": "OrderHistory"
}

Do not log:

  • Passwords
  • Access tokens
  • Authorization headers
  • Credit-card numbers
  • Sensitive customer data

Useful information:

  • Trace/correlation ID
  • Request ID
  • Service name
  • API endpoint
  • HTTP method
  • Status code
  • Response time
  • Exception type
  • Stack trace
  • Downstream service
  • Retry count
  • Circuit-breaker state
  • Timeout information
  • Database failures
  • Kafka/message information where applicable
  • Host/container/pod information

Example:

{
"level": "ERROR",
"service": "order-service",
"traceId": "abc-123",
"endpoint": "/api/orders",
"downstream": "customer-service",
"status": 503,
"retryCount": 2,
"circuitState": "OPEN",
"durationMs": 3100,
"exception": "SocketTimeoutException"
}

3. Interview Summary: Resilient API Design

Section titled “3. Interview Summary: Resilient API Design”

I would standardize the backend error contract using @ControllerAdvice, validate and map inconsistent downstream responses into stable DTOs, and use Resilience4j for timeout, limited exponential-backoff retries, circuit breakers, and business-appropriate fallbacks. On the React side, I would centralize API error handling through Axios interceptors and show user-friendly notifications. I would use structured logging with a correlation/trace ID across frontend and backend, capturing status, latency, retry count, downstream service, circuit state, and exception details while ensuring that credentials and sensitive customer data are never logged.


Design a scalable Java Full Stack architecture where ReactJS consumes multiple backend APIs.

Discuss:

  • API Gateway
  • Authentication
  • Caching
  • Observability
  • Containerization
  • CI/CD
  • Horizontal scaling
  • Microservices vs Monolith

A scalable architecture can be designed as:

flowchart TB
    U[Users] --> R[ReactJS Application]
    R -->|HTTPS / JWT| G[API Gateway]

    G --> US[User Service]
    G --> OS[Order Service]
    G --> PS[Payment Service]
    G --> NS[Notification Service]

    US --> UDB[(User DB)]
    OS --> ODB[(Order DB)]
    PS --> PDB[(Payment DB)]

    US --> C[(Redis Cache)]
    OS --> C

    OS --> K[Kafka / Event Streaming]
    K --> NS

    US --> O[Observability]
    OS --> O
    PS --> O
    G --> O

    subgraph Kubernetes
        US
        OS
        PS
        NS
    end

React should generally not communicate directly with every microservice.

Instead:

flowchart LR
    R[React] --> G[API Gateway]
    G --> U[User Service]
    G --> O[Order Service]
    G --> P[Payment Service]
    G --> N[Notification Service]

The API Gateway provides a single entry point.

  • Routing
  • Authentication/token validation
  • Authorization checks where appropriate
  • Rate limiting
  • Request/response transformation
  • CORS
  • TLS termination
  • Load balancing
  • Correlation ID propagation
  • API versioning
  • Circuit breaking where appropriate

Example:

GET /api/orders
|
v
API Gateway
|
v
Order Service

React does not need to know where the Order Service is physically deployed.


Use an OAuth2/OIDC-based identity provider such as:

  • Keycloak
  • Okta
  • Azure AD / Microsoft Entra ID
  • Another enterprise identity provider

Typical flow:

sequenceDiagram
    participant R as React
    participant IDP as Identity Provider
    participant G as API Gateway
    participant S as Backend Service

    R->>IDP: Login
    IDP-->>R: Access Token
    R->>G: API Request + Bearer Token
    G->>G: Validate JWT
    G->>S: Forward Request
    S->>S: Authorize Resource
    S-->>G: Response
    G-->>R: Response

JWT claims can include:

sub
roles
scopes
exp
issuer

Authentication:

Who are you?

Authorization:

What are you allowed to do?

For example:

ADMIN → Create/Delete users
USER → View own orders

Spring Security can enforce authorization at the service level.

Do not rely exclusively on the gateway for authorization.

Each microservice should protect its own resources because services may also be accessed through internal communication.


Use Redis for frequently accessed and relatively stable data.

Architecture:

flowchart LR
    R[React] --> G[Gateway]
    G --> P[Product Service]
    P --> C{Redis Cache}

    C -->|Cache Hit| R
    C -->|Cache Miss| DB[(Database)]
    DB --> C
    C --> P
    P --> R
  • Product/catalog data
  • Reference/master data
  • User preferences
  • Frequently accessed configuration
  • Session-related data where appropriate

Define:

  • TTL
  • Cache eviction strategy
  • Cache invalidation
  • Maximum cache size

For rapidly changing data, avoid aggressive caching because stale data can become a business problem.


For distributed systems, logs alone are not enough.

Use the three pillars:

  1. Logs
  2. Metrics
  3. Distributed tracing

Use structured JSON logs and centralized collection.

flowchart LR
    S[Spring Boot Services] --> F[Fluent Bit / Log Collector]
    F --> E[Elasticsearch]
    E --> K[Kibana]

Log:

  • Trace ID
  • Request ID
  • Service
  • Endpoint
  • Status
  • Latency
  • Exception
  • Business correlation information

Never log passwords, tokens, or sensitive customer information.


Use:

flowchart LR
    S[Spring Boot] --> A[Spring Boot Actuator]
    A --> P[Prometheus]
    P --> G[Grafana]

Monitor:

  • CPU
  • Memory
  • Request rate
  • Error rate
  • Response time
  • JVM memory
  • GC
  • Thread pools
  • Database connection pools
  • Kafka lag
  • Circuit breaker state

Use OpenTelemetry with a tracing backend such as Jaeger or Tempo.

A request might flow through:

React
Gateway
Order Service
Payment Service
Database

A shared trace ID allows the complete request path to be investigated.

This helps answer:

Why did this API take 5 seconds?


Each Spring Boot service can be packaged as a Docker image.

flowchart LR
    A[Spring Boot Application] --> B[Docker Image]
    B --> C[Container]
    C --> D[Kubernetes Pod]

Example Dockerfile:

FROM eclipse-temurin:21-jre
COPY target/order-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Containers should be:

  • Stateless
  • Small
  • Immutable
  • Configured through environment/config management
  • Free from persistent local storage

Persistent data belongs in external databases or storage systems.


Kubernetes allows multiple instances of a service to run simultaneously.

flowchart TB
    LB[Load Balancer]
    LB --> P1[Order Pod 1]
    LB --> P2[Order Pod 2]
    LB --> P3[Order Pod 3]

    P1 --> DB[(Order Database)]
    P2 --> DB
    P3 --> DB

If traffic increases:

flowchart LR
    A[3 Pods] --> B[Traffic / CPU Increases]
    B --> C[Horizontal Pod Autoscaler]
    C --> D[6 Pods]

Use Kubernetes HPA based on:

  • CPU
  • Memory
  • Request rate
  • Other meaningful application metrics

Services should be stateless so any pod can process a request.


A production CI/CD pipeline could be:

flowchart LR
    A[Developer] --> B[Git]
    B --> C[Pull Request]
    C --> D[Code Review]
    D --> E[Build]
    E --> F[Unit Tests]
    F --> G[Static Analysis]
    G --> H[Security Scan]
    H --> I[Docker Build]
    I --> J[Container Registry]
    J --> K[Deploy Dev]
    K --> L[Integration Tests]
    L --> M[Deploy QA]
    M --> N[Approval]
    N --> O[Production]

Possible tools:

  • GitHub/GitLab
  • Maven
  • JUnit
  • SonarQube
  • Docker
  • Jenkins/GitHub Actions/GitLab CI
  • Kubernetes
  • Helm

For critical systems, use:

  • Blue-green deployments
  • Canary releases
  • Automated rollback
  • Health checks

Do not automatically choose microservices simply because scalability is required.


flowchart LR
    R[React] --> S[Spring Boot Application]
    S --> DB[(Single Database)]
  • Simpler development
  • Easier debugging
  • Easier deployment
  • Less infrastructure
  • Easier transactions
  • Lower operational complexity

For small or medium applications, a modular monolith can be an excellent starting point.


flowchart LR
    G[API Gateway] --> U[User Service]
    G --> O[Order Service]
    G --> P[Payment Service]
    G --> N[Notification Service]

    U --> UDB[(User DB)]
    O --> ODB[(Order DB)]
    P --> PDB[(Payment DB)]
  • Independent deployment
  • Independent scaling
  • Team ownership
  • Technology isolation
  • Fault isolation
  • Business-domain separation
  • Network failures
  • Distributed transactions
  • More monitoring
  • More deployment complexity
  • Service discovery/configuration
  • Data consistency challenges
  • Higher infrastructure cost

For a new system, start with a well-structured modular monolith unless there is a clear business or technical reason for microservices.

Extract services when there are:

  • Independent scaling requirements
  • Clear business boundaries
  • Different release cycles
  • Large development teams
  • Fault-isolation requirements
  • Strong domain boundaries

A good evolution path is:

flowchart LR
    A[Modular Monolith] --> B[Identify Bounded Contexts]
    B --> C[Extract High-Value Service]
    C --> D[Independent Deployment]
    D --> E[Independent Scaling]
    E --> F[Full Microservices Where Justified]

This avoids introducing distributed-system complexity before it is necessary.


flowchart TB
    U[Users] --> R[ReactJS]

    R -->|HTTPS + JWT| G[API Gateway]

    G --> US[User Service]
    G --> OS[Order Service]
    G --> PS[Payment Service]
    G --> NS[Notification Service]

    US --> UDB[(User DB)]
    OS --> ODB[(Order DB)]
    PS --> PDB[(Payment DB)]

    US --> REDIS[(Redis)]
    OS --> REDIS

    OS --> KAFKA[Kafka]
    KAFKA --> NS

    subgraph K8S[Kubernetes Cluster]
        G
        US
        OS
        PS
        NS
    end

    K8S --> HPA[Horizontal Pod Autoscaler]

    US --> OTEL[OpenTelemetry]
    OS --> OTEL
    PS --> OTEL
    NS --> OTEL
    G --> OTEL

    OTEL --> TRACE[Tracing Backend]

    US --> PROM[Prometheus]
    OS --> PROM
    PS --> PROM
    NS --> PROM

    PROM --> GRAF[Grafana]

    US --> LOG[Centralized Logging]
    OS --> LOG
    PS --> LOG
    NS --> LOG
    G --> LOG

    LOG --> KIBANA[Kibana]

Layer Main Responsibility
ReactJS UI, client-side state, routing, user experience
API Gateway Routing, security boundary, rate limiting, API composition
Spring Boot Services Business logic and domain capabilities
Spring Security Authentication/authorization enforcement
Redis Low-latency caching
Kafka Asynchronous event-driven communication
Database Persistent business data
Docker Application packaging
Kubernetes Container orchestration and scaling
HPA Horizontal scaling
Prometheus Metrics
Grafana Metrics dashboards and alerts
OpenTelemetry Distributed tracing/telemetry
Centralized logging Searchable application logs
CI/CD Automated build, test, security and deployment

I would design the application with React as the presentation layer and an API Gateway as the single entry point to Spring Boot backend services. Authentication would use OAuth2/OIDC with JWT, while authorization would be enforced at both the gateway and service level. Redis would be used for frequently accessed data, and Kafka could handle asynchronous communication between services.

For observability, I would implement centralized structured logging, Prometheus/Grafana metrics, and distributed tracing using OpenTelemetry. Each service would be containerized using Docker and deployed on Kubernetes, with HPA providing horizontal scaling.

CI/CD would automate build, testing, security scanning, container creation, and deployment. For critical services, I would use canary or blue-green deployments with automated rollback.

However, I would not choose microservices automatically. I would start with a modular monolith when the system and team are small or medium-sized, and extract services when independent scaling, deployment, fault isolation, or strong domain boundaries justify the additional distributed-system complexity.


For a senior/lead interview, emphasize these decisions:

“I design services to be stateless so they can scale horizontally.”

“Retries are controlled and only used for transient failures. Circuit breakers prevent repeatedly calling unhealthy dependencies.”

“Authentication and authorization are separate concerns, and services shouldn’t blindly trust the gateway.”

“I use Redis selectively and define an explicit cache invalidation strategy.”

“Every request should be traceable across React, gateway, microservices, databases, and asynchronous messaging.”

“Containers should be immutable and deployments should support health checks and rollback.”

“Microservices are a means to solve organizational, scalability, and domain problems—not a default architecture.”

“Each microservice should own its data where practical, and distributed transactions should be avoided in favor of patterns such as Saga and event-driven workflows when appropriate.”


flowchart TB
    A[React UI]
    B[Secure API Gateway]
    C[Authenticated Spring Boot Services]
    D[Redis Cache]
    E[Databases]
    F[Kafka Events]
    G[Observability]
    H[Kubernetes]
    I[CI/CD]

    A --> B
    B --> C
    C --> D
    C --> E
    C --> F
    C --> G
    C --> H
    I --> H

The overall design principle is:

Secure at the edge, validate contracts, keep services stateless, cache carefully, communicate asynchronously where appropriate, observe every request, scale horizontally, and introduce microservices only when their benefits outweigh their operational complexity.