skip to content

Compare @Query repositories, JdbcAggregateTemplate, and NamedParameterJdbcTemplate. When do you choose each, and what are the architectural trade-offs?

level: principalimportance: nice to knowfreq 25%

answer

  1. three layers: repository > JdbcAggregateTemplate > NamedParameterJdbcTemplate
  2. aggregate semantics + events only in top two
  3. raw JDBC = reports, bulk, DTO joins, no cascade
  4. template for insert/update control + assigned ids
  5. highest abstraction that fits; drop for a concrete reason

basics

~20 s

Use @Query repositories for declarative CRUD and hand SQL reads. Use JdbcAggregateTemplate for programmatic aggregate operations and explicit insert/update control. Use NamedParameterJdbcTemplate for raw SQL that has no aggregate mapping, like reports or bulk operations.

solid answer

~50 s

These three sit at descending levels of abstraction. @Query repository methods are the most declarative: derived queries plus hand-written SQL with automatic aggregate mapping or a RowMapper, minimal boilerplate, and repository-proxy transactions. JdbcAggregateTemplate (org.springframework.data.jdbc.core) is the programmatic aggregate API the repositories delegate to, use it when interfaces are too rigid: generic/dynamic persistence, batch importers, or explicit insert() vs update() control (for example client-assigned IDs where save() would wrongly UPDATE). It still cascades to owned entities and fires the same lifecycle callbacks/events. NamedParameterJdbcTemplate (org.springframework.jdbc.core.namedparam) is raw Spring JDBC: no aggregate awareness, no events, you hand-map with a RowMapper; ideal for reporting queries, complex joins into DTOs, and set-based bulk statements. Trade-off: higher levels reduce boilerplate and keep aggregate invariants; lower levels give control and performance at the cost of manual mapping and losing the object-graph semantics.

code

java · 27 lines
java
@Service
public class OrderService {

    private final OrderRepository repo;                 // declarative CRUD + @Query
    private final JdbcAggregateTemplate aggregate;      // programmatic aggregate ops
    private final NamedParameterJdbcTemplate jdbc;      // raw SQL for reports/bulk

    public OrderService(OrderRepository repo,
                        JdbcAggregateTemplate aggregate,
                        NamedParameterJdbcTemplate jdbc) {
        this.repo = repo; this.aggregate = aggregate; this.jdbc = jdbc;
    }

    Order create(Order withAssignedId) {
        return aggregate.insert(withAssignedId);        // force INSERT, keep cascade+events
    }

    Optional<Order> get(Long id) { return repo.findById(id); }

    List<Revenue> monthlyRevenue() {                    // reporting: no aggregate mapping
        return jdbc.query(
            "SELECT date_trunc('month', created_at) AS m, sum(total) AS revenue " +
            "FROM orders GROUP BY 1 ORDER BY 1",
            (rs, i) -> new Revenue(rs.getObject("m", java.time.LocalDate.class),
                                   rs.getBigDecimal("revenue")));
    }
}

go deeper

for a junior

Know repositories are the default; templates exist for lower-level needs.

for a middle

Distinguish aggregate-aware JdbcAggregateTemplate from raw NamedParameterJdbcTemplate.

for a senior

Justify choosing each and name what raw JDBC bypasses (cascade, version, events).

for a principal

Reason about aggregate-integrity boundaries, performance of collection replacement vs set-based SQL, and a deliberate layered-persistence strategy across a codebase.

## The three layers 1. **`@Query` / derived repository methods** — declarative interface methods on `CrudRepository`/`ListCrudRepository`. Derived methods generate SQL from method names; `@Query` lets you drop to hand-written SQL. Results map to aggregates automatically (via `EntityRowMapper`) or to your type via a `RowMapper`. The repository proxy adds transactional semantics and routes through the aggregate engine. 2. **`JdbcAggregateTemplate`** (`org.springframework.data.jdbc.core`) — the **programmatic, aggregate-aware** API (`insert`, `update`, `save`, `delete`, `findById`, `findAll`, `count`). It is exactly what `SimpleJdbcRepository` delegates to, so it preserves **aggregate semantics** (cascade to owned children, replace owned collections on update) and fires the same **lifecycle callbacks/events** (`BeforeConvertCallback`, `BeforeSaveCallback`, `AfterSaveCallback`, etc.). Auto-configured as an injectable bean. 3. **`NamedParameterJdbcTemplate` / `JdbcTemplate`** (`org.springframework.jdbc.core[.namedparam]`) — **raw Spring JDBC**. Execute arbitrary SQL, map with a `RowMapper`/`ResultSetExtractor`. **No** aggregate knowledge, **no** cascade, **no** events. Maximum control and often the best performance for set-based work. ## Choosing - **Everyday CRUD + simple finders** -> repository (derived or `@Query`). Least code, most readable, aggregate invariants preserved. - **Explicit insert vs update / client-assigned IDs / dynamic-type persistence / batch import tooling** -> `JdbcAggregateTemplate`. You get control while keeping cascades and callbacks. Classic case: assigned UUID id where `save()` would issue a zero-row UPDATE, so call `insert()`. - **Reporting, analytics, heavy joins into DTOs, set-based bulk UPDATE/DELETE, vendor-specific SQL that maps to no aggregate** -> `NamedParameterJdbcTemplate`. You accept manual mapping in exchange for full SQL freedom and no object-graph overhead. ## Architectural trade-offs - **Abstraction vs control**: higher layers minimize boilerplate and centralize aggregate rules; lower layers expose raw SQL and require you to own mapping and correctness. - **Aggregate integrity**: repository and `JdbcAggregateTemplate` treat the aggregate as the consistency boundary (root owns children). Dropping to `NamedParameterJdbcTemplate` for writes **bypasses** cascade/version handling — easy to violate invariants or skip optimistic locking. Prefer it for reads and clearly-scoped bulk operations. - **Events/callbacks**: only the aggregate layers fire lifecycle callbacks and domain events. If auditing/eventing hangs off those, raw-JDBC writes silently skip them. - **Performance**: repositories replace owned collections on update (delete + re-insert children), which can be costly for large collections; a targeted `NamedParameterJdbcTemplate` UPDATE may be far cheaper. Set-based bulk operations also avoid per-aggregate round-trips. - **Testability & consistency**: mixing all three is fine and idiomatic — many teams read reports with `NamedParameterJdbcTemplate`, do normal writes through repositories, and use `JdbcAggregateTemplate` for the few programmatic-control cases. Keep the choice deliberate and documented so aggregate invariants are not accidentally bypassed. ## Transactions All three participate in Spring-managed transactions when called within a `@Transactional` boundary. Repository proxies add their own transactional metadata; the two templates rely on the ambient transaction — wrap multi-statement flows explicitly to keep them atomic. ## Summary heuristic Start at the **highest** abstraction that expresses the need; drop a level only for a concrete reason (control, performance, or SQL that has no aggregate). Never use raw JDBC writes to sidestep aggregate rules casually — that is how invariants and optimistic locking quietly break.

  • What do you lose by doing a write through NamedParameterJdbcTemplate instead of a repository or JdbcAggregateTemplate?
    Aggregate semantics (cascade to owned children, owned-collection replacement), optimistic-locking/@Version handling, and lifecycle callbacks/domain events. It is fine for scoped bulk operations and reads, but casual raw-JDBC writes can silently bypass invariants and auditing.
  • Why might a targeted NamedParameterJdbcTemplate UPDATE outperform saving the aggregate?
    Saving an aggregate replaces owned collections by deleting and re-inserting children and does per-aggregate round-trips. A single set-based UPDATE touches only the needed rows in one statement, avoiding that overhead.

saying these in an interview costs you the question

  • Using raw JDBC for aggregate writes without realizing cascade/version/events are bypassed
  • Claiming JdbcAggregateTemplate and NamedParameterJdbcTemplate are the same thing
  • Believing repositories always outperform raw SQL for bulk/set-based work
  • Thinking events/callbacks fire for NamedParameterJdbcTemplate operations

context