Production & Advanced — Cheat Sheet
Spring Boot · 6 topics. Download the PDF or the Instagram carousel and share it.
Application Events
Spring's event system decouples components — one service publishes an event without knowing who handles it. @TransactionalEventListener ensures events fire only after the transaction commits, preventing side effects from rolled-back data.
- ✓Events decouple publishers from listeners — the UserService doesn't know or care about welcome emails.
- ✓@EventListener methods can be in any Spring bean — listeners discover events by parameter type.
- ✓@TransactionalEventListener(AFTER_COMMIT) is essential for database-triggered events — prevents firing on rollback.
- ✓Default event dispatch is synchronous — add @Async to run listeners in a thread pool.
- ✓@EnableAsync on a @Configuration class is required for @Async to work.
- ✓Use events for cross-cutting concerns (email, audit, cache invalidation) — not for primary business logic.
// Event object — plain class or Java record
public record UserRegisteredEvent(String userId, String email, Instant registeredAt) {}
// Publisher — inject ApplicationEventPublisher
@Service
public class UserService {
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
public UserService(UserRepository userRepository,
ApplicationEventPublisher eventPublisher) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public User register(RegisterRequest req) {
User user = userRepository.save(new User(req));
// Publish event — listeners fire after this method returns
eventPublisher.publishEvent(
new UserRegisteredEvent(user.getId(), user.getEmail(), Instant.now())
);
return user;
}
}
// Listeners — any @Component can listen
@Component
public class WelcomeEmailListener {
private final EmailService emailService;
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcome(event.email());
}
}
@Component
public class GamificationListener {
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
// Award signup XP
gamificationService.awardXp(event.userId(), "signup", 10);
}
}Kafka Messaging
Spring Kafka wraps the Kafka client with KafkaTemplate for producing and @KafkaListener for consuming. Understanding partitions, consumer groups, and error handling is essential for production event-driven architectures.
- ✓Producer key determines partition assignment — same key always goes to the same partition (ordering guarantee).
- ✓acks=all + enable.idempotence=true gives at-least-once delivery with duplicate protection on the producer side.
- ✓Consumer group ID determines the group — each partition is assigned to one consumer per group for parallel processing.
- ✓concurrency on @KafkaListener sets the number of consumer threads — max useful value equals the partition count.
- ✓DeadLetterPublishingRecoverer sends failed messages to {topic}.DLT after exhausting retries — prevents consumer blocking.
- ✓Auto-commit is dangerous — use manual offset commit (AckMode.MANUAL) for exactly-once processing guarantees.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
# application.yml
spring:
kafka:
bootstrap-servers: ${KAFKA_BROKERS:localhost:9092}
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
acks: all # wait for all replicas before acknowledging
retries: 3
properties:
enable.idempotence: true # exactly-once producer semantics
@Service
public class OrderEventProducer {
private static final String TOPIC = "order-events";
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
public OrderEventProducer(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderPlaced(Order order) {
OrderEvent event = new OrderEvent(order.getId(), "ORDER_PLACED",
order.getCustomerId(), Instant.now());
// Key = customerId → same customer's orders go to same partition (ordered)
kafkaTemplate.send(TOPIC, order.getCustomerId(), event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Failed to publish order event: {}", order.getId(), ex);
// Dead-letter, alert, or store for retry
} else {
log.info("Published to partition {} offset {}",
result.getRecordMetadata().partition(),
result.getRecordMetadata().offset());
}
});
}
}Feign Client
Feign is a declarative HTTP client — define an interface annotated with Spring MVC annotations and Feign generates the implementation. Add Resilience4j for circuit breaking and retry to make inter-service calls production-safe.
- ✓@EnableFeignClients on your main class triggers scanning for @FeignClient interfaces.
- ✓Feign interfaces use Spring MVC annotations (@GetMapping, @RequestBody etc.) — familiar and readable.
- ✓FallbackFactory (preferred over Fallback) receives the exception so you can log and handle it properly.
- ✓Always configure connect and read timeouts — Feign has no defaults, meaning it can wait forever.
- ✓Combine with Resilience4j circuit breaker to stop cascading failures when a downstream service is down.
- ✓For async inter-service calls, use WebClient instead of Feign — Feign is synchronous/blocking.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
// Enable Feign on main class or @Configuration
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApp { ... }
// Feign client interface — Feign generates the implementation
@FeignClient(
name = "payment-service",
url = "${services.payment.url:http://payment-service}",
fallbackFactory = PaymentClientFallbackFactory.class
)
public interface PaymentClient {
@PostMapping("/api/v1/payments/charge")
PaymentResponse charge(@RequestBody ChargeRequest request);
@GetMapping("/api/v1/payments/{paymentId}")
PaymentResponse getPayment(@PathVariable String paymentId);
@DeleteMapping("/api/v1/payments/{paymentId}/refund")
RefundResponse refund(@PathVariable String paymentId);
}
// Inject and use exactly like any other Spring bean
@Service
public class OrderService {
private final PaymentClient paymentClient;
public Order placeOrder(PlaceOrderRequest req) {
// Feign handles HTTP, serialization, error mapping
PaymentResponse payment = paymentClient.charge(
new ChargeRequest(req.userId(), req.total(), req.paymentMethod())
);
return createOrder(req, payment.getId());
}
}API Gateway
Spring Cloud Gateway is a reactive API gateway — it routes external traffic to microservices, applies cross-cutting filters (auth, rate limiting, logging), and is configured either in YAML or via Java DSL.
- ✓Spring Cloud Gateway is reactive (WebFlux) — do not mix it with Spring MVC/Tomcat blocking code.
- ✓Routes = Predicates (when to match) + Filters (what to transform) + URI (where to forward).
- ✓Use lb://service-name URIs with Spring Cloud LoadBalancer for client-side load balancing.
- ✓GlobalFilter applies to all routes; GatewayFilter applies to specific routes.
- ✓Validate JWTs at the gateway and forward the user ID as a trusted header to downstream services.
- ✓Circuit breaker + retry filters in the gateway protect against downstream service failures.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
# application.yml — declarative route configuration
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: http://auth-service:8080
predicates:
- Path=/api/v1/auth/**
filters:
- StripPrefix=0 # keep the path as-is
- id: course-service
uri: http://course-service:8080
predicates:
- Path=/api/v1/courses/**
- Method=GET,POST
filters:
- AddRequestHeader=X-Gateway, spring-cloud-gateway
- RewritePath=/api/v1/courses/(?<segment>.*), /courses/${segment}
- CircuitBreaker=name=course-cb,fallbackUri=forward:/fallback
- id: user-service
uri: lb://user-service # lb:// = Spring Cloud LoadBalancer
predicates:
- Path=/api/v1/users/**
filters:
- name: Retry
args:
retries: 3
methods: GET
statuses: BAD_GATEWAYDocker & Deployment
A multi-stage Dockerfile builds a lean production image. Docker Compose orchestrates your Spring Boot app with its dependencies (PostgreSQL, Redis) for local development and testing.
- ✓Multi-stage builds keep production images small — only the JRE and app code, no JDK or Maven.
- ✓Spring Boot's layered JARs optimize rebuild time — dependency layers rarely change between deploys.
- ✓Always run the JVM process as a non-root user inside the container.
- ✓Use -XX:MaxRAMPercentage=75.0 instead of -Xmx in containers — JVM reads the cgroup memory limit.
- ✓depends_on with service_healthy waits for the database health check before starting the app.
- ✓Buildpacks (spring-boot:build-image) generate a production image automatically without a Dockerfile.
# Stage 1: Build with Maven + JDK FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /app # Download dependencies separately (cacheable layer) COPY pom.xml . COPY .mvn/ .mvn COPY mvnw . RUN ./mvnw dependency:go-offline -q # Build COPY src/ src/ RUN ./mvnw package -DskipTests -q # Extract layered JAR (Spring Boot 2.3+) RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted # Stage 2: Runtime — JRE only, no JDK or Maven FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Non-root user for security RUN addgroup -S spring && adduser -S spring -G spring USER spring # Copy layers from smallest-change (deps) to largest-change (app) COPY --from=builder /app/target/extracted/dependencies/ ./ COPY --from=builder /app/target/extracted/spring-boot-loader/ ./ COPY --from=builder /app/target/extracted/snapshot-dependencies/ ./ COPY --from=builder /app/target/extracted/application/ ./ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=40s CMD wget -q http://localhost:8080/actuator/health -O- | grep -q UP ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "org.springframework.boot.loader.launch.JarLauncher"]
Observability
Observability is knowing what your service is doing in production. The three pillars are metrics (Micrometer → Prometheus), logs (structured with MDC correlation IDs), and traces (Spring Micrometer Tracing → Zipkin/Grafana Tempo).
- ✓Micrometer is the metrics facade — auto-configures JVM, HTTP, and data source metrics out of the box.
- ✓@Timed adds latency metrics to methods; Counter tracks occurrence counts; Gauge tracks current values.
- ✓Tag every metric with application name — required to filter metrics by service in Prometheus/Grafana.
- ✓MDC injects key-value pairs (correlation ID) into every log line — clear it after each request with MDC.clear().
- ✓Spring Boot 3 + micrometer-tracing auto-propagates trace/span IDs across service calls and into MDC.
- ✓Sample 1–10% of traces in production with tracing.sampling.probability — full sampling is expensive.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name} # tag all metrics
@Service
public class PaymentService {
private final MeterRegistry meterRegistry;
private final Counter paymentCounter;
private final Timer paymentTimer;
public PaymentService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.paymentCounter = Counter.builder("payments.processed")
.description("Total payments processed")
.tag("env", "production")
.register(meterRegistry);
this.paymentTimer = Timer.builder("payments.duration")
.description("Payment processing latency")
.register(meterRegistry);
}
public PaymentResult charge(ChargeRequest req) {
return paymentTimer.recordCallable(() -> {
PaymentResult result = processCharge(req);
paymentCounter.increment(1, Tags.of("status", result.status()));
return result;
});
}
// Simpler: @Timed annotation (requires @EnableAspectJAutoProxy)
@Timed(value = "payments.duration", description = "Payment charge latency")
public PaymentResult chargeWithTimed(ChargeRequest req) {
return processCharge(req);
}
}