Skip to main content

Class 12: Production Ready

Duration: 30 minutes Difficulty: Advanced Prerequisites: Completed Classes 1-11

What You'll Learn

By the end of this class, you will:

  • Add comprehensive monitoring and metrics
  • Implement response caching
  • Set up proper logging
  • Configure query complexity limits
  • Prepare for production deployment

Production Readiness Checklist

┌─────────────────────────────────────────────────────────────────────┐
│ PRODUCTION READINESS CHECKLIST │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ Performance │
│ □ DataLoader for N+1 prevention │
│ □ Query complexity limits │
│ □ Query depth limits │
│ □ Timeout configuration │
│ □ Connection pooling │
│ │
│ ✅ Security │
│ □ Authentication implemented │
│ □ Authorization at field level │
│ □ Input validation │
│ □ Introspection disabled in production │
│ □ Rate limiting │
│ │
│ ✅ Observability │
│ □ Request logging │
│ □ Error tracking │
│ □ Metrics collection │
│ □ Distributed tracing │
│ □ Health checks │
│ │
│ ✅ Reliability │
│ □ Graceful error handling │
│ □ Circuit breakers for external services │
│ □ Retry logic where appropriate │
│ □ Graceful shutdown │
│ │
└─────────────────────────────────────────────────────────────────────┘

Step 1: Add Actuator for Health Checks

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

📁 Update application.properties:

# Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=when-authorized
management.health.defaults.enabled=true

# GraphQL specific
spring.graphql.graphiql.enabled=${GRAPHIQL_ENABLED:false}
spring.graphql.schema.introspection.enabled=${INTROSPECTION_ENABLED:false}
spring.graphql.websocket.path=/graphql

Step 2: Add GraphQL Metrics

📁 src/main/java/com/example/moviedb/config/GraphQLMetricsConfig.java

package com.example.moviedb.config;

import graphql.ExecutionResult;
import graphql.GraphQLError;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Configuration
public class GraphQLMetricsConfig {

@Bean
public WebGraphQlInterceptor metricsInterceptor(MeterRegistry registry) {
Counter requestCounter = Counter.builder("graphql.requests.total")
.description("Total GraphQL requests")
.register(registry);

Counter errorCounter = Counter.builder("graphql.errors.total")
.description("Total GraphQL errors")
.register(registry);

Timer requestTimer = Timer.builder("graphql.request.duration")
.description("GraphQL request duration")
.register(registry);

return (request, chain) -> {
long startTime = System.nanoTime();
String operationName = getOperationName(request);

return chain.next(request)
.doOnNext(response -> {
// Record metrics
long duration = System.nanoTime() - startTime;
requestTimer.record(duration, TimeUnit.NANOSECONDS);
requestCounter.increment();

// Count errors
if (!response.getErrors().isEmpty()) {
response.getErrors().forEach(error -> {
Counter.builder("graphql.errors")
.tag("operation", operationName)
.tag("type", getErrorType(error))
.register(registry)
.increment();
});
errorCounter.increment(response.getErrors().size());
}

// Record by operation
Timer.builder("graphql.operation.duration")
.tag("operation", operationName)
.tag("status", response.getErrors().isEmpty() ? "success" : "error")
.register(registry)
.record(duration, TimeUnit.NANOSECONDS);
});
};
}

private String getOperationName(WebGraphQlRequest request) {
String name = request.getOperationName();
return name != null ? name : "anonymous";
}

private String getErrorType(GraphQLError error) {
if (error.getExtensions() != null && error.getExtensions().containsKey("classification")) {
return error.getExtensions().get("classification").toString();
}
return "UNKNOWN";
}
}

Step 3: Request Logging

📁 src/main/java/com/example/moviedb/config/GraphQLLoggingConfig.java

package com.example.moviedb.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;

import java.util.UUID;

@Configuration
public class GraphQLLoggingConfig {

private static final Logger log = LoggerFactory.getLogger("GraphQL");

@Bean
public WebGraphQlInterceptor loggingInterceptor() {
return (request, chain) -> {
String requestId = UUID.randomUUID().toString().substring(0, 8);
MDC.put("requestId", requestId);

String operationName = request.getOperationName() != null
? request.getOperationName()
: "anonymous";

long startTime = System.currentTimeMillis();

log.info("→ GraphQL Request [{}] operation={}", requestId, operationName);

if (log.isDebugEnabled()) {
log.debug("Query: {}", truncate(request.getDocument(), 500));
log.debug("Variables: {}", request.getVariables());
}

return chain.next(request)
.doOnNext(response -> {
long duration = System.currentTimeMillis() - startTime;
int errorCount = response.getErrors().size();

if (errorCount > 0) {
log.warn("← GraphQL Response [{}] operation={} duration={}ms errors={}",
requestId, operationName, duration, errorCount);
response.getErrors().forEach(error ->
log.warn(" Error: {}", error.getMessage()));
} else {
log.info("← GraphQL Response [{}] operation={} duration={}ms",
requestId, operationName, duration);
}
})
.doFinally(signal -> MDC.remove("requestId"));
};
}

private String truncate(String str, int maxLength) {
if (str == null) return null;
if (str.length() <= maxLength) return str;
return str.substring(0, maxLength) + "...";
}
}

Step 4: Query Complexity and Depth Limits

📁 src/main/java/com/example/moviedb/config/QueryProtectionConfig.java

package com.example.moviedb.config;

import graphql.analysis.MaxQueryComplexityInstrumentation;
import graphql.analysis.MaxQueryDepthInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class QueryProtectionConfig {

@Bean
public Instrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(15); // Max 15 levels deep
}

@Bean
public Instrumentation maxQueryComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(200); // Max complexity score
}
}

Step 5: Response Caching

📁 src/main/java/com/example/moviedb/config/CacheConfig.java

package com.example.moviedb.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;

import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
import java.util.Map;

@Configuration
public class CacheConfig {

@Bean
public Cache<String, Map<String, Object>> graphqlCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats()
.build();
}

@Bean
public WebGraphQlInterceptor cacheInterceptor(Cache<String, Map<String, Object>> cache) {
return (request, chain) -> {
// Only cache queries, not mutations or subscriptions
if (!isQuery(request)) {
return chain.next(request);
}

String cacheKey = generateCacheKey(request);
Map<String, Object> cachedResponse = cache.getIfPresent(cacheKey);

if (cachedResponse != null) {
// Return cached response
return Mono.just(new CachedWebGraphQlResponse(cachedResponse));
}

return chain.next(request)
.doOnNext(response -> {
if (response.getErrors().isEmpty()) {
cache.put(cacheKey, response.getData());
}
});
};
}

private boolean isQuery(WebGraphQlRequest request) {
String document = request.getDocument().toLowerCase();
return !document.contains("mutation") && !document.contains("subscription");
}

private String generateCacheKey(WebGraphQlRequest request) {
try {
String content = request.getDocument() +
request.getVariables().toString() +
(request.getOperationName() != null ? request.getOperationName() : "");
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(content.getBytes());
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to generate cache key", e);
}
}
}

Step 6: Timeout Configuration

📁 Update application.properties:

# Request timeouts
spring.graphql.websocket.connection-init-timeout=10s

# Server timeouts
server.tomcat.connection-timeout=30s

# Custom timeout (we'll implement this)
graphql.timeout.default=30s
graphql.timeout.subscription=300s

📁 src/main/java/com/example/moviedb/config/TimeoutConfig.java

package com.example.moviedb.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.concurrent.TimeoutException;

@Configuration
public class TimeoutConfig {

@Value("${graphql.timeout.default:30s}")
private Duration defaultTimeout;

@Bean
public WebGraphQlInterceptor timeoutInterceptor() {
return (request, chain) -> chain.next(request)
.timeout(defaultTimeout)
.onErrorResume(TimeoutException.class, e -> {
throw new RuntimeException("Query timeout exceeded: " + defaultTimeout);
});
}
}

Step 7: Disable Introspection in Production

📁 src/main/java/com/example/moviedb/config/IntrospectionConfig.java

package com.example.moviedb.config;

import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.GraphQlSource;

@Configuration
public class IntrospectionConfig {

@Value("${spring.graphql.schema.introspection.enabled:true}")
private boolean introspectionEnabled;

@Bean
public GraphQlSource.SchemaResourceBuilder schemaResourceBuilder() {
GraphQlSource.SchemaResourceBuilder builder = GraphQlSource.schemaResourceBuilder();

if (!introspectionEnabled) {
builder.configureGraphQl(graphQL ->
graphQL.fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
);
}

return builder;
}
}

Step 8: Rate Limiting

📁 src/main/java/com/example/moviedb/config/RateLimitConfig.java

package com.example.moviedb.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;

@Configuration
public class RateLimitConfig {

private static final int MAX_REQUESTS_PER_MINUTE = 100;

@Bean
public Cache<String, AtomicInteger> rateLimitCache() {
return Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.build();
}

@Bean
public WebGraphQlInterceptor rateLimitInterceptor(Cache<String, AtomicInteger> cache) {
return (request, chain) -> {
String clientId = getClientId();
AtomicInteger counter = cache.get(clientId, k -> new AtomicInteger(0));

int count = counter.incrementAndGet();
if (count > MAX_REQUESTS_PER_MINUTE) {
throw new RuntimeException("Rate limit exceeded. Max " +
MAX_REQUESTS_PER_MINUTE + " requests per minute.");
}

return chain.next(request);
};
}

private String getClientId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return "user:" + auth.getName();
}
return "anonymous";
}
}

Step 9: Health Check Endpoint

📁 src/main/java/com/example/moviedb/health/GraphQLHealthIndicator.java

package com.example.moviedb.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.stereotype.Component;

@Component
public class GraphQLHealthIndicator implements HealthIndicator {

private final ExecutionGraphQlService graphQlService;

public GraphQLHealthIndicator(ExecutionGraphQlService graphQlService) {
this.graphQlService = graphQlService;
}

@Override
public Health health() {
try {
// Execute a simple introspection query
var result = graphQlService.execute(
org.springframework.graphql.ExecutionGraphQlRequest.create("{ __typename }")
).block();

if (result != null && result.getErrors().isEmpty()) {
return Health.up()
.withDetail("graphql", "Available")
.build();
}

return Health.down()
.withDetail("graphql", "Query returned errors")
.build();

} catch (Exception e) {
return Health.down()
.withDetail("graphql", "Exception: " + e.getMessage())
.build();
}
}
}

Step 10: Production Configuration

📁 src/main/resources/application-prod.properties:

# Production profile settings

# Disable dev tools
spring.graphql.graphiql.enabled=false
spring.graphql.schema.introspection.enabled=false

# Security
spring.security.require-ssl=true

# Logging
logging.level.com.example.moviedb=INFO
logging.level.GraphQL=INFO

# Performance
server.tomcat.max-threads=200
server.tomcat.accept-count=100
server.tomcat.connection-timeout=20s

# Metrics
management.endpoints.web.exposure.include=health,info,prometheus
management.endpoint.health.probes.enabled=true
management.health.livenessState.enabled=true
management.health.readinessState.enabled=true

# Caching
graphql.cache.enabled=true
graphql.cache.ttl=300

# Rate limiting
graphql.ratelimit.enabled=true
graphql.ratelimit.requests-per-minute=100

Deployment Checklist

┌─────────────────────────────────────────────────────────────────────┐
│ DEPLOYMENT CHECKLIST │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Pre-Deployment: │
│ □ All tests passing │
│ □ Security review completed │
│ □ Performance benchmarks acceptable │
│ □ Documentation updated │
│ □ Schema changes reviewed (backwards compatible?) │
│ │
│ Configuration: │
│ □ Introspection disabled │
│ □ GraphiQL disabled │
│ □ Proper logging levels │
│ □ Timeouts configured │
│ □ Rate limits configured │
│ □ SSL/TLS configured │
│ │
│ Monitoring: │
│ □ Health checks configured │
│ □ Metrics exposed (Prometheus) │
│ □ Alerts configured │
│ □ Error tracking (Sentry/etc) │
│ □ Dashboard created │
│ │
│ Post-Deployment: │
│ □ Smoke tests passing │
│ □ Metrics flowing │
│ □ No unexpected errors │
│ □ Response times acceptable │
│ │
└─────────────────────────────────────────────────────────────────────┘

Summary

In this class, you learned:

✅ Adding comprehensive metrics with Micrometer ✅ Request logging with MDC correlation ✅ Query complexity and depth limits ✅ Response caching strategies ✅ Rate limiting implementation ✅ Disabling introspection in production ✅ Health check endpoints ✅ Production configuration best practices

Congratulations! 🎉

You've completed the Spring GraphQL Tutorial! You now have the knowledge to:

  • Build GraphQL APIs from scratch
  • Design effective schemas
  • Handle queries, mutations, and subscriptions
  • Implement security and authorization
  • Test your GraphQL APIs
  • Deploy to production with confidence

Next Steps

  1. Build something! Apply what you've learned to a real project
  2. Explore Federation - For microservices architectures
  3. Learn about persisted queries - For additional security and performance
  4. Study GraphQL tooling - Apollo Studio, GraphQL Voyager, etc.
  5. Join the community - GraphQL Conf, Discord communities, GitHub discussions

Happy coding! 🚀