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 — 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 @Transactional service methods,
│ 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).MovieViewis aRef-backed entity;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/) — Spring's declarative@Transactionalat the service level. Storm's Spring integration binds repository operations to the active transaction, so the writes on a request either all commit or all roll back. The two operations with a non-trivial boundary (the importer and the gallery service) use Spring's programmaticTransactionTemplate. - 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 — no Docker
required. Tests receive an ORMTemplate and a SqlCapture as parameters, so
they can assert on the SQL Storm generates.
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).
- Storm documentation: https://orm.st