Example: Flashcard

The example app builds the same features as ch07-jdbc-plus from spring-jdbc-book without Spring Boot: decks, cards, tags, spaced-repetition study, smart decks, CSV import and export, and statistics.

DB

Repositories use NamedParameterJdbcTemplate directly. The executed SQL is visible in the code. Inserts are the exception: SimpleJdbcInsert reads the table metadata and returns the generated key, with SimplePropertySqlParameterSource taking the values straight off the domain record. SmartDeckRepository still spells its parameters out, because its SmartCondition enum has to reach the driver as name() rather than as an enum. Row mapping goes through DataClassRowMapper. The reflection this pair needs under a native image is a config the build generates from the domain package.

DI

There is no container. FlashcardContext wires everything by calling constructors directly, playing the role of Spring’s ApplicationContext by hand.

Transactions

Instead of AOP, services call Transactions.write()/read(), a thin wrapper around TransactionTemplate. The wrapped block is exactly the transaction scope.

JSON API

As a bonus, a JSON API (/api/decks, /api/decks/{id}/cards) sits on the same service layer to show the framework’s REST support. Its wire format is in flashcard.web.Codecs as hand-written JsonWriter/JsonReader lambdas.

Introspection

/_routes lists every route, and /openapi.json is the same list as an OpenAPI 3.1 document. Both are built from app.routes(), which is what a routing table exposed as data buys. The document goes through spider-silk-openapi, and the app decides what it covers: the /api routes, since everything else here answers with HTML. Those are also the only routes registered with a description, which the page shows in a column of its own and the document as each operation’s summary.

Every response

FlashcardApp.createApp names all three of the response-wide concerns, which is what they look like on a real application. securityHeaders(…​) takes the defaults plus a policy this app can run under: default-src 'self' covers everything it loads, and style-src also allows inline because stats.jte sizes its chart bars with a style attribute, which no stylesheet can do. HSTS is deliberately absent, since main serves this over http://localhost. gzip() covers the pages, the stylesheet, the JSON, and the CSV export alike. cors(…​) is scoped to /openapi.json alone, and not to /api/decks, which creates decks. That document is the one thing here meant to be read by something that is not this application, such as a Swagger UI served elsewhere.

Routing

There is no Controller interface. FlashcardApp.registerRoutes is the whole table, and handlers arrive as an Action class, a public method reference, or a lambda.

Run

gradle :example-flashcard:run                # precompiled templates
gradle :example-flashcard:run --args=--dev   # hot-reloads edited .jte files
# http://localhost:8080

The two commands are jte’s production and development modes: the first renders the template classes the build compiled, the second recompiles a template whose file changed on its next render. The database is an H2 file (~/db/spider-silk/flashcard), so data survives restarts.

gradle :example-flashcard:nativeCompile builds the same app as a GraalVM native image.

Tests

gradle test

Repository and service tests run against in-memory H2 without mocking. The rollback test in DeckServiceTest shows the TransactionTemplate boundary actually working.