What is AggregateReference in Spring Data JDBC, why does it exist, and how does it behave on read and write?
answer
- Reference other aggregates by id, not object
- AggregateReference<T, ID>
- AggregateReference.to(id) to create
- Read gives getId() only — no load, no join
- IdOnlyAggregateReference; cross-aggregate boundary
basics
~20 sAggregateReference<T, ID> is a typed link to another aggregate root by its id only. It stores the foreign-key id in a column but does NOT load the target object — reading gives you just getId(); you fetch the referenced aggregate separately if you need it.
solid answer
~40 s`AggregateReference<T, ID>` (org.springframework.data.jdbc.core.mapping) models a reference from one aggregate root to another **by id**, honoring the DDD rule that aggregates reference each other by identity, not by object graph. On write it stores the referenced aggregate's id in a plain foreign-key column (named via NamingStrategy from the property). On read Spring Data JDBC populates an `AggregateReference` (the `IdOnlyAggregateReference` implementation) whose `getId()` returns the id — it does **not** join, load, or lazily fetch the target aggregate. If you need the full object you query its repository yourself. You create one with the static factory `AggregateReference.to(id)`. This is deliberately different from within-aggregate references (`@MappedCollection`, embedded objects), which Spring Data JDBC loads and saves as one unit. AggregateReference is the boundary marker between aggregates.
code
java · 22 linesimport org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.annotation.Id;
class Book {
@Id Long id;
String title;
// link to the Author aggregate by id only:
AggregateReference<Author, Long> author; // column "author" holds author.id
}
class Author { @Id Long id; String name; }
// --- writing ---
Book b = new Book();
b.title = "Clean Aggregates";
b.author = AggregateReference.to(42L); // just stores the id 42
bookRepository.save(b);
// --- reading ---
Book loaded = bookRepository.findById(1L).orElseThrow();
Long authorId = loaded.author.getId(); // 42 -- Author itself is NOT loaded
Author author = authorRepository.findById(authorId).orElseThrow(); // explicit fetchgo deeper
Know it links to another aggregate by id and getId() is all you get on read.
Explain create via AggregateReference.to(id), FK-column storage, and no automatic loading.
Contrast within-aggregate (@MappedCollection) vs cross-aggregate references and the cascade/delete implications.
Use aggregate boundaries + AggregateReference to design explicit load/consistency boundaries and avoid N+1 traversal.
## The DDD idea behind it Spring Data JDBC is built around **Domain-Driven Design aggregates**. An *aggregate* is a cluster of objects treated as one unit for loading and saving, with a single *aggregate root*. A core DDD rule: **aggregates reference other aggregates only by identity (id), never by holding the other aggregate's object graph.** `AggregateReference` is the concrete API for that rule. ## The type `AggregateReference<T, ID>`: - `T` = the referenced aggregate root type (e.g. `Person`). - `ID` = the type of that aggregate's id (e.g. `Long`). In your entity you declare a field like `AggregateReference<Person, Long> owner;`. ## Persistence behavior - **Storage:** it maps to a **single foreign-key column** holding the referenced id. The column name comes from the NamingStrategy applied to the property name (e.g. property `owner` → column `owner`), overridable with `@Column`. - **Write:** `AggregateReference.to(personId)` creates a reference; saving the owner writes `personId` into the column. Spring Data JDBC does **not** cascade-save the referenced aggregate — you are only storing its id. - **Read:** the framework reconstitutes an `AggregateReference` whose `getId()` returns the stored id. The concrete class is `IdOnlyAggregateReference`. **No join is issued and the target aggregate is not materialized.** There is no lazy proxy — you literally only have the id. ## Getting the actual target Because nothing is loaded, if you need the referenced `Person` you call `personRepository.findById(ref.getId())` explicitly. This keeps each aggregate's load boundary explicit and avoids surprise N+1 traversals. ## Contrast with within-aggregate references - `@MappedCollection` / nested entities / `@Embedded` → **within** the aggregate; loaded and saved together with the root, deleted with it. - `AggregateReference` → **across** aggregate boundaries; only the id crosses, nothing is loaded or cascaded. ## Gotchas - People expect `getId()`-plus-object like a JPA `@ManyToOne`; there is no managed target and no lazy loading in Spring Data JDBC. - Deleting the owner does **not** delete the referenced aggregate (correct — it is a separate aggregate). - The referenced id must exist for referential integrity if you have a DB FK constraint; Spring Data JDBC won't create the target for you. - `AggregateReference.to(null)` / mapping a null column yields a null reference (no id). - The id type parameter must match the target's `@Id` type, or reads/writes fail type conversion.
- How does AggregateReference differ from a @MappedCollection child within the aggregate?A @MappedCollection child is part of the same aggregate: it is loaded, saved, and deleted together with the root. AggregateReference points to a separate aggregate — only its id is stored; the target is never loaded or cascaded.
- I loaded a Book and want the Author object. What do I do?Read the id with book.author.getId() and call authorRepository.findById(id) yourself. Spring Data JDBC does not lazily load or join the referenced aggregate.
saying these in an interview costs you the question
- Expecting AggregateReference to lazily load the target like a JPA @ManyToOne
- Thinking saving the owner cascades a save/insert of the referenced aggregate
- Assuming deleting the owner deletes the referenced aggregate