This page describes what happens when the application calls authors.findByName("Sylvie Germain"): from the CDI proxy down to the pool, through JDQL → SQL translation and ResultSet → entity materialization. It is also the right place to understand why Mansart favors static codegen.

Overview — repository method call

Diagram

The whole hot path is reflection-free: query plans are prepared once and cached, parameter/attribute binding goes through MethodHandle cached once.

Codegen — APT (compile time)

mansart-data-processor is a javax.annotation.processing.Processor that scans compiled CUs:

  1. For each class annotated @Entity (Mansart or JPA) → generates Book.java (rich metamodel) and Book.java (JPA standard metamodel).

  2. For each interface annotated @Repository → generates BookRepositoryImpl.java (final public implementation class) + an entry in META-INF/mansart-repositories.list.

Source code generated under target/generated-sources/annotations/, marked @Generated("io.vidocq.mansart.data.processor.MansartProcessor"). The processor has no runtime dependency — it lives only in the consuming module’s APT classpath.

Diagram

Why APT and not runtime codegen?

  1. AOT-friendly — GraalVM native-image, Leyden CDS: no proxy to register, everything is in the bytecode at build time.

  2. Diagnosable — generated code is readable, debuggable, visible in stack traces.

  3. Zero warmup — no dynamic classloading on first call.

  4. strict Java Modules-friendly — generated classes are in the same package as sources, no need for opens.

The Connection wrapper (hand-written)

mansart-pool ships a plain hand-written PooledConnection implements Connection: every method delegates to the physical connection, and close() returns the connection to the pool instead of closing it.

Why hand-written rather than java.lang.reflect.Proxy:

  • Connection has 50+ methods — a reflective proxy costs an invoke per call; direct delegation is JIT-friendly from the first call.

  • No bytecode generation and no ASM / Byte Buddy dependency — matches the ecosystem’s "zero external dependency" rule.

Pool acquire / release

Diagram
  • Semaphore permits — hard pool bound, fair=false (perf).

  • ConcurrentLinkedDeque idle — lock-free stack, push/pop at the head (hot LIFO).

  • ConcurrentHashMap.newKeySet inUse — set of borrowed connections, for metrics and leak detection.

  • Housekeeper virtual thread — periodic wake-up, evicts connections that are too old or idle, pre-warms up to minIdle.

Details and numbers: see mansart-pool BENCH.md.

Threading — virtual threads

  • All blocking JDBC executions (PreparedStatement.execute*, ResultSet.next) run on virtual threads. No platform pool.

  • The JDBC connection is never held across a synchronized block that wraps a blocking operation (anti-pattern: thread pinning).

  • The transactional context lives in a ThreadLocal on the transaction’s thread (see mansart-transactions); it does not auto-propagate into StructuredTaskScope forks — keep transactional work on the transaction’s thread.

  • mansart-pool validates in CI with -Djdk.tracePinnedThreads=full that no pinning appears under 1000 parallel borrows.

ResultSet → entity mapping

No reflection. The _Book metamodel captures, in <clinit>, one MethodHandle setter per attribute, obtained via MethodHandles.privateLookupIn(Book.class, lookup). For each row:

Book row = constructor.invoke();           // MethodHandle constructor
_Book.title.setter.invokeExact(row, rs.getString(1));
_Book.author.setter.invokeExact(row, refLoader.get(rs.getLong(2)));
// ...

invokeExact on a MethodHandle is inlined by C2 — perf equivalent to direct field access after warmup.

Transactions — propagation and XA

  • Single-resource (the dominant case) — the @Transactional interceptor opens/closes the transaction. The JDBC connection is enlisted as a degenerate XAResource (onePhase=true).

  • Multi-resource (M4) — true 2PC: prepare then commit on each enlisted XAResource. Recovery log (M5) records PREPARED transactions that did not complete, for recovery on reboot.

  • Suspend / resume — TransactionManager.suspend() removes the context from the current thread and returns it; resume(t) reinstates it. Typical batch case: a worker job suspending the caller’s TX to issue a clean INSERT/COMMIT.

See mansart-transactions PLAN.md for the M1-M8 roadmap.