skip to content

What is JdbcAggregateTemplate and when would you use it instead of a repository?

level: seniorimportance: should knowfreq 40%

answer

  1. engine behind SimpleJdbcRepository, auto-configured bean
  2. aggregate-level (root + owned), not raw SQL
  3. insert/update force it; save infers new-ness
  4. client-assigned id -> use insert(), not save()
  5. same callbacks/events as repositories

basics

~20 s

JdbcAggregateTemplate is the lower-level engine behind Spring Data JDBC repositories. It offers programmatic aggregate operations, insert, update, save, delete, findById, findAll, count, without declaring a repository interface. Use it for dynamic or fine-grained control over persistence.

solid answer

~50 s

org.springframework.data.jdbc.core.JdbcAggregateTemplate is the programmatic, aggregate-aware persistence API that Spring Data JDBC repositories are built on. It is auto-configured as a bean you can inject. It exposes methods like insert, update, save, delete, deleteAll, findById, findAll, findAllById, and count, all operating on whole aggregates (root plus owned entities) and firing the same lifecycle callbacks and events as repositories. The key distinction from save() semantics: save() infers insert-vs-update from whether the aggregate is new (id null, or @Version state), whereas insert() and update() force the operation explicitly. That matters when you assign IDs yourself, where save() would wrongly attempt an UPDATE, so you call insert() instead. Reach for the template when a repository interface is too rigid: generic/dynamic persistence code, libraries, batch tooling, or when you need explicit insert/update control. It is aggregate-level, unlike the raw, SQL-level NamedParameterJdbcTemplate.

code

java · 24 lines
java
@Service
public class OrderImporter {

    private final JdbcAggregateTemplate template;

    public OrderImporter(JdbcAggregateTemplate template) {
        this.template = template;
    }

    @Transactional
    public void importWithAssignedId(Order order) {
        // Order has a client-assigned (non-null) id, so save() would try an UPDATE.
        // insert() forces the INSERT.
        template.insert(order);
    }

    public Order load(Long id) {
        return template.findById(id, Order.class);
    }

    public long total() {
        return template.count(Order.class);
    }
}

go deeper

for a junior

Recognize it as a lower-level way to do CRUD without a repository interface.

for a middle

Know its core methods and that it works on aggregates, not raw SQL.

for a senior

Explain save vs insert/update semantics, the client-assigned-id trap, and that repositories delegate to it.

for a principal

Position it as an architectural escape hatch: generic/dynamic persistence, batch tooling, callbacks/events parity, transaction and optimistic-locking considerations.

## What it is `org.springframework.data.jdbc.core.JdbcAggregateTemplate` implements `JdbcAggregateOperations`. It is the **engine** that `SimpleJdbcRepository` (the default implementation behind your `CrudRepository` interfaces) delegates to. Spring Boot's Spring Data JDBC auto-configuration registers it as a **bean**, so you can simply `@Autowired`/constructor-inject it — no interface required. ## Aggregate-aware, not raw SQL Every operation works on an **aggregate**: the aggregate root plus the entities it owns (e.g. an `Order` with its `OrderItem`s). Saving a root cascades to owned children; deleting a root deletes them. This is a higher level than `org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate` / `JdbcTemplate`, which just execute SQL and know nothing about your object graph. ## Core methods - `insert(T)` — always issues an INSERT. - `update(T)` — always issues an UPDATE. - `save(T)` — decides insert vs update (see below); `saveAll(iterable)`. - `delete(T, ...)`, `deleteById(id, type)`, `deleteAll(type)`. - `findById(id, type)`, `findAll(type)`, `findAllById(ids, type)`, `count(type)`, `existsById`. All of these fire the same **lifecycle events / `EntityCallback`s** (e.g. `BeforeConvertCallback`, `BeforeSaveCallback`, `AfterSaveCallback`) as repository calls, because repositories route through the template. ## The crucial save() vs insert()/update() distinction `save()` must decide whether the aggregate is **new**: - If the id property is `null` (for object ids) or zero (for primitive numeric ids), it is treated as new -> INSERT. - If the aggregate has a `@Version` property, that version drives new-detection and enables **optimistic locking** (version 0/null = new; a mismatch on update throws `OptimisticLockingFailureException`). **Problem case**: if you assign primary keys yourself (e.g. UUIDs or natural keys generated in the app), the id is *non-null* for a brand-new row, so `save()` sees it as existing and issues an **UPDATE that affects zero rows**. The fix is to call **`insert()`** explicitly. Conversely `update()` forces an UPDATE when you know the row exists. This explicit control is one of the main reasons to use the template directly. ## When to use the template over a repository - **Client-assigned IDs**: use `insert()` to avoid the failed-UPDATE trap. - **Dynamic / generic persistence**: code that must persist arbitrary aggregate types where you cannot pre-declare a typed repository interface (frameworks, importers, admin tooling). - **Batch / seeding utilities** and tests that need direct, explicit control. - **Fine-grained control** over insert vs update within one flow. For everyday CRUD, a `CrudRepository`/`ListCrudRepository` interface is more declarative and preferred; the template is the escape hatch. ## Transactions Template methods are **not** implicitly transactional the way repository proxies are; wrap multi-step template usage in `@Transactional` (or rely on the surrounding service's transaction) so cascades and multiple operations commit atomically. ## Gotchas - Calling `save()` on an entity with a **pre-set id and no @Version** silently does an UPDATE (0 rows) — use `insert()`. - The template is aggregate-scoped: `delete`/`findById` require you to pass the **entity type** as an argument. - It is not `JdbcTemplate`: do not expect raw `query(sql, ...)` methods here; for arbitrary SQL use `NamedParameterJdbcTemplate` or a `@Query` repository method. - Owned collections are fully **replaced** on update (children deleted and re-inserted) — a general Spring Data JDBC behavior that also applies through the template.

  • Why can save() fail to persist a brand-new aggregate that has a client-assigned ID?
    save() decides insert vs update by new-detection: a non-null id (and no @Version indicating newness) makes the aggregate look existing, so it issues an UPDATE that matches no rows and persists nothing. Calling insert() forces the INSERT.
  • How does JdbcAggregateTemplate relate to your CrudRepository interfaces?
    It is the underlying implementation. SimpleJdbcRepository (the default repository proxy) delegates its operations to JdbcAggregateTemplate, so both share the same aggregate handling, lifecycle callbacks, and events.

saying these in an interview costs you the question

  • Thinking JdbcAggregateTemplate is just JdbcTemplate with raw SQL methods
  • Believing save() always inserts a new row regardless of id state
  • Assuming template calls are automatically transactional
  • Not knowing it operates on whole aggregates including owned children

context