The Scope of "No Reflection"
The principle applies to the framework core: routing, parameter extraction, and JSON handling use no reflection anywhere.
The default template engine sits right at the boundary, so its scope is worth stating exactly.
jte compiles every template to a Java class, so ${deck.title} runs as a compiled method call: the model is never reflected over, and a typo in that expression is a compile error rather than a silently empty value.
What remains is loading: the engine looks the generated class up by name and invokes its render method reflectively — reflection confined to classes the build generated, never reaching a class you wrote.
That confinement is what keeps the native image story mechanical: jte’s own build extension writes the reflection config for exactly those generated classes, and neither a model class nor a framework class needs an entry.
A template engine in a module of its own answers for itself: Handlebars falls back to reflection for a record or a bean, FreeMarker reads one through its object wrapper, and Thymeleaf evaluates every expression through OGNL. That is one more reason each lives outside core — take the module and you have taken its reflection with it, and an app that stays on the default jte keeps its model access reflection-free.
The example project’s repositories choose spring-jdbc’s DataClassRowMapper, which constructs the domain records reflectively — the example’s choice, not the framework’s, and deliberately kept to show where the boundary runs.
Under a native image that choice is paid for with a reflection config confined to the one domain package, generated by the build; core contributes not a single entry to it.