Skip to main content

Why Your Spring Boot App Takes 8 Seconds to Start: A Bean-by-Bean Diagnosis

Spring Boot logs one startup number and hides four phases behind it. This walks through turning on the ApplicationStartup instrumentation that ships switched off, reading the step tree by self time rather than total duration — the mistake that makes people optimise a bean costing 10 ms instead of the one costing 504 — and then puts measured numbers on component scanning (0.11 ms per class inspected, whether or not it is a bean), on the silent truncation a 2048-step buffer performs on your recording, and on the JDK 25 AOT cache, which took 6.93 s to 4.82 s on the companion project without a line of application code changing.

Spring Boot Actuator in Production: Every Endpoint, Securing It, and Custom Health Indicators

Every Actuator endpoint on Spring Boot 4.1.1, what each one leaks, and how to secure it — measured rather than described. /actuator/env masks every value by default and heapdump is not exposed by include:"*" at all, but turn it on and it hands over 59 MB containing the credentials env just masked. Plus the three gates a request passes, a separate management port, custom indicators for a database, Kafka and an external API — and the naive Kafka health check that blocks for exactly 60.002 seconds because request.timeout.ms is not the property that bounds it.

RSocket vs gRPC vs WebSocket on Spring Boot 4.1: When Each One Wins

One Spring Boot 4.1 application serving the same two operations over gRPC, RSocket and a raw WebSocket, benchmarked in a single JVM. Raw WebSocket is the fastest and has the worst tail. protobuf is 35 bytes where JSON is 83. And for a consumer that asked for a hundred messages then went quiet, the three servers produced 100, 1,208,219 and 320,255 — which is the number that decides it.

Server-Sent Events and WebSocket on Spring Boot 4: SseEmitter, STOMP, and Which to Pick

Server-Sent Events and WebSocket are compared as rivals, but one question separates them: does the client need to send anything back? A beginner-to-advanced guide with everything measured — why your dashboard silently reconnects every 30 seconds, why a dead SSE client is only discovered on the second write, why a STOMP client can publish straight to a topic and skip your controller, and why Spring's 64 KB message limit is never the one that fires.

Kafka vs RabbitMQ vs Pulsar for Java Teams: A Decision Framework with Benchmarks

Three brokers, three questions, one set of measurements. Ordering, replay, consumer scaling and operational footprint measured against real Kafka 4.2.1, RabbitMQ 3.10.25 and Pulsar 4.2.4 brokers, ending in a decision table where every row has a transcript behind it — including the client-side buffer that silently defeats consumer fan-out in two of the three.

@Scheduled, ShedLock and Distributed Cron: Scheduling That Survives Three Replicas

Scale a Spring Boot deployment to three replicas and every @Scheduled method runs three times per tick. Measured on Boot 4.1.1 and PostgreSQL: 24 executions where 8 were due, a @SchedulerLock annotation that silently does nothing without @EnableSchedulerLock, a single scheduler thread that fires 35 of 40 executions in a burst rather than skipping them, and a node whose clock is 40 seconds fast taking a lock somebody else is holding.

@Async in Spring Boot 4: Executors, Virtual Threads and the Self-Invocation Trap

@Async is a proxy, and every surprising thing it does follows from that. Measured on Spring Boot 4.1.1 and JDK 25: the self-invocation trap, the IllegalArgumentException a plain return type throws, why max-size does nothing until queue-capacity is bounded, two Executor beans leaving @Async on an unpooled SimpleAsyncTaskExecutor, the new spring.task.execution.propagate-context property, and JEP 491 measured at 4806 ms on JDK 21 against 301 ms on JDK 25.

Kafka Error Handling with Spring Kafka 4.1: DLT, Retry Topics and Poison Pills

What Spring Kafka actually does when your listener throws, measured rather than described: the stock DefaultErrorHandler is ten deliveries zero milliseconds apart and then the record is dropped. Plus the poison pill that stops a partition before any listener exists, the DLT suffix that changed to -dlt so a misconfigured recoverer logs a warning and loses the record, why a dead-lettered poison pill arrives base64-encoded, why kafka_dlt-exception-fqcn is useless for triage, and what @RetryableTopic costs in ordering — with the delivery trace to prove it.

Spring Boot and RabbitMQ: Exchanges, Queues, Bindings and a Working Dead-Letter Queue

All four AMQP exchange types against a real broker, manual acknowledgement, and a dead-letter path that actually works — including the three triggers that fill a DLQ, the one that never does, and the message that vanishes with no error at all. Why publisher-returns and mandatory are two settings in two different places, why basicNack with requeue=true is an infinite loop that never dead-letters and never raises a queue-depth alarm, what x-death.reason tells you that nothing else does, and why changing a queue's TTL is a migration rather than a config edit.

Spring Boot 4.1 and Apache Kafka: Producer, Consumer and Serialisation from Scratch

The on-ramp to Spring Kafka under Boot 4, where three things changed at once: adding spring-kafka no longer gives you auto-configuration, JsonSerializer is now the Jackson 2 one and cannot write an Instant, and the default partitioner is not the formula you would write. A producer, a consumer, serialisation, keys and partitions — with every claim run against a real Kafka broker, including the effective-configuration table showing which durability defaults come from Kafka rather than Spring, and why ConsumerFactory.isAutoCommit() answers true while no consumer auto-commits.