Same Customer/Order/OrderItem domain modelled with Hibernate/Spring Data JPA and Spring Data JDBC side by side, both running through a shared StatementLoggingDataSource so SQL-statement counts are directly comparable. 11 tests, 11 captured transcripts, 6 doc chapters. Companion repo for the ankurm.com article on when to drop the ORM.
2.7 KiB
05 — The aggregate boundary
← 04 the JDBC side · next: when this gets expensive →
Source: jdbc/Order.java (the
AggregateReference<Customer, Long> customer field). Test:
JdbcBehaviorTest#c_aggregateReferenceDoesNotLoadTheCustomer,
transcript 08.
Domain-Driven Design's aggregate pattern says: an aggregate is a cluster of objects saved and
loaded as one transactional unit, and a reference to another aggregate is held by identity, not
by object graph. Spring Data JDBC enforces this at the type level. Order.customer is not a
Customer field — it is an AggregateReference<Customer, Long>, a typed wrapper around a
foreign-key value. AggregateReference.getId() returns that value without touching the database,
because the value is already sitting in a column that was read as part of loading the order.
Scenario 08 demonstrates the consequence directly:
loading an order and then reading order.getCustomer().getId() produces zero statements
touching the customer table. There is no join, no lazy proxy, no N+1 waiting to happen — because
there is no mechanism in Spring Data JDBC that would ever load a second aggregate as a side
effect of loading the first one. If you want the Customer, you ask a CustomerRepository for
it, explicitly, and that is a second, deliberate query.
This is the mechanism that makes scenario 06's
"fixed cost regardless of item count" claim scale past one aggregate: the fixed cost is fixed
per aggregate, and nothing about loading Order #1 ever cascades into loading Order #2's
Customer, Order #2's Customer's other orders, and so on. JPA's @ManyToOne(fetch = LAZY)
gives you a proxy that resolves this the moment something touches it — convenient until the thing
touching it is a toString() in a log statement three services away from the code that loaded
the entity.
The trade is that Spring Data JDBC will not silently fetch related data across an aggregate boundary for you, ever, under any circumstance. If your domain genuinely needs a graph fetch (here's every order together with its customer's other orders), you write that query yourself. Some teams experience that as "more code to write." Others experience the JPA alternative as "a query plan I can no longer predict by reading the entity class."
Continue to 06 — when this gets expensive.