Replacing optimized Spring Boot with Javalin and Jdbi saved 27.8 MiB of median application RSS (14.9%) in this small database-backed service. Readiness fell from 3.79 to 2.72 seconds, while HTTP p95 stayed similar at five requests per second. These are single-run observations of complete application stacks.
The saving comes with more application-owned wiring: repositories, transaction boundaries, scheduling, health responses and metrics. The useful question is how much memory that trade buys, and which integrations the application must maintain.
This follows the Spring Boot optimization experiment, which combined extracted deployment, Spring AOT and a trained JDK AOT cache. Here, optimized Spring Boot is the baseline. A separate comparison below covers both stacks as Oracle Native Image executables on JDK 25.
Replacing Boot means taking ownership of its integrations
Market Replay polls a deterministic HTTP quote feed once a minute, stores three quotes atomically, and serves latest-price and history JSON endpoints plus a browser UI. Spring Boot supplies MVC/Tomcat, Spring JDBC, transaction integration, Actuator/Micrometer and application wiring. This baseline already uses SQL directly, without an ORM.
Javalin supplies the HTTP layer and Jetty integration. It does not supply the repository and transaction model used here. The application wires its JSON mapper, scheduler, pool, repositories, health responses and metrics. To preserve useful behavior, it needs more than routes calling ad hoc JDBC code: connection lifetimes must be bounded, failures must roll back writes, and service operations must be able to coordinate repositories.
Javalin applications can organize handlers into controller-style classes, but Javalin does not provide Spring's integrated annotation-based MVC stack with its controller, request-binding, validation and error-handling facilities. This application uses direct route registration; the remaining conventions require explicit application code or additional libraries. Preserving endpoint behavior therefore does not mean preserving Spring's development experience.
Repositories and transactions with Jdbi
Jdbi already provides SQL execution, parameter binding, mapping, managed handles and transaction callbacks. Its managed callbacks can share a handle across repository calls. The experimental adapter adds a repository base and context, convenient query callbacks, a service-level transaction callback and explicit rollback-only marking. It delegates transaction lifecycle to Jdbi rather than implementing a separate transaction system. Plain Jdbi is also a valid choice; the extra layer is a convenience and an API to maintain, not a prerequisite for transactions. The persistence source and focused tests show the experiment-era core and measured quote repository.
In the example, db is the experimental adapter around Jdbi. Its transactional and useHandle methods are adapter APIs; createUpdate, bind and execute are Jdbi calls.
In the measured application, QuoteRepository.insert places the three quote inserts inside a repository-scoped transaction. The implementation uses a managed handle and Jdbi's named bindings for each insert:
db.transactional(() -> {
for (FixtureQuote quote : batch.quotes()) {
db.useHandle(handle -> handle.createUpdate(INSERT)
.bind("fetchedAt", Timestamp.from(fetchedAt))
// Other batch fields are bound here.
.bind("symbol", quote.symbol())
.bind("price", quote.price())
.execute());
}
});
Jdbi manages the transaction; the prototype provides the explicit callback and handle conventions. A failed insert escaping the callback rolls back the batch. Reads use db.query and an explicit row mapper. This is the measured ingestion boundary, rather than a service coordinating several repositories.
For a service that needs several repositories to participate in one operation, the same prototype also provides an explicit service-level callback. Participating calls must use the intended managed scope on the same thread and Jdbi instance. An exception escaping the boundary triggers rollback; a caught exception does not automatically mark the transaction rollback-only, although the prototype provides an explicit marker. These are focused callback semantics, not a claim of Spring transaction-propagation compatibility. The prototype is experimental, and its resource cost was not measured separately.
Comparing complete application stacks on HotSpot JDK 27
The HotSpot comparison retains the same application behavior, quote SQL, UI, local MariaDB, heap policy and request schedule. Spring F is the optimized deployment from the previous article, not the slower ordinary JAR.
The result: 27.8 MiB (14.9%) less median application RSS, readiness of 2.72 versus 3.79 seconds, and nearly identical HTTP p95 at five requests per second.
| Component | Optimized Spring Boot F | Javalin + repository prototype |
|---|---|---|
| Runtime | HotSpot 27+35-2325, G1, -Xmx80m | Same |
| HTTP stack | Boot 4.1.1, Framework 7.0.9, Tomcat 11.0.24 | Javalin 7.2.4, Jetty 12.1.14 |
| Persistence | Spring JDBC | Jdbi Core 3.55.0, java-repository-poc 0.1.0-SNAPSHOT |
| Pool | HikariCP 7.0.2, minimum/maximum 1/4 | Same |
| JDBC driver | MariaDB Connector/J 3.5.10 | Same |
| Deployment optimization | Extracted layout, Spring AOT, JDK AOT cache | Thin JAR plus dependency JARs, JDK AOT cache |
| Metrics | Actuator/Micrometer Prometheus | Application JSON metrics |
The public Javalin environment metadata and Spring method record the deployment inputs. Boot 4.1.1's tagged support range ends at Java 26; these are empirical JDK 27 compatibility observations.
Both applications shared a one-vCPU, 512 MiB Ubuntu VM with local MariaDB and no swap. Readiness required health UP and three stored quotes. After idle and warm-up phases, the driver offered five reads per second for five minutes, with at most four requests in flight and no retries.
Measurement protocol and cache training
Both applications ran in the same one-vCPU, 512 MiB Ubuntu 24.04 x86_64 local VM budget, without swap. Application, MariaDB and the deterministic Python quote fixture shared that budget. MariaDB used a 64 MiB InnoDB buffer pool, 8 MiB key buffer and 16 MiB Aria cache. These settings are not a limit on database RSS.
Each accepted run followed a separate boot, database restart and empty-schema setup outside startup timing. Setup warmed storage. Readiness required health UP and three persisted quotes, polled about every 50 ms. Then came 60 seconds idle, 120 seconds warm-up, 300 seconds measured latest/history reads and 60 seconds recovery. Warm-up and load offered five requests per second, at most four in flight, with no retries. The scheduled ingestion ran alongside the reads.
The external HTTP driver used SSH forwarding. OS sampling was approximately once per second, with transition samples excluded from steady summaries. Metrics were scraped every ten seconds. Javalin's startup-only cache training ran on a separate builder and exited before opening a database connection; it did not train request paths. Actual cache acceptance was checked. Neither primary run started StatLite or periodic DB-health probes. These details are in the published Javalin method.
| Observation | Spring Boot F, HotSpot 27 | Javalin + Jdbi prototype, HotSpot 27 |
|---|---|---|
| HTTP and persistence readiness | 3.79 s | 2.72 s |
| Idle application RSS, median | 178.7 MiB | 145.0 MiB |
| Workload application RSS, median / sampled peak | 186.9 / 189.3 MiB | 159.1 / 160.1 MiB |
| Workload MariaDB RSS, median | 70.6 MiB | 73.3 MiB |
| Workload fixture RSS, median | 19.1 MiB | 21.4 MiB |
| Workload host MemAvailable, minimum | 66.0 MiB | 93.2 MiB |
| Workload HTTP p50 / p95 | 3.55 / 5.54 ms | 3.98 / 5.59 ms |
| Workload sampled application CPU | 6.28 s | 5.85 s |
| Successful workload requests / errors | 1,500 / 0 | 1,500 / 0 |
| Observed OOM / swap | None / none | None / none |
Sources: Javalin comparison and calculated differences and the canonical Spring F record at /runs/F.
Both ingested ten ordered batches and completed every offered workload request. These are single runs: Spring F was measured October 23 and Javalin October 28 UTC, on separate boots where package state and host activity could differ. The CPU difference is small in absolute terms. Javalin logged a timekeeping-watchdog delay and skipped clock-skew check; the cause and timing effect are unknown. Successful request and sampling checks do not certify clock accuracy.
Five achieved requests per second is a property of the offered workload, not a throughput ceiling. Nor can the application RSS difference be assigned entirely to the HTTP framework or persistence layer. Tomcat versus Jetty, Spring JDBC versus Jdbi, logging, JSON libraries, training coverage and metrics implementation all contribute to the complete stacks. There is no Javalin-without-the-wrapper measurement isolating the prototype's overhead.
Database-server memory is reported separately from Java RSS. Pool objects, connections and driver state contribute to application memory, but their share was not isolated. Both stacks use the same pool settings, so this is not evidence that Javalin has a cheaper pool. Host MemAvailable includes the database, fixture, kernel and other activity; its improvement cannot be derived simply by subtracting process medians or summing RSS.
Comparing Native Image deployments on Oracle GraalVM 25
Both applications also have validated native executables built with Oracle GraalVM 25.0.4+7.1, Java build 25.0.4+7-LTS-jvmci-b01, using Serial GC, x86-64-v3 and a runtime -Xmx80m guard. These native results use JDK 25; the HotSpot results above use JDK 27.
The native runs used the same VM budget, database settings and workload protocol. Each executable passed behavior checks before measurement, including transactions and recovery. Monitoring and periodic database-health probes stayed off.
Spring used production-default O2. Javalin's published study selected the quick-build Ob run for its lower observed app RSS and comparable performance; its separately measured production-default O2 run supplies the comparison at the same optimization level. Both are shown so the selection is visible.
At O2, Javalin used 38.8% less application RSS, while minimum host MemAvailable remained approximately 205 MiB for both stacks.
| Observation | Spring Boot native, O2 | Javalin native, O2 | Javalin native, selected Ob |
|---|---|---|---|
| HTTP and persistence readiness | 0.183 s | 0.113 s | 0.112 s |
| Idle application RSS, median | 106.8 MiB | 52.5 MiB | 46.7 MiB |
| Workload application RSS, median / sampled peak | 107.0 / 107.6 MiB | 65.5 / 65.7 MiB | 59.4 / 59.8 MiB |
| Workload MariaDB RSS, median | 71.6 MiB | 75.8 MiB | 84.0 MiB |
| Workload host MemAvailable, minimum | 204.9 MiB | 204.1 MiB | 202.7 MiB |
| Workload HTTP p50 / p95 | 3.46 / 4.84 ms | 2.76 / 4.34 ms | 2.88 / 4.03 ms |
| Workload sampled application CPU | 1.67 s | 1.48 s | 1.59 s |
| Successful workload requests / errors | 1,500 / 0 | 1,500 / 0 | 1,500 / 0 |
| Observed OOM / swap | None / none | None / none | None / none |
Sources: the Javalin native study and optimization comparison and the Spring Oracle native study.
At O2, Javalin used 41.5 MiB less median application RSS, about 38.8% below Spring native. That did not translate into equivalent additional host memory. Both O2 deployments retained approximately 205 MiB minimum host MemAvailable. File-backed mappings overlap page-cache accounting, and the guests also started with different background memory usage. Process RSS alone can therefore overstate the practical memory savings from switching application stacks.
The selected Javalin Ob run saved 47.6 MiB of application RSS, about 44.5%, but includes a different compiler setting. All three runs persisted ten ordered batches and completed every workload request. The small startup and latency differences from single runs do not establish a repeatable performance ranking or saturation capacity. Ob's result is not a general recommendation over production-default O2.
Native deployment also adds build and compatibility work. Spring's complete Maven build took about 11m 48s, with 5.0 GiB peak builder RSS and a 107 MiB executable plus six companion libraries. Javalin O2's native build took about 6m 22s externally, with 4.0 GiB peak builder RSS and a 54 MiB executable plus a small management library. Exact byte counts are in the linked experiment records. Both used the retained six-GiB/two-vCPU builder, separate from the runtime VM. These timings cover different build workflows: Spring's Maven lifecycle versus Javalin compilation from retained JARs and metadata. Javalin also capped the compiler heap at 4 GiB; Spring used its default compiler budget. These figures describe practical build costs; the workflows do not isolate framework or compiler build speed.
Native packaging required reachability metadata and static-resource inclusion for Javalin and a focused JSON-binding hint for Spring. The actual packages passed transaction, failure/recovery and persistence/restart checks; those checks validate the exercised paths, not every mapper, driver or database.
These native observations were collected on October 28 in separate VM boots. Spring native used kernel 6.8.0-147 and libc 2.39-0ubuntu8.9; the Javalin native capture records kernel 6.8.0-134. Javalin O2 logged a timekeeping-watchdog delay and skipped clock-skew check during load; its timing effect is unknown. Package state, metrics implementations and ordinary host variation remain limitations. The native and HotSpot tables compare practical deployments: changes from HotSpot 27/G1 to Native Image 25/Serial cannot be attributed to compilation alone, and equal heap guards do not equate memory layouts.
What the smaller stack gives up
The measured Javalin application preserves the quote behavior. The application-owned integrations described above must remain correct as requirements grow; the experiment does not establish Actuator-equivalent coverage. Schema migration, security integration and deployment conventions also need deliberate choices; the prototype does not provide a replacement for those parts of an application platform.
Sampled RSS peaks may miss spikes, readiness precision is bounded by polling, and the short recovery window establishes neither a leak nor long-term safety.
The public reanalysis guide explains what the curated release can verify: projected sample statistics, request outcomes, percentiles and consistency with accepted summaries. It is not a complete source-and-runner package for rebuilding the experiment. These are local VM observations under a modest workload, not predictions for every VPS or Java application.
A practical option, with a narrower contract
For this application, Javalin plus Jdbi and the repository prototype saved measurable application memory beyond optimized Spring Boot while retaining persistent SQL data, transactional ingestion and the same API/UI behavior. That makes it worth considering for a small service whose team wants explicit wiring and can own the missing integrations.
For an application that already depends on Spring's MVC, transaction facilities and operational ecosystem, the observed saving may be less valuable than keeping those integrations. The experiment does not measure development effort or prove that assembling the smaller stack reduces total cost.
Can we retain more of Spring Boot's development convenience without bringing back its full runtime footprint? That's the question I'll explore next by comparing five Java application stacks, including Micronaut and Jex.