Spring Boot provides familiar architectural conventions: controllers, services, repositories and transaction boundaries. I wanted to preserve the useful parts of that programming model while keeping business logic independent of Spring Boot or any particular HTTP framework. Spring Boot supplied the architectural influence; the goal was not to reproduce its APIs.
CoilBase emerged as a small, reusable foundation for repositories, services and explicit transactions. Three implementations reused the same CoilBase business logic across Javalin, Avaje Jex and Micronaut. Spring Boot and Micronaut Data served as independent references.
That led to two questions: How portable is the business logic, and what runtime costs do different frameworks introduce? To explore both, I compared all five implementations on a one-vCPU, 512 MiB VM with colocated MariaDB.
There is an important scope limit: this application is a synthetic quote/status/audit fixture. It reads latest quotes, inserts quotes, and updates a record’s status with an audit event. It has no Portfolio users, holdings, valuation or holding-edit behavior. It answers a narrow deployment and portability question; it cannot establish the requirements of a realistic Portfolio application or the capacity of these frameworks in general.
The five implementations
The reference uses Spring Boot, Spring MVC and Spring JDBC. The alternatives are Javalin + CoilBase, Avaje Jex + CoilBase, Micronaut Data JDBC, and Micronaut + CoilBase.
CoilBase is a PVR Labs project built around Jdbi, explicit SQL, constructor wiring and repository/service organization, currently available as 0.1.0-preview3. The experiment used an early CoilBase prototype; the API has since evolved, including making RepositoryContext an interface. Code excerpts here come from the measured version.
Spring JDBC is a useful reference because it keeps SQL explicit. Micronaut Data provides generated repositories and mapping. CoilBase adds a reusable persistence context and repository conveniences around Jdbi; Jdbi itself already provides portable SQL and transaction facilities. This experiment does not isolate the incremental value or overhead of CoilBase against direct Jdbi.
What stayed shared, and what changed
The same repositories, services and transaction boundaries work across Javalin, Avaje Jex and Micronaut. The application service has no HTTP-framework or DI imports. Each host supplies its own controllers, configuration, lifecycle and application composition. Javalin and Micronaut distributions were also checked for reuse of the same compiled shared artifact.
The service makes the status-change transaction explicit:
public void changeStatus(StatusChange change) {
change.validate();
db.transactional(() -> {
records.updateStatus(change.recordId(), change.status());
audit.insert(change.eventId(), change.recordId(), change.status());
});
}
The service owns the boundary around two writes. A conflicting audit event must roll back the earlier status update. A direct MariaDB test checks that a service call participates in an enclosing transaction on the same connection, uncommitted writes remain invisible to another connection, exceptions roll back the work, successful work commits, and owned connections close. The HTTP contract checks the externally visible behavior across the implementations.
This is synchronous, single-datasource transaction participation through explicit callbacks. It does not demonstrate independent nested transactions, propagation policies, distributed transactions or cross-thread handoff. It is not Spring transaction compatibility.
The hosts offer different programming models. Javalin uses explicit route registration and application wiring. Avaje Jex combines annotation-based controllers and generated routing adapters with explicit constructor wiring. Micronaut provides framework-managed dependency injection and serialization.
All three reuse the same business logic, repositories and transaction boundaries. Switching HTTP frameworks primarily changes the composition layer, dependency injection or constructor wiring, controllers and framework-specific configuration. The business logic can remain unchanged; controllers and HTTP integrations stay specific to their host. Each host still needs correct JSON responses, error classification, datasource configuration and resource cleanup.
The differences are practical. Jex’s empty-success response handling required an explicit writeEmpty(201) to preserve the insertion contract. Micronaut required generated metadata for shared records. Those details belong at the host boundary; sharing service code does not remove them.
The deployment being measured
The runtime VM had one vCPU, 512 MiB RAM and no swap. Application and MariaDB ran together, with the same recorded database policy, fixtures, pool bounds and request contract. HotSpot used OpenJDK 27 and G1. Each stack had two accepted runs.
A run contained 60 seconds idle, 120 seconds warm-up, 300 seconds measured workload and 60 seconds recovery. The measured workload sent about five requests per second: 1,200 latest reads, 150 quote inserts and 150 status/audit transactions. Each run completed all 1,500 measured requests without errors, timeouts or unsent arrivals. Independent SQL checks verified final state against the request trace.
RSS below is process resident memory, not Java heap. Each value is the median of samples within one workload run. The pair shows both accepted observations, rather than hiding variation in an average. MiB means 1,048,576 bytes.
HotSpot: fit and remaining headroom
HotSpot workload memory
Application RSS in MiB · median within each run · zero-based scale
| Stack | Workload app RSS, run 1 / 2 (MiB) | Database-backed readiness, run 1 / 2 (s) |
|---|---|---|
| Spring Boot + Spring JDBC | 180.6 / 187.9 | 7.425 / 8.653 |
| Javalin + CoilBase | 135.6 / 120.9 | 3.063 / 2.920 |
| Micronaut Data JDBC | 175.0 / 174.0 | 4.397 / 4.345 |
| Micronaut + CoilBase | 166.5 / 167.6 | 4.248 / 5.275 |
| Jex + CoilBase | 116.0 / 116.4 | 2.022 / 2.543 |
All five fit this small workload inside the combined VM budget. Jex had the lowest workload application RSS in both runs, but application RSS is only one part of the deployment. MariaDB workload RSS was approximately 76–79 MiB across the per-stack summaries; the operating system and other memory also count.
Spring’s minimum host MemAvailable during the workload was 69.2 and 63.1 MiB. Its minimum over all sampled phases reached 51.1 MiB in the second run. Jex’s first-run workload minimum was 138.5 MiB. Those phase windows must remain distinct: a startup minimum and a workload minimum are different observations.
That remaining memory matters when considering another process or a less predictable workload. These runs do not prove that any stack is safe indefinitely at this budget. Application CPU was roughly 1–2% of one core during the workload, so the comparison mostly describes baseline footprint under light traffic. It is not a saturation or maximum-throughput test.
Javalin’s two RSS observations differ by 14.7 MiB. Both are retained. Two runs provide descriptive observations, not confidence intervals, and the experiment does not identify the cause of that variation. Small latency differences also include the host and SSH tunnel and should not become framework rankings.
Native Image: smaller runtime, more preparation
Native Image workload memory
Application RSS in MiB · median within each run · zero-based scale
The native follow-up built and validated all five implementations, with two accepted runs each under the same fixture workload and VM budget.
| Stack | Native workload median RSS range (MiB) | Native readiness range (s) |
|---|---|---|
| Spring Boot + Spring JDBC | 84.0–85.0 | 0.172–0.273 |
| Javalin + CoilBase | 58.3–59.2 | 0.078–0.172 |
| Micronaut Data JDBC | 89.7–91.7 | 0.175–0.188 |
| Micronaut + CoilBase | 81.8–82.4 | 0.211–0.212 |
| Jex + CoilBase | 33.4–51.0 | 0.058–0.076 |
Each native run completed 1,500 workload requests without errors and passed independent database verification. Readiness means the measured database-backed readiness check, including polling granularity; it is not merely the time until a port opens.
These native deployments used less process RSS and reached readiness sooner than their recorded HotSpot counterparts. That is a whole-stack observation. Native used GraalVM JDK 25 and Serial GC, while HotSpot used JDK 27 and G1. Spring also used build-time AOT. The comparison cannot attribute the differences solely to compilation.
Jex varied from 33.4 to 51.0 MiB despite an unchanged executable and launch policy. Native GC telemetry was unavailable, so attributing that difference to a particular collection or retention event would be speculation. Close or overlapping observations should be treated as ties.
The compilation bill
Native compilation was a substantial downside on the two-vCPU, 6-GiB builder. Successful compiler-reported generation times with Oracle GraalVM 25.0.4+7.1 and -O2 were:
| Stack | Successful native generation |
|---|---|
| Jex + CoilBase | 6m27s |
| Javalin + CoilBase | 6m42s |
| Spring Boot + Spring JDBC | 10m04s |
| Micronaut + CoilBase | 12m16s |
| Micronaut Data JDBC | 13m03s |
These times exclude Maven packaging, tracing, tests, transfers and failed attempts. Jex first compiled for 6m11s, then failed JDBC startup; a metadata-enabled rebuild took another 6m27s. Micronaut + CoilBase had a failed 1m08s compilation before its native initialization fix. A successful-generation table understates the total time to reach a working package.
The builder also had considerably more memory and CPU than the runtime VM. Native can suit a small deployment without being convenient to build on that same server. Faster readiness and lower runtime memory come with longer iteration and compatibility work. These measurements establish neither universal framework build-time rankings nor a native-to-JVM build-time ratio.
Choosing the tradeoffs
The reusable service and repositories demonstrate a narrow point: familiar application structure can survive a move between HTTP hosts when the host boundary and transaction scope are explicit. The work shifts toward composition, lifecycle, dependency compatibility and HTTP integration. Spring and Micronaut supply conveniences that may be worth their measured footprint for a particular application.
For this light synthetic workload, all five deployments ran within the budget, and the lightweight hosts left more room in the HotSpot comparison. Native reduced runtime memory and readiness time at a noticeable build and preparation cost. These observations help frame a deployment choice; they do not replace testing the application that will actually run there.
For the CoilBase API and runnable examples, see the project documentation.
What's next?
The next step is a more formal, public Java framework benchmark project, with Quarkus and Helidon planned as additional frameworks. It will use a proper application contract, a pinned CoilBase preview version, explicit correctness verification and published results.
The aim is to reuse the same application behavior, business logic, SQL and transaction boundaries across additional frameworks, changing primarily the integration layer. Reducing application-level variability should make future comparisons more consistent and reproducible. Framework-specific differences in controllers, composition, configuration and lifecycle remain meaningful and will still be measured; sharing the application core does not make the deployments perfectly equivalent.
Curated evidence: accepted per-run summaries and verification script. This summary package does not include complete raw captures or runnable deployment sources.