Definition
- JTA/XA -- Java Transaction API with XA drivers coordinates multiple resources (database + message broker) in one atomic transaction using two-phase commit.
- Two-phase commit -- phase one: coordinator asks resources to prepare and vote; phase two: commit if all voted yes, rollback if any voted no; blocking protocol that holds locks across network calls.
- @Modifying -- a Spring Data annotation on
@Querymethods that tells the framework the statement is an UPDATE or DELETE (DML), not a SELECT, so it callsexecuteUpdate()and returns affected row count. - clearAutomatically=true -- clears the persistence context after the bulk update so subsequent reads do not return stale entities.
- flushAutomatically=true -- flushes pending Hibernate changes before executing the bulk statement so the statement can see them.
At the National Bank of India, JTA/XA is like a senior manager who holds the vault door and the safe deposit box open at the same time, refusing to close either until both are confirmed. It works but blocks everyone else. Meanwhile, @Modifying is like Rajesh telling the system: "This is not a balance inquiry, this is a mass update to freeze overdrawn accounts. After the update, refresh all the teller screens (clearAutomatically) so they see the new status."
Understanding the Concepts
What are distributed transactions with JTA/XA?
JTA (Java Transaction API) with XA drivers lets one logical transaction span several resources, such as a database and a message broker, or two databases. The coordinator (transaction manager) runs two phases. In the prepare phase, it asks every resource: "Can you commit?" Each resource votes YES or NO. If all vote YES, the commit phase tells every resource to make changes permanent. If any vote NO, all roll back.
The weakness is blocking. Between prepare and commit, resources hold row-level locks across network round-trips. A coordinator crash can leave resources in an "in-doubt" state holding locks indefinitely.
package com.bank.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.jta.JtaTransactionManager;
@Configuration
public class JtaConfig {
@Bean
public AtomikosDataSourceBean accountsXaDataSource() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName("accounts");
ds.setXaDataSourceClassName(
"org.postgresql.xa.PGXADataSource");
return ds;
}
@Bean
public JtaTransactionManager transactionManager() {
return new JtaTransactionManager();
}
}
JTA/XA flow:
Coordinator PostgreSQL Kafka (not XA!)
│ │ │
│── PREPARE ──────▶│ │
│ vote YES ◀─────│ │
│── PREPARE ────────────────────────▶ │
│ NOT SUPPORTED ◀───────────────────│ Kafka has no XA!
│
Result: cannot use 2PC with Kafka
Alternative: Transactional Outbox + Saga
Why is JTA/XA discouraged in microservices?
In microservices, each service owns its own database and communicates via messages or HTTP. JTA/XA couples services tightly: all participants must be available at commit time. It blocks: locks are held across network boundaries. The coordinator is a single point of failure. And many cloud-native stores (Kafka, DynamoDB, MongoDB) do not support XA.
For a modern banking microservice architecture, the preferred alternative is the Transactional Outbox plus Saga pattern. Each service commits locally, an outbox event is relayed asynchronously, and compensating transactions handle failures.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class AccountMaintenanceService {
private final AccountRepository accountRepo;
private final OutboxRepository outboxRepo;
public AccountMaintenanceService(AccountRepository accountRepo,
OutboxRepository outboxRepo) {
this.accountRepo = accountRepo;
this.outboxRepo = outboxRepo;
}
@Transactional // single local TX, no XA
public void freezeOverdrawnAccount(String accountId) {
accountRepo.updateStatus(accountId, "FROZEN");
outboxRepo.save(new OutboxEvent(
"ACCOUNT_FROZEN",
"{\"accountId\":\"" + accountId + "\"}")
);
// Both committed atomically in one local TX
}
}
What is @Modifying in Spring Data?
@Modifying is a Spring Data annotation placed on a @Query method to indicate that the query is a DML statement (UPDATE or DELETE) rather than a SELECT. Without @Modifying, Spring Data assumes the query returns results and tries to call getResultList(). With @Modifying, it calls executeUpdate() and returns the number of affected rows.
A @Modifying query must run inside an active @Transactional context. Without a transaction, Spring throws InvalidDataAccessApiUsageException.
package com.bank.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.math.BigDecimal;
public interface AccountRepository extends JpaRepository<Account, String> {
// Bulk UPDATE: freeze all overdrawn accounts
@Modifying
@Query("UPDATE Account a SET a.status = 'FROZEN' " +
"WHERE a.balance < :threshold")
int freezeOverdrawnAccounts(
@Param("threshold") BigDecimal threshold);
// Bulk DELETE: purge closed zero-balance accounts
@Modifying
@Query("DELETE FROM Account a " +
"WHERE a.status = 'CLOSED' AND a.balance = 0")
int purgeClosedAccounts();
}
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class NightlyMaintenanceService {
private final AccountRepository accountRepo;
public NightlyMaintenanceService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional // Required for @Modifying queries
public void runNightlyMaintenance() {
int frozen = accountRepo.freezeOverdrawnAccounts(
BigDecimal.ZERO);
int purged = accountRepo.purgeClosedAccounts();
System.out.println("Frozen: " + frozen
+ ", Purged: " + purged);
}
}
Expected output:
Frozen: 23, Purged: 5
What does clearAutomatically do?
A bulk UPDATE or DELETE bypasses the Hibernate persistence context. It sends SQL directly to the database without updating the in-memory entity state. If you loaded an Account entity before the bulk update, that entity still has the old status in memory even though the database row is now FROZEN.
Setting clearAutomatically = true tells Spring Data to call entityManager.clear() after the bulk statement executes. This evicts all entities from the persistence context, so the next read goes to the database and gets fresh data.
package com.bank.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.math.BigDecimal;
public interface AccountRepository extends JpaRepository<Account, String> {
// Without clearAutomatically: stale entities in memory
@Modifying
@Query("UPDATE Account a SET a.status = 'FROZEN' " +
"WHERE a.balance < 0")
int freezeOverdrawnWrong();
// With clearAutomatically: persistence context cleared
@Modifying(clearAutomatically = true)
@Query("UPDATE Account a SET a.status = 'FROZEN' " +
"WHERE a.balance < 0")
int freezeOverdrawnCorrect();
}
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StaleEntityDemo {
private final AccountRepository accountRepo;
public StaleEntityDemo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void demonstrateStale() {
Account acc = accountRepo.findById("SAV2001").orElseThrow();
System.out.println("Before: " + acc.getStatus()); // ACTIVE
accountRepo.freezeOverdrawnWrong(); // bulk UPDATE
Account same = accountRepo.findById("SAV2001").orElseThrow();
System.out.println("After (no clear): " + same.getStatus());
// Still ACTIVE! Entity from persistence context cache
}
@Transactional
public void demonstrateFresh() {
Account acc = accountRepo.findById("SAV2001").orElseThrow();
System.out.println("Before: " + acc.getStatus()); // ACTIVE
accountRepo.freezeOverdrawnCorrect(); // bulk UPDATE + clear
Account fresh = accountRepo.findById("SAV2001").orElseThrow();
System.out.println("After (cleared): " + fresh.getStatus());
// FROZEN! Fresh read from database
}
}
demonstrate Stale:
Before: ACTIVE
After (no clear): ACTIVE <-- STALE! DB says FROZEN
demonstrateFresh:
Before: ACTIVE
After (cleared): FROZEN <-- CORRECT!
What does flushAutomatically do?
flushAutomatically = true flushes all pending Hibernate changes to the database before executing the bulk statement. Without this, if you modified an entity in memory but Hibernate has not flushed yet, the bulk statement will not see those changes.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class FlushDemo {
private final AccountRepository accountRepo;
public FlushDemo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void withoutFlush() {
Account acc = accountRepo.findById("SAV2001").orElseThrow();
acc.setBalance(new BigDecimal("-500")); // in memory only
// Without flushAutomatically: Hibernate has NOT flushed
// The bulk UPDATE does not see the -500 balance
int frozen = accountRepo.freezeOverdrawnAccounts(
BigDecimal.ZERO);
// SAV2001 may NOT be frozen because DB still has old balance
}
}
// With flushAutomatically=true:
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("UPDATE Account a SET a.status = 'FROZEN' " +
"WHERE a.balance < :threshold")
int freezeOverdrawnFull(@Param("threshold") BigDecimal threshold);
// Flush happens BEFORE the UPDATE -- pending changes are visible
// Clear happens AFTER the UPDATE -- stale entities evicted
What are common mistakes?
The first mistake is forgetting @Transactional on the service method that calls a @Modifying query.
// WRONG: no @Transactional
public void freezeAccounts() {
accountRepo.freezeOverdrawnAccounts(BigDecimal.ZERO);
// Throws: InvalidDataAccessApiUsageException
// "Executing an update/delete query"
}
// CORRECT: wrapped in @Transactional
@Transactional
public void freezeAccounts() {
accountRepo.freezeOverdrawnAccounts(BigDecimal.ZERO);
}
The second mistake is not using clearAutomatically and then reading entities that were just bulk-updated, getting stale data.
What should a senior engineer know?
A senior engineer knows that @Modifying with JPQL executes through Hibernate's query engine, while @Modifying with nativeQuery = true executes raw SQL. Native queries bypass Hibernate's entity mapping entirely, so clearAutomatically is even more critical because Hibernate has no way to know which entities were affected.
Senior engineers also know that multiple @Modifying statements within one @Transactional method commit together atomically, which is sufficient for single-database bulk operations without needing JTA. Cross-resource atomicity is where JTA applies, but the outbox pattern avoids it.
// Native query: bypasses Hibernate entirely
@Modifying(clearAutomatically = true)
@Query(value = "UPDATE accounts SET status = 'FROZEN' " +
"WHERE balance < 0", nativeQuery = true)
int freezeOverdrawnNative();
// Hibernate does not know which entities changed
// clearAutomatically is essential
What are common interview follow-up questions?
- What exception does Spring throw if @Modifying runs without @Transactional?
- Can @Modifying be used with INSERT statements in JPA?
- What is the difference between clearAutomatically and manually calling entityManager.clear()?
- When would you choose JTA/XA over the Outbox pattern?
- How do you test a @Modifying query with stale entity detection in a @SpringBootTest?
Visual Overview
@Modifying with clearAutomatically and flushAutomatically:
┌──────────────────────────────────────────────────┐
│ @Transactional │
│ │
│ 1. acc.setBalance(-500) [in-memory change] │
│ │
│ 2. flushAutomatically=true │
│ └─▶ Hibernate flushes: UPDATE SET balance=-500│
│ │
│ 3. @Modifying @Query: UPDATE SET status='FROZEN' │
│ └─▶ Now sees balance=-500 in DB │
│ │
│ 4. clearAutomatically=true │
│ └─▶ entityManager.clear() │
│ │
│ 5. accountRepo.findById("SAV2001") │
│ └─▶ Fresh read from DB: status=FROZEN ✓ │
│ │
│ 6. COMMIT │
└──────────────────────────────────────────────────┘
Key Takeaway
Distributed transactions with JTA and XA coordinate multiple resources into one atomic unit using two-phase commit. In the prepare phase every resource votes, and in the commit phase the coordinator tells them all to commit. The protocol is blocking: between prepare and commit, resources hold their locks across network round-trips, and a coordinator crash can leave them stuck. For that reason, microservices prefer the Transactional Outbox pattern with Saga, where each service commits locally and events relay asynchronously.
The @Modifying annotation tells Spring Data that a @Query is a bulk UPDATE or DELETE rather than a SELECT. These queries must run inside @Transactional. Because a bulk statement bypasses the persistence context, setting clearAutomatically=true clears the context afterward so subsequent reads get fresh data from the database. Setting flushAutomatically=true flushes pending Hibernate changes before the bulk statement so the statement sees them.
In the National Bank of India nightly maintenance job, Rajesh freezes overdrawn accounts and purges closed zero-balance accounts using @Modifying queries. Both DML statements commit together within one database, which is atomic without needing JTA. Cross-resource atomicity is where JTA would apply, but the outbox pattern gives reliability without the blocking cost.
Remember one sentence: @Modifying marks DML queries that need @Transactional, and always set clearAutomatically=true to avoid stale entities.
---