Entry 2 ended with a note being saved and an audit row potentially failing.
Now we open the kitchen door and look at one of the most important things in real Spring applications:
What happens when one database operation succeeds, but the next one fails?
This is where @Transactional stops being just another annotation and starts making sense.
Think of a bank transfer
Imagine you transfer ₹1,000 from Account A to Account B.
The application has to do at least two things:
Subtract ₹1,000 from Account A.
Add ₹1,000 to Account B.
What if step 1 succeeds but the application crashes before step 2?
Account A loses money.
Account B gets nothing.
That is exactly the kind of problem a database transaction is designed to prevent.
Think of a transaction as a sealed kitchen order:
Either the complete order is served, or the entire order is cancelled.
No half-cooked order reaches the customer.
The realtime path: POST /api/notes
Suppose creating a note also creates an audit record.
The request is:
{
"title": "Revise transactions",
"body": "Understand @Transactional"
}
The flow looks like this:
Security filter checks the user's token.
Controller receives the request.
Controller calls
noteService.createNote(...).Spring starts a database transaction.
Service saves the note.
Service saves the audit record.
Everything succeeds → COMMIT.
If something fails → ROLLBACK.
The important part is step 7.
The database does not permanently accept the changes until the transaction successfully completes.
What @Transactional actually does
You might see code like this:
@Service
public class NoteService {
private final NoteRepository noteRepository;
private final AuditRepository auditRepository;
public NoteService(
NoteRepository noteRepository,
AuditRepository auditRepository) {
this.noteRepository = noteRepository;
this.auditRepository = auditRepository;
}
@Transactional
public Note createNote(Long userId, CreateNoteRequest request) {
Note note = new Note();
note.setUserId(userId);
note.setTitle(request.getTitle());
note.setBody(request.getBody());
Note savedNote = noteRepository.save(note);
Audit audit = new Audit();
audit.setUserId(userId);
audit.setAction("NOTE_CREATED");
auditRepository.save(audit);
return savedNote;
}
}
You don't manually write:
beginTransaction();
commit();
rollback();
Spring manages that boundary for you.
Conceptually:
BEGIN TRANSACTION
INSERT INTO notes ...
INSERT INTO audit ...
COMMIT
If something goes wrong:
BEGIN TRANSACTION
INSERT INTO notes ...
INSERT INTO audit ...
↓
ERROR
ROLLBACK
The note insertion is rolled back too.
The important question: when does rollback happen?
Suppose the audit insertion throws an exception.
The service does not simply return the note.
The transaction manager sees the failure and rolls the transaction back according to its rollback rules.
So you don't end up with:
notes table
----------------
Note #101 exists
audit table
----------------
No audit record
Instead, both operations are rolled back.
notes table
----------------
Note #101 does not exist
audit table
----------------
No audit record
That is the real value of a transaction.
One transaction, multiple database operations
This is why transaction boundaries normally belong around a business operation, not around every individual repository method.
For example:
@Transactional
public void placeOrder(...) {
saveOrder();
reduceStock();
createPaymentRecord();
createAuditRecord();
}
These operations are logically one business action.
If stock is reduced but the payment record cannot be created, you usually don't want the stock reduction to remain.
The transaction groups them together.
Controller vs Service: where should @Transactional go?
A common fresher question is:
Should I put @Transactional on the controller?
Usually, no.
Keep the controller responsible for HTTP concerns.
@PostMapping("/orders")
public OrderResponse createOrder(
@RequestBody CreateOrderRequest request) {
return orderService.createOrder(request);
}
The service owns the business operation:
@Transactional
public OrderResponse createOrder(CreateOrderRequest request) {
// business operation
}
That gives you a clean separation:
Controller
↓
Service + Transaction
↓
Repository
↓
Database
A transaction is not the same as validation
This is another thing freshers often mix up.
Suppose the title is empty:
{
"title": "",
"body": "hello"
}
You can reject this with validation:
@NotBlank
private String title;
That is a validation problem.
You don't need a database transaction just to discover that the request is invalid.
The flow can be:
Request
↓
Validation
↓
400 Bad Request
But once you start changing multiple pieces of database state, transactions become important:
Request
↓
Validation
↓
Service
↓
Transaction
├── Save order
├── Update stock
└── Save audit
↓
COMMIT
What happens when the method returns?
This is an important detail.
When a transactional service method finishes successfully, Spring commits the transaction around the method execution.
Conceptually:
Controller calls service
↓
Spring opens transaction
↓
Service executes
↓
Service returns
↓
Spring commits transaction
↓
Controller sends HTTP response
If the transactional operation fails according to the transaction's rollback rules:
Controller calls service
↓
Spring opens transaction
↓
Service executes
↓
Exception
↓
Spring rolls back
↓
Exception handling
↓
Error response
The exact behavior depends on the exception type and transaction configuration, so don't memorize “every exception always rolls back.” Understand the rollback rules.
The classic fresher mistake
Imagine this:
@Transactional
public void createOrder() {
orderRepository.save(order);
paymentRepository.save(payment);
sendEmail();
}
A fresher may think:
“Everything inside this method is automatically undone if anything happens.”
Not necessarily.
Database changes and external systems are different things.
If sendEmail() successfully sends an email and then a database operation causes the transaction to roll back, the email cannot magically be unsent.
You might end up with:
Database → ROLLED BACK
Email → ALREADY SENT
This is where real-world systems become more interesting.
For simple applications, keep your transaction focused on the database work.
For larger systems, you may need patterns such as events, outbox patterns, retries, or distributed transaction strategies depending on the architecture.
What about readOnly = true?
For read-only service operations, you may see:
@Transactional(readOnly = true)
public UserProfile getProfile(Long userId) {
return userRepository.findById(userId)
.map(this::toProfile)
.orElseThrow();
}
The important idea is that you're telling Spring and the underlying persistence setup that this operation is intended for reading.
But don't think of readOnly = true as a magical security lock that makes every database write impossible. It is primarily a transaction/read-only hint, and its exact behavior depends on the database and persistence configuration.
The bigger picture
At this point, the restaurant has become a little more realistic.
Entry 1:
Customer
↓
Waiter
↓
Controller
↓
Service
↓
Repository
↓
Database
Entry 2 added the security guard:
Customer
↓
Security Filter
↓
Controller
↓
Service
↓
Repository
↓
Database
Entry 3 adds the transaction boundary:
Customer
↓
Security Filter
↓
Controller
↓
┌─────────────────────────────┐
│ Transaction │
│ │
│ Service │
│ ↓ │
│ Repository │
│ ↓ │
│ Database │
│ │
└─────────────────────────────┘
↓
COMMIT
Now you can see why Spring applications have these layers.
They are not there just because “that's how Spring projects are structured.”
Each layer solves a different problem.
Controller → HTTP
Security → Identity and access
Service → Business rules
Transaction → Keep related database changes consistent
Repository → Database access
Database → Persistent data
Practice for this page
Take the same notes application from Entries 1 and 2.
Add an audit table.
When a user creates a note:
Save the note.
Save an audit record.
Put both operations inside one transaction.
Intentionally make the audit operation fail.
Check the database.
Confirm that the note was also rolled back.
Then remove @Transactional and repeat the experiment.
Seeing the difference yourself will teach you more than memorizing the definition of a transaction.
Next, we can go one level deeper:
What really happens between repository.save() and the SQL reaching PostgreSQL?
That is where JPA, Hibernate, persistence context, dirty checking, and flush start to make sense.
Comments (0)