An example movie browser built with Storm ORM on Spring Boot 4 and Kotlin. 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 data-class entities, metamodel-based queries, coroutine-native transactions, and schema validation. No JPA, no proxies, no persistence context.
- Kotlin 2.2 / Java 21, Spring Boot 4.1 (WebMVC + Thymeleaf, virtual threads enabled)
- Storm ORM (
storm-kotlin-spring-boot-starter) with the KSP metamodel generator and the Storm compiler plugin - PostgreSQL 17 (Docker Compose) with Flyway migrations
- kotlinx.serialization for JSON APIs and cache values; Jackson for parsing external APIs
- JUnit 5 +
storm-teston H2 for repository tests, Playwright for interface tests
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 suspending 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/kotlin/st/orm/demo/imdb/
├── model/ Storm entities (@PK, @FK) and projections
├── repository/ EntityRepository interfaces with QueryBuilder queries
├── service/ Business logic in suspend `transaction { }` blocks,
│ plus the streaming IMDB importer
├── web/ MVC controllers (pages) and REST controllers (/api/**)
└── serialization/ kotlinx.serialization 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 data classes with@PK,@FK,@UK, and composite keys (MovieGenre,Principal).MovieViewis a database-view-backed projection;MovieSummary/PersonSummaryselect 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 data classes; computed expressions use SQL template lambdas with metamodel references. - Transactions (
service/): Storm's coroutine-native suspendtransaction { }blocks at the service level, bridged withrunBlockingonly at MVC entry points. The Spring transaction integration is deliberately excluded inapplication.yamlbecause suspend mode manages transactions on theDataSourcedirectly. - Streaming import (
service/ImdbDataImporter.kt): Flow-based pipeline that parses TSV rows into entities and hands them to Storm's suspending 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/ApiModels.kt): Storm entities serialized with kotlinx.serialization for the REST endpoints, and a Spring cache that stores values as serialized JSON to prove entities survive the round-trip (CacheConfiguration.kt).
./gradlew testRepository tests run on an in-memory H2 database via @StormTest, so no
Docker is 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).