AOT-first · Java 21 · Apache-2.0

Fails at javac.
Not at 3 a.m.

Lynxus is a pragmatic MyBatis alternative. Your SQL is validated at compile time, your mappers become ordinary Java classes, and runtime is one explicit JDBC contract.

MAVENio.github.lynxus:lynxus-spring-boot-starter
✓ No runtime XML✓ No OGNL✓ No JDK proxies
lynxus-example0 problems
package com.example.user;import io.github.lynxus.annotation.*;@Mapperpublic interface UserMapper {  @Select("SELECT id, name FROM users WHERE id = #{userIdid}")  User findById(@Param("id") Long id);  @Insert("""      INSERT INTO users (id, name)      VALUES (#{id}, #{name})""")  int insert(@Param("id") Long id, @Param("name") String name);}
ready · annotation processor enabled1 / 6

A mistyped parameter is a build error, not a production incident.

JAVA 21/ANNOTATION PROCESSING/EXPLICIT JDBC/SPRING BOOT/GRAALVM NATIVE/APACHE-2.0

01 How it works

Four moves. No runtime ceremony.

The mapper stays the source of truth. Everything else — validation, implementations, the mapper index — is produced by javac.

UserMapper.javasource
package com.example.user;@Mapperpublic interface UserMapper {  @Select("SELECT id, name FROM users WHERE id = #{userIdid}")  User findById(@Param("id") Long id);  @Insert("INSERT INTO users (id, name) VALUES (#{id}, #{name})")  int insert(@Param("id") Long id, @Param("name") String name);}

SQL sits next to the method that uses it — no XML file to hunt down, no namespace to keep in sync.

02 Side by side

Same query. One fails before your app starts.

The migration is mostly deletion: the XML goes away, the runtime parser goes away, and the mapper you already wrote stays.

BEFORE

MyBatis

runtime work
<!-- resources/mapper/UserMapper.xml --><mapper namespace="com.example.UserMapper">  <select id="findById" resultType="User">    SELECT id, name FROM users    <where>      <if test="id != null">AND id = #{id}</if>    </where>  </select></mapper>
2 files1 XML parse1 proxy
AFTER

Lynxus

compile-time
@Mapperpublic interface UserMapper {  @Select("SELECT id, name FROM users WHERE id = #{id}")  User findById(@Param("id") Long id);}
1 file0 XML parse0 proxies

Same SQL. Same result. One of them fails before your app starts.

03 What is not there

Less machinery. Fewer surprises.

No parser to boot, no expression language to debug, no proxies to explain in a stack trace. Startup reads a generated index; every call is a prepared statement and a row mapper you can open in your IDE.

AOT & Native Image guide
0XML files parsed at startup
0OGNL expressions evaluated
0JDK proxies per mapper
1executor contract to implement

04 Get started

Point javac at your SQL.

Java 21, one dependency, and a mapper you already know how to write.

MAVENio.github.lynxus:lynxus-spring-boot-starter