Skip to main content

Spring Boot Docker Compose Support: Real Containers for Local Development

Testcontainers (used throughout earlier posts in this series) is for tests: start a container, run assertions, tear it down, all inside the test JVM's lifecycle. Spring Boot's Docker Compose support solves a neighbouring problem — local development. Run the app normally, with a compose.yaml next to it, and Spring Boot starts the declared containers itself, wires up a real connection to them, and stops them again when the app exits. No spring.datasource.* property anywhere, and no manual docker compose up either. This post proves that end to end: a real docker compose up actually ran, a real Postgres container came up on a real (non-default) host port, a repository backed by a plain JdbcTemplate read and wrote through it, and — checked after the JVM exited, not assumed — the container actually stopped again. Versions used in this post. Spring Boot 4.1.1 · Spring Framework 7.0.9 · PostgreSQL JDBC driver 42.7.13 · postgres:17-alpine · Docker 29.8.2 · Docker Compose v5.5.1 · JDK 25 (Temurin). Confirmed via mvn dependency:tree and the real docker/docker compose CLI versions on the machine that ran this post's tests. This demo is small enough that it doesn't warrant its own repository module — every file is linked for download right where it's discussed below.

Structured Logging in Spring Boot 4: SLF4J Fluent API, MDC, and Custom Formatters

A human-readable log line is great for a terminal and terrible for a log aggregator, which has to regex-scrape fields back out of it. Structured logging flips that: write one JSON object per line instead, with fields as real JSON members from the start. Spring Boot 4 ships this as a built-in feature — no Logstash encoder dependency, no hand-rolled JSON layout — and this post builds a small service around it: one line enabled via a property, SLF4J's fluent API and MDC both landing as real JSON fields with zero extra code, and a custom formatter for when none of the built-in formats fit. Everything below was run for real: a Spring Boot context actually bootstrapped with logging.structured.format.console=ecs in effect, its console output captured and asserted on line by line, not assumed from the reference docs. Versions used in this post. Spring Boot 4.1.1 · Spring Framework 7.0.9 · Logback 1.5.38 · SLF4J 2.0.18 · JDK 25 (Temurin). Confirmed via mvn dependency:tree on spring-boot-starter alone — no web starter, no JSON library anywhere on the classpath. This demo is small enough that it doesn't warrant its own repository module — every source file is linked for download right where it's discussed below.

Apache Camel 4 on Spring Boot: Content-Based Routing and Dead-Letter Handling

Apache Camel's whole pitch is that integration logic — where a message goes, what happens when processing it fails — is declared once in a route, not scattered across try/catch blocks and if-statements. This post builds a small order-intake route on Camel 4 and Spring Boot 4: content-based routing on a header, and a redelivery policy that retries a failing message twice before giving up and shunting it to a dead-letter endpoint. The route logic itself turned out to be the easy part. The real content here is three things that only showed up by actually running it: confirming Camel 4 resolves cleanly against Spring Boot 4.1's dependencies at all, a genuinely useful testability trick (routing to property-placeholder endpoints so a test can swap in mocks without touching the route), and a sneaky test-isolation bug in that same trick's test harness. Versions used in this post. Spring Boot 4.1.1 · Apache Camel 4.22.1 (camel-spring-boot-bom) · JDK 25 (Temurin). Camel 4.22.1 was the latest release on Maven Central at the time of writing, checked against the real maven-metadata.xml rather than assumed, and mvn dependency:tree confirmed it pulls in Spring Boot 4.1.1 and Spring Framework 7.0.9 with no version conflicts. This demo is small enough that it doesn't warrant its own repository module — every source file is linked for download right where it's discussed below.

Distributed Caching in Spring Boot: Hazelcast vs Infinispan vs Redis

Spring's cache abstraction makes a specific promise: annotate a method with @Cacheable, and the business code never knows or cares which product is actually storing the cached value. This post puts that promise to the test with three genuinely different backends — Hazelcast and Infinispan, both embedded in-process, and Redis, a real out-of-process server — running the exact same service class and the exact same test logic against each, swapped only by which CacheManager bean wins. The interesting part turned out not to be the happy path, which is identical across all three, but three real failures along the way: a missing transitive dependency that breaks startup regardless of which backend you asked for, a JMX collision between embedded cache managers sharing one test JVM, and one backend that — unlike the other two — refuses to create a cache you never declared. Versions used in this post. Spring Boot 4.1.1 · com.hazelcast:hazelcast-spring 5.5.0 · org.infinispan:infinispan-spring-boot4-starter-embedded 16.1.4 · Redis server 8.8.3 (Testcontainers) · JDK 25 (Temurin) · Testcontainers 2.0.5. All three resolved and verified via mvn dependency:tree and javap against the real jars — none of the class names or artifact coordinates below were assumed from documentation.

Spring Data Redis JsonOperations: Partial Reads and Writes on JSON Documents

Most Redis client code still treats a value as an opaque blob: read the whole thing, deserialize it, change one field in Java, serialize it, write the whole thing back. Spring Data Redis's new JsonOperations skips that round trip entirely — it talks the RedisJSON module's own commands (JSON.SET, JSON.GET, JSON.ARRAPPEND, ...), which read and mutate a document inside Redis, server-side, addressed by a JSONPath expression. You can fetch one field, append to a nested array, or read a computed slice of a document without ever pulling the rest of it over the wire. This post builds a small shopping-cart service around it: a whole-document save and read, a single-field path read, an array append that never touches the rest of the document, and a delete — each backed by a real Redis 8 container and a passing test. Versions used in this post. Spring Boot 4.1.1 · Spring Data Redis 4.2.0-M2 (milestone — see below) · Jackson 2 jackson-databind (added explicitly) · Redis server 8.8.3 · JDK 25 (Temurin) · Testcontainers 2.0.5. JsonOperations/RedisJsonTemplate does not exist at all in the 4.1.1 GA line that Spring Boot 4.1.1's own BOM manages — confirmed by inspecting the resolved 4.1.1 jar with javap, not assumed from docs. It ships first in the 4.2 milestone train, so this post pins that one artifact to 4.2.0-M2 on top of an otherwise-ordinary Boot 4.1.1 project. That override is the actual subject of this post, not an oversight. This demo is small enough that it doesn't warrant its own repository module. Every source file is linked for download right where it's discussed below, and the full set is also uploaded to this site's media library if you'd rather grab everything at once.

Full-Text Search with Spring Data Elasticsearch on Spring Boot 4

Spring Data Elasticsearch's ElasticsearchRepository looks exactly like JpaRepository — derived query methods, @Id, the same muscle memory. That similarity hides the one decision that actually matters in Elasticsearch: how each field is mapped. Map a field "text" and it's tokenized, stemmed and fuzzy-matchable; map it "keyword" and it's an exact, unanalyzed string good for filtering, sorting and aggregating. Pick wrong and nothing throws an error — the field just quietly stops doing what you expect. This post builds a small product-search service on Spring Boot 4.1 and Elasticsearch 9.4 — a repository for the simple lookups, and the query DSL for everything a repository method can't express: multi-field relevance scoring with per-field boosting, typo-tolerant fuzzy matching, a price filter that narrows results without touching their rank, and a terms aggregation for category facet counts. Versions used in this post. Spring Boot 4.1.1 · Spring Data Elasticsearch 6.1.1 · elasticsearch-java client 9.4.5 · Elasticsearch server 9.4.8 · JDK 25 (Temurin) · Testcontainers 2.0.5 (testcontainers-elasticsearch). The server version is pinned to match the client's minor version — confirmed via mvn dependency:tree rather than assumed, after discovering elsewhere in this series that a mismatched tag can be silently broken upstream.

Spring Boot 4 with MongoDB: Transactions, Aggregations and Indexes That Actually Work

Spring Data MongoDB makes a document database feel so much like a relational one — repositories, derived query methods, @Id — that it's easy to forget the two places where the resemblance stops being helpful: transactions and indexes. Both are opt-in, both fail silently when you skip them, and both are exactly the kind of thing a tutorial glosses over because the happy path doesn't need them. This post builds a small order-placement service on Spring Boot 4.1 and MongoDB 9.0 — repositories for the simple lookups, an aggregation pipeline for a report no repository method can express, a multi-document transaction across two collections, and a compound index whose absence is provable with explain(), not just asserted in prose. Versions used in this post. Spring Boot 4.1.1 · Spring Data MongoDB (via spring-data-bom 2026.0.1) · MongoDB Java driver 5.8.1 · MongoDB server 9.0.2 · JDK 25 (Temurin) · Testcontainers 2.0.5 (testcontainers-mongodb). Run against a single-node MongoDB replica set in Docker — Testcontainers' MongoDBContainer configures replica-set mode automatically on this version; there is no .withReplicaSet() call to make, unlike what older tutorials show.

Time-Series Data in Java: QuestDB vs InfluxDB vs TimescaleDB

The same 100,000-row sensor workload, ingested and downsampled identically on QuestDB, InfluxDB 3 Core, and TimescaleDB — with real benchmark numbers and a real Arrow Flight / Netty bug in the official InfluxDB 3 Java client, reproduced and fixed.

Java on the GPU with TornadoVM 7: When It Beats the Vector API

How TornadoVM 7.2.0 task graphs and @Parallel loops work, with a measured plain-Java versus Vector API matrix-multiply baseline (up to 43x over the naive loop). Honest about the gap: the TornadoVM code compiles but could not be run without a GPU, so there are no TornadoVM timings.