Spring Boot / Intermediate: data, security, and framework evidence / Lesson 8
Transaction proxies and error semantics
Define a business atomic unit and verify how proxy entry, propagation, rollback rules, and transaction-manager behavior implement it.
On this page
Lesson goals
- Explain why annotation presence is not transaction evidence.
- Choose rollback and propagation from the business contract.
- Preserve failures and verify committed state separately.
Choose what must commit together
A transfer's debit and credit normally belong to one database atomic unit. Spring's transaction abstraction coordinates supported resources through a transaction manager; it does not make arbitrary external effects rollback-capable. Define the unit and resource manager before selecting annotations. A controller method calling two separately committed operations can leave partial business state even if each individual repository method is transactional.
The transaction should be short and avoid user interaction or slow remote calls while database resources are held. Decide who owns completion and error reporting. A service body returning a value is not yet proof the outer proxy invocation completed its commit successfully; failures can arise during flush or commit. Preserve the causal failure instead of converting it into a successful response.
Enter the managed boundary for a declared unit
The transfer fragment delegates domain checks to contextual Account methods and declares one service transaction. It requires managed repositories, entities, a suitable manager, and a chosen database isolation/concurrency design. It is not executed and does not prove lost-update protection merely by using @Transactional. Version checks, locks, predicates, and constraints remain part of the invariant.
By default in the usual Spring configuration, unchecked exceptions trigger rollback while checked exceptions do not automatically do so; configuration can change that policy. Declare a relevant rollbackFor when a checked business failure must abort the unit. Do not catch the failure, return normally, and assume Spring knows the intended rejection. Conversely, do not make every expected rejection an unexplained server error.
package training.catalog;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
class TransferService {
private final AccountRepository accounts;
TransferService(AccountRepository accounts) { this.accounts = accounts; }
@Transactional
public void transfer(long from, long to, long cents) {
if (cents <= 0) throw new IllegalArgumentException("amount");
Account source = accounts.findById(from).orElseThrow();
Account target = accounts.findById(to).orElseThrow();
source.debit(cents);
target.credit(cents);
}
}
The contextual account/repository contracts must preserve their invariants. No database transaction, isolation behavior, or commit was observed.
Separate proxy entry from same-instance calls
In the usual proxy-based mode, a call within the same instance does not pass through the external proxy and can bypass transaction advice. Manual construction or a direct unit-test call can do the same. A private method annotation is not a reliable public transaction boundary. Keep the actual proxy mode and method visibility documented instead of trusting annotation placement alone.
The second fragment separates a coordinator from a chunk writer. REQUIRES_NEW is an explicit policy: each eligible proxied call can use an independent transaction, so earlier chunks may remain committed if a later chunk fails. It is not a generic fix for an all-or-nothing import. Suspended outer transactions and connection acquisition can also create pool and locking costs.
package training.catalog;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
class ChunkWriter {
private final ImportRepository rows;
ChunkWriter(ImportRepository rows) { this.rows = rows; }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void write(List<ImportRow> chunk) { rows.saveAll(chunk); }
}
@Service
class ImportCoordinator {
private final ChunkWriter writer;
ImportCoordinator(ChunkWriter writer) { this.writer = writer; }
public void run(List<List<ImportRow>> chunks) {
for (var chunk : chunks) writer.write(chunk);
}
}
Calls cross managed bean boundaries, but their actual transaction behavior needs integration tests and configured storage. The fragment is not run.
Handle rollback-only and read-only meanings accurately
An inner participating operation can mark a shared transaction rollback-only. Catching its exception in an outer method does not necessarily restore committability; completion may raise UnexpectedRollbackException. Treat this as evidence about ownership and failure policy, not an invitation to suppress another exception and claim success. REQUIRES_NEW or savepoint-style behavior has different semantics and manager support requirements.
readOnly expresses a transaction hint/optimization contract, not a universal authorization barrier that forbids every possible write. Isolation and timeout behavior also depend on the manager and database. Async work on another thread does not automatically inherit a thread-bound transaction. Give each asynchronous unit a deliberate boundary and coordinate cross-resource effects explicitly.
Verify the real proxy and stored outcome
A meaningful integration test injects failure after the first related change, calls through the managed service, and verifies final database state after completion. Test unchecked and relevant checked failures, self-invocation risk, participation, version conflict, and commit-time rejection. A mock repository or plain Java model cannot establish any of those database rollback guarantees.
Keep the test environment isolated and explicitly authorized; these fragments do not provision one. Separate transaction tests from HTTP-thread assumptions, and never point them at a configured project datasource. Record the manager, database, propagation, failure path, and observed final state. The transaction contract is complete only when a reviewer can explain both success and every supported failure outcome.
Common errors and fixes
An annotated method ran without the expected transaction
- Why it happens
- The call bypassed the managed proxy or selected unsupported semantics.
- How to fix it
- Inspect the real caller, proxy, method visibility, and manager.
- Prevent it next time
- Exercise the managed boundary in integration tests.
UnexpectedRollbackException
- Why it happens
- Participating work marked the transaction rollback-only.
- How to fix it
- Repair the ownership/failure policy and observe completion honestly.
- Prevent it next time
- Test an inner failure that the outer method catches.
A checked failure left changes committed
- Why it happens
- The expected rollback rule was not configured.
- How to fix it
- Declare the appropriate policy and verify actual stored state.
- Prevent it next time
- Test relevant checked and unchecked failures.
Check your understanding
1. Why does adding REQUIRES_NEW to a same-instance helper not necessarily create a new transaction?
Show worked solution
In proxy mode, the self-call does not cross the external advice boundary. Move a justified boundary to a managed collaborator or use a reviewed programmatic design. Then verify the selected partial-commit semantics and resource costs rather than assuming the annotation fixes atomicity.
2. A failed inner REQUIRED operation is caught, and the outer method returns normally. What must still be checked?
Show worked solution
Whether the transaction is rollback-only and what happens at actual completion. Catching a Java exception is not transaction recovery. Preserve a truthful failed outcome and verify the database state instead of returning success before the proxy has finished.
Key takeaways
- The business atomic unit precedes propagation choice.
- Proxy entry and rollback policy determine annotation effects.
- Only controlled persistence evidence verifies actual commit/rollback behavior.