An example movie browser built with Storm ORM on Spring Boot 4 and Java 21. It imports the public IMDB dataset into PostgreSQL and serves a server-rendered web app (Thymeleaf + a little vanilla JS) for browsing movies, people, genres, ratings, and a watchlist.
The project exists to show what idiomatic Storm looks like in a real Spring Boot application: immutable record entities, metamodel-based queries, Spring-managed transactions, and schema validation. No JPA, no proxies, no persistence context.
- Java 21, Spring Boot 4.1 (WebMVC + Thymeleaf, virtual threads enabled)
- Storm ORM (
storm-spring-boot-starter+storm-java21) with the annotation-processor metamodel generator andRAWSQL string templates - PostgreSQL 17 (Docker Compose) with Flyway migrations
- Jackson (
storm-jackson3) for the JSON APIs and cache values, including Storm'sRefserialization - JUnit 5 +
storm-teston H2 for repository tests, Playwright for interface tests
Storm's Java API builds SQL with JDK String Templates (JEP 430), a
preview feature that shipped in Java 21 and 22 and was then withdrawn. The
project therefore pins the toolchain to Java 21 specifically and enables
preview features everywhere: --enable-preview is wired into every
JavaCompile task, every Test task, and bootRun in build.gradle.kts. The
app must be built and run on a Java 21 JDK; later JDKs no longer have the
feature. The RAW."... \{expression} ..." syntax you see in the repositories is
that preview feature in action; the interpolated \{...} values are separated
from the SQL fragments at compile time, so the templates are injection-safe by
construction.
String templates were withdrawn for a redesign, not abandoned: Project Amber is reworking the feature, and a revised proposal is expected to return to the JDK. Storm deliberately ships String Template support today rather than waiting: the Java API is production-ready, and its template syntax will track the redesigned feature as it lands. Once string templates return to the JDK as a stable feature, the Java API moves front and center alongside Kotlin, without preview flags or a version pin. Everything else in this example (entities, repositories, the metamodel, transactions) is stable Java and unaffected by the preview status.
Prerequisites: JDK 21 and Docker.
# 1. Start PostgreSQL
docker compose up -d
# 2. Start the application
./gradlew bootRun
# 3. Open the app
open http://localhost:8080On first startup the app runs the Flyway migration and imports the IMDB
dataset: movies with at least 1,000 votes (configurable via
imdb.import.minimum-vote-count), plus their genres, cast, crew, and ratings.
The dataset files (~1.2 GB) are downloaded once and cached in ./data, then
streamed through Storm's batch inserts, so expect the first startup to take a
few minutes. The import is skipped entirely on subsequent startups once movie data
is present.
To start over with an empty database:
docker compose down -vMovie posters, person photos, and plot summaries are fetched at runtime from the IMDB suggestion API and the Wikipedia REST API, so the app looks best with internet access.
src/main/java/st/orm/demo/imdb/
├── model/ Storm entities (@PK, @FK) and projections, as records
├── repository/ EntityRepository interfaces with QueryBuilder queries
├── service/ Business logic in Storm transaction(...) blocks,
│ plus the streaming IMDB importer
├── web/ MVC controllers (pages) and REST controllers (/api/**)
└── serialization/ Jackson support: custom serializers and the
JSON-serialized Spring cache
src/main/resources/
├── db/migration/ Flyway schema (V1__create_schema.sql)
├── templates/ Thymeleaf views
└── static/ CSS, JS, images
Each part of the app demonstrates a Storm feature:
- Entities (
model/): immutable records with@PK,@FK,@UK, and composite keys (MovieGenre,Principal).MovieViewandPersonGalleryhold their foreign keys asRef, so writing one never joins the referenced graph;MovieSummary/PersonSummaryare database-view-style projections that select a subset of columns. - Repositories (
repository/):EntityRepositoryinterfaces with default methods using the type-safe QueryBuilder and generated metamodel (Movie_.startYear,Principal_.person). Aggregations return plain records; computed expressions useRAWSQL string templates with metamodel references. - Transactions (
service/): Storm's programmatictransaction(...)API at the service level, withTransactionOptionsfor the read-only request boundaries.MovieService.viewMovieis the one declarative@Transactionalexample: both run through the same Spring transaction manager, so they compose freely. The gallery service keeps Spring's programmaticTransactionTemplatefor its split fetch/store boundary. - Observability (
application.yaml): every query and transaction is reported as a Micrometer Observation (storm.query,storm.transaction), surfaced by Actuator at/actuator/metrics/storm.query. Query observations follow the OpenTelemetry database semantic conventions, and the trace context rides along as a SQL comment on every statement. - Streaming import (
service/ImdbDataImporter.java): aStream-based pipeline that parses TSV rows into entities and hands them to Storm's batch insert, one pass per file, without materializing entity lists. - Schema validation: on by default. The starter verifies every entity
against the live database schema at startup;
EntitySchemaValidationTestdoes the same in the test suite. - Serialization (
serialization/,web/API models): Storm entities serialized with Jackson for the REST endpoints (Reffields viastorm-jackson3,BigDecimalandInstantvia field serializers), and a Spring cache that stores values as serialized JSON to prove entities survive the round-trip (CacheConfiguration.java).
./gradlew testRepository tests run on an in-memory H2 database via @StormTest. Tests receive
an ORMTemplate and a SqlCapture as parameters, so they can assert on the SQL
Storm generates. EntitySchemaValidationTest runs on PostgreSQL instead, through
@StormTest(database = POSTGRESQL): Storm starts a Testcontainers-managed
PostgreSQL once per test run and applies the Flyway migration to it, so the
entities are validated against the schema and dialect the application deploys
with. That one test needs Docker, like running the application does.
The Playwright interface tests run against a live application:
./gradlew installPlaywrightBrowsers # once
./gradlew bootRun # in one terminal
./gradlew e2eTest # in anotherEverything lives in src/main/resources/application.yaml. The defaults match
the Compose file (database imdb, user/password storm on localhost:5432).
Import behavior is tunable under imdb.import (cache directory, minimum vote
count, dataset base URL).