Spring transactions
Why Spring @Transactional fails on self-invocation
By the BackendDrills editorial team · Published and checked October 6, 2026 · 6-minute read
Before you start: Basic Spring beans and SQL transactions. Find this reading in a study path →
In Spring's default proxy mode, a call from one method to another on the same object does not pass through the transaction proxy. An annotation on the inner method therefore does not start a new transaction for that call. An outer transaction may still exist; inspect the whole call path.
A small example to reason about
Imagine a stock-import job that writes two adjustments. The second adjustment fails validation. The team expects the first write to disappear, but it remains in the database. Start with the route into the service, not the spelling of the annotation.
// Illustrative fragment: methods belong to the same Spring bean.
public void importStock() {
applyAdjustments(); // ordinary call on this object
}
@Transactional
public void applyAdjustments() {
saveFirstAdjustment();
throw new IllegalStateException("second adjustment rejected");
}
Assume the outer method has no transaction and each repository operation can commit independently. Under those assumptions, the annotation on applyAdjustments cannot provide the expected all-or-nothing boundary for this invocation. Changing the annotation's propagation setting alone does not make this local call cross a proxy.
Draw the boundary before choosing a fix
- Identify the caller: a controller, scheduled task, another bean, or a method on the same object.
- Check that the service is created by Spring, transaction management is enabled, and the correct transaction manager serves the relevant datasource.
- Observe transaction activity at the write site in a controlled test. A log entry saying that the method ran does not establish that interception happened.
A useful design is to move the unit of database work into a separate Spring-managed service and call it through the injected bean. Alternatively, place the transaction on the externally invoked service operation when that is the intended business boundary. Keep slow remote calls outside that database boundary where the business design permits it.
A verification test that catches the original failure
Create a fresh test database, call the public entry point through the application context, force the second write to throw, and inspect the committed rows afterward. Avoid wrapping the entire test in a transaction that hides the application's missing boundary. Include a success case so a repair that prevents every write cannot pass.
| Observation | Next question |
|---|---|
| No transaction at either write | Did the call reach a managed proxy? |
| A transaction exists, but data remains | Was the exception swallowed, or did a separate transaction commit? |
| Only one datasource rolls back | Which transaction manager owns each write? |
Explain the decision aloud
Describe the caller, the actual interception boundary, the business invariant and the rollback test. Do not conclude that every rollback problem is self-invocation: exception handling, multiple resources and explicit propagation create different failure paths.
Scope: Spring imperative transaction management in proxy mode. AspectJ weaving has different interception behavior. This example is educational and should be adapted to the application's transaction model.
Check the underlying behavior
Original illustrative examples, prepared with AI assistance and checked against the linked primary documentation. No customer incident or vendor endorsement is claimed. Our editorial approach.
Practice the next decision
Try a complete free backend drill: inspect evidence, make three decisions and review the reasoning. No account or card required.
Try a free incident drill →Selected practice from the study paths
- The rollback that never happened. · Complete free drill
- The exception was thrown. The data committed. · Full edition drill
Free readings need no account. Full edition drills require verified access; opening a paid link does not expose its answers.