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 that DataClassRowMapper and SimplePropertySqlParameterSource need under a native image comes from a config the build generates from the domain package. FlashcardDatabase creates the H2 DataSource and applies schema.sql to it before handing it out. Its file() serves the running app, and its inMemory() gives each test a database of its own.

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

Beyond the features of ch07-jdbc-plus, 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 and as two generated codecs: the NewDeck body record carries @JsonBound, and the domain record DeckSummary is bound through a mixin in the same class.

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 exposes the routing table as data. 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 /_routes shows in a column of its own and /openapi.json carries as each operation’s summary.

Every response

FlashcardContext.createApp configures all three of the response-wide concerns, as a real application would. 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 the app 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 only resource here meant for a client other than this application, such as a Swagger UI served elsewhere.

Logging

The application logs through the slf4j API, with Logback as the backend and src/main/resources/logback.xml as its configuration. FlashcardContext.createApp registers a request logger that writes one line per request to the logger flashcard.access. Jetty’s lines and the framework’s servlet log follow the same configuration, and so do spring-jdbc’s, since its commons-logging finds slf4j on the classpath. The tests run under logback-test.xml, which keeps the console to warnings and errors. Logging shows the same setup on Log4j and on JDK logging.

Routing

There is no Controller interface. FlashcardContext.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. FlashcardApp turns --dev into the devMode flag of FlashcardContext, and FlashcardContext picks the TemplateRenderer from that flag. The database is the H2 file FlashcardDatabase.file() opens (~/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 the in-memory H2 that FlashcardDatabase.inMemory() creates, without mocking. The rollback test in DeckServiceTest shows the TransactionTemplate boundary actually working.