Transaction Management Temelleri
Giriş — Neden Transaction?
Bir an için kendini banka müşterisi olarak düşün. Hesabından 5.000 TL başka bir hesaba transfer ediyorsun. Sistem senin hesabından 5.000 TL düştü ama tam o sırada elektrik kesildi — karşı tarafa para yatmadı. Sen 5.000 TL kaybettin, karşı taraf almadı. Para buharlaştı mı?
İşte transaction tam olarak bunu engelleyen mekanizma. Ya her şey başarılı olur, ya da hiçbir şey olmamış gibi geri alınır. Yazılımda "yarım iş" kabul edilemez — özellikle parayla, stokla, siparişle uğraşıyorsan.
Spring Boot'ta transaction yönetimi belki de en kritik konulardan biri. Yanlış yapılandırılmış bir transaction, production'da veri tutarsızlığına, müşteri kaybına ve gece 3'te alarm çalmalarına yol açar. Bu derste transaction'ı temelden öğrenecek, Spring'in @Transactional mekanizmasını derinlemesine anlayacaksın.
1. ACID Prensipleri — Transaction'ın 4 Kuralı
Her transaction 4 temel kurala uymalıdır. Bunlara ACID denir:
Atomicity (Atomiklik) — Ya Hep Ya Hiç
Analoji: Düşün ki bir kutuya 3 hediye koyacaksın. Ya 3'ünü de koyarsın ve kutuyu kapatırsın, ya da hiçbirini koymamış olursun. Kutuya 2 hediye koyup yarıda bırakmak yok.
Atomicity, bir transaction içindeki tüm işlemlerin tek bir birim olarak çalışmasını garanti eder. Hepsi başarılı olursa commit, herhangi biri başarısız olursa hepsi rollback edilir.
// Atomicity örneği: Banka transferi
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance().subtract(amount)); // 1. adım: düş
to.setBalance(to.getBalance().add(amount)); // 2. adım: ekle
accountRepository.save(from);
accountRepository.save(to);
// İkisi de başarılı → commit. Biri bile fail → ikisi de rollback.
}Consistency (Tutarlılık) — Kurallar Her Zaman Geçerli
Analoji: Satranç tahtasında hamle yapıyorsun. Hamleden önce tahta geçerli bir durumda, hamleden sonra da geçerli olmalı. Veziri tahtanın dışına koyamazsın.
Transaction öncesi ve sonrası veritabanı tutarlı bir durumda olmalıdır. Foreign key, unique constraint, check constraint gibi kurallar asla ihlal edilmez.
// Consistency: Bakiye negatif olamaz
@Transactional
public void withdraw(Long accountId, BigDecimal amount) {
Account account = accountRepository.findById(accountId).orElseThrow();
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException("Yetersiz bakiye!");
// Transaction rollback olur — tutarsız duruma düşmez
}
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
}Isolation (İzolasyon) — Her Transaction Kendi Dünyasında
Analoji: Sınav salonunda herkes kendi kağıdını çözüyor. Yan masadakinin cevaplarını görmen engelleniyor. Her öğrenci izole.
Eş zamanlı çalışan transaction'lar birbirini etkilemez. Biri diğerinin yarım kalmış verisini göremez (veya görme derecesi isolation level'a bağlıdır).
Durability (Kalıcılık) — Söz Verdiysen Tutar
Analoji: Noterden tasdikli bir sözleşme imzaladın. Deprem olsa, yangın çıksa bile o sözleşme geçerli.
Transaction commit edildikten sonra veriler kalıcıdır. Sunucu çökse bile, veritabanı restart olduğunda commit edilmiş veriler kaybolmaz. Bu, veritabanının WAL (Write-Ahead Logging) mekanizmasıyla sağlanır.
💡 İpucu: ACID prensiplerini mülakatarda çok soruyorlar. "Bana ACID'i gerçek hayattan örnekle anlat" diye sorulduğunda banka transferi analojisi her zaman işe yarar.
2. Spring'de @Transactional — Sihirli Annotation
Spring'de transaction yönetimi için en yaygın yol @Transactional annotation'ıdır. Bu annotation bir metoda konduğunda, Spring otomatik olarak:
Metod çağrılmadan önce transaction başlatır
Metod başarılı biterse commit eder
Exception fırlarsa rollback eder
Temel Kullanım
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final StockService stockService;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepository,
StockService stockService,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.stockService = stockService;
this.paymentService = paymentService;
}
@Transactional
public Order createOrder(OrderRequest request) {
// 1. Sipariş oluştur
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setStatus(OrderStatus.PENDING);
order = orderRepository.save(order);
// 2. Stok düş
stockService.decreaseStock(request.getProductId(), request.getQuantity());
// 3. Ödeme al
paymentService.processPayment(request.getPaymentInfo(), order.getTotalAmount());
// 4. Siparişi onayla
order.setStatus(OrderStatus.CONFIRMED);
return orderRepository.save(order);
// Herhangi bir adımda exception → hepsi rollback
}
}@Transactional Nereye Konulur?
@Transactional hem class hem method seviyesinde konabilir:
// Method seviyesinde — sadece bu metod transactional
@Service
public class UserService {
@Transactional
public void createUser(UserDTO dto) {
// transaction içinde
}
public User getUser(Long id) {
// transaction dışında (veya default propagation'a göre)
}
}
// Class seviyesinde — tüm public metodlar transactional
@Service
@Transactional
public class OrderService {
public void createOrder(OrderRequest req) {
// transaction içinde
}
@Transactional(readOnly = true) // override edilebilir
public Order getOrder(Long id) {
// read-only transaction
}
}⚠️ Dikkat:
@Transactionalsadece public metodlarda çalışır. Private veya protected metodlara koyarsan Spring bunu sessizce yoksayar — hata bile vermez! Bu çok sık yapılan bir hatadır.
Hangi @Transactional?
İki tane @Transactional annotation var. Doğru olanı import et:
// ✅ DOĞRU — Spring'in kendi annotation'ı
import org.springframework.transaction.annotation.Transactional;
// ❌ YANLIŞ — Jakarta/JPA annotation'ı (Spring ile tam uyumlu değil)
import jakarta.transaction.Transactional;Spring'in annotation'ı çok daha fazla parametre sunar (propagation, isolation, timeout, readOnly, rollbackFor...). Her zaman org.springframework.transaction.annotation.Transactional kullan.
3. Transaction Propagation — Transaction'lar Arası İlişki
Bir transactional metod başka bir transactional metodu çağırdığında ne olur? Yeni bir transaction mı açılır, yoksa mevcut transaction'a mı katılır? İşte propagation bu soruyu cevaplar.
REQUIRED (Varsayılan)
"Varsa katıl, yoksa yenisini aç."
Bu varsayılan davranıştır. Çoğu durumda istediğin budur.
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED) // default, yazmasan da bu
public void createOrder(OrderRequest request) {
orderRepository.save(order);
stockService.decreaseStock(request.getProductId(), request.getQuantity());
// stockService de REQUIRED ise → aynı transaction'a katılır
}
}
@Service
public class StockService {
@Transactional // REQUIRED (default)
public void decreaseStock(Long productId, int quantity) {
// OrderService'in transaction'ını kullanır
// Burada exception olursa → her şey rollback olur
Product product = productRepository.findById(productId).orElseThrow();
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}Senaryo: createOrder bir transaction başlatır. decreaseStock çağrıldığında zaten aktif bir transaction var → ona katılır. İkisi aynı transaction içinde çalışır. decreaseStock'ta hata olursa her ikisi de rollback olur.
REQUIRES_NEW
"Her zaman yeni transaction aç. Mevcut varsa askıya al."
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action, String details) {
AuditLog log = new AuditLog();
log.setAction(action);
log.setDetails(details);
log.setTimestamp(LocalDateTime.now());
auditLogRepository.save(log);
// Bu log her zaman kaydedilir — ana transaction fail etse bile
}
}
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest request) {
try {
orderRepository.save(order);
stockService.decreaseStock(request.getProductId(), request.getQuantity());
paymentService.processPayment(request.getPaymentInfo(), order.getTotalAmount());
} catch (Exception e) {
// Ana transaction rollback olsa bile...
auditService.logAction("ORDER_FAILED", e.getMessage());
// ...audit log KAYIT EDİLİR (kendi transaction'ında commit oldu)
throw e;
}
}
}Ne zaman kullanılır? Audit log, notification, hata kaydı gibi ana işlemden bağımsız olması gereken işlemlerde.
⚠️ Dikkat:
REQUIRES_NEWayrı bir database connection kullanır. Çok sık kullanırsan connection pool tükenebilir. Dikkatli ol.
NESTED
"Mevcut transaction içinde bir savepoint oluştur."
@Service
public class OrderService {
@Transactional
public void createOrderWithBonus(OrderRequest request) {
// Ana iş
Order order = orderRepository.save(new Order(request));
try {
bonusService.grantBonus(order.getCustomerId(), 100);
// NESTED: Bonus verilmezse sadece bonus rollback olur
} catch (Exception e) {
// Bonus verilemedi ama sipariş devam eder
log.warn("Bonus verilemedi: {}", e.getMessage());
}
// Sipariş yine kaydedilir
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
}
}
@Service
public class BonusService {
@Transactional(propagation = Propagation.NESTED)
public void grantBonus(Long customerId, int points) {
// Savepoint ile çalışır
// Hata olursa sadece bu kısım rollback olur
bonusRepository.addPoints(customerId, points);
}
}💡 İpucu:
NESTEDtüm transaction manager'lar tarafından desteklenmez. JPA/Hibernate ileJpaTransactionManagerkullanıyorsan çalışır, ama dikkatli test et. JDBCDataSourceTransactionManagerile sorunsuz çalışır.
SUPPORTS
"Transaction varsa katıl, yoksa transaction'sız çalış."
@Service
public class ReportService {
@Transactional(propagation = Propagation.SUPPORTS)
public Report generateReport(Long reportId) {
// Transaction varsa içinde çalışır
// Transaction yoksa transaction'sız çalışır
// Read-only işlemler için uygundur
return reportRepository.generateMonthlyReport(reportId);
}
}MANDATORY
"Transaction olmalı. Yoksa hata fırlat."
@Service
public class StockService {
@Transactional(propagation = Propagation.MANDATORY)
public void decreaseStock(Long productId, int quantity) {
// Bu metod her zaman bir transaction içinden çağrılmalı
// Transaction olmadan çağrılırsa → IllegalTransactionStateException
Product product = productRepository.findById(productId).orElseThrow();
product.setStock(product.getStock() - quantity);
}
}
// Doğru kullanım:
@Transactional
public void createOrder(OrderRequest req) {
stockService.decreaseStock(req.getProductId(), req.getQuantity()); // ✅ OK
}
// Yanlış kullanım:
public void someMethod() {
stockService.decreaseStock(1L, 5); // ❌ IllegalTransactionStateException!
}Ne zaman kullanılır? Bir metodun asla tek başına çağrılmaması gerektiğini garanti etmek istediğinde. "Bu metodu transaction'sız çağırırsan hata alırsın" demek gibi.
NOT_SUPPORTED
"Transaction'ı askıya al, transaction'sız çalış."
@Service
public class HeavyReportService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public byte[] generateHeavyPdfReport(Long reportId) {
// Uzun süren işlem — transaction tutmak gereksiz
// Mevcut transaction varsa askıya alınır
// DB connection'ı gereksiz yere meşgul etmez
return pdfGenerator.generate(reportId);
}
}Ne zaman kullanılır? Uzun süren, transaction gerektirmeyen işlemlerde. PDF üretimi, dosya işleme, harici API çağrıları gibi.
NEVER
"Transaction olmamalı. Varsa hata fırlat."
@Service
public class CacheService {
@Transactional(propagation = Propagation.NEVER)
public void refreshCache() {
// Bu metod kesinlikle transaction dışında çalışmalı
// Transaction içinden çağrılırsa → IllegalTransactionStateException
cache.evictAll();
cache.warmUp();
}
}Propagation Özet Tablosu
| Propagation | Mevcut TX var | Mevcut TX yok |
|---|---|---|
| REQUIRED | Katılır | Yenisini açar |
| REQUIRES_NEW | Askıya alır, yenisini açar | Yenisini açar |
| NESTED | Savepoint oluşturur | Yenisini açar |
| SUPPORTS | Katılır | TX'siz çalışır |
| MANDATORY | Katılır | ❌ Exception fırlatır |
| NOT_SUPPORTED | Askıya alır, TX'siz çalışır | TX'siz çalışır |
| NEVER | ❌ Exception fırlatır | TX'siz çalışır |
4. Isolation Levels — Eş Zamanlılık Kontrolü
Birden fazla transaction aynı anda çalıştığında bazı anomaliler (sorunlar) ortaya çıkabilir. Isolation level, bu sorunlardan hangilerine tolerans gösterdiğini belirler.
Anomali Türleri
Dirty Read — Kirli Okuma
Transaction A bir veriyi güncelledi ama henüz commit etmedi. Transaction B bu güncellenmiş (commit edilmemiş) veriyi okudu. Transaction A rollback etti. Transaction B artık hiç var olmamış bir veriyle çalışıyor.
Zaman Transaction A Transaction B
───── ───────────── ─────────────
T1 UPDATE balance = 1000
(commit etmedi)
T2 SELECT balance → 1000 (dirty read!)
T3 ROLLBACK (balance = 500)
T4 balance = 1000 ile işlem yapıyor
AMA gerçek değer 500!Non-Repeatable Read — Tekrarlanamayan Okuma
Transaction B aynı satırı iki kez okuyor. Arada Transaction A o satırı güncelleyip commit ediyor. İki okuma farklı sonuç veriyor.
Zaman Transaction A Transaction B
───── ───────────── ─────────────
T1 SELECT balance → 500
T2 UPDATE balance = 1000
T3 COMMIT
T4 SELECT balance → 1000 (farklı!)
Aynı query, farklı sonuç!Phantom Read — Hayalet Okuma
Transaction B bir koşulla satır listesi okuyor. Arada Transaction A yeni satır ekliyor. Transaction B aynı sorguyu tekrar çalıştırıyor ve daha fazla satır görüyor.
Zaman Transaction A Transaction B
───── ───────────── ─────────────
T1 SELECT * WHERE city='IST' → 3 satır
T2 INSERT INTO users
(city='IST')
T3 COMMIT
T4 SELECT * WHERE city='IST' → 4 satır
Hayalet satır belirdi!Isolation Level'lar
READ_UNCOMMITTED — En Düşük İzolasyon
Commit edilmemiş veriyi okuyabilirsin. Dirty read mümkün. Hemen hemen hiç kullanılmaz.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public BigDecimal getApproximateTotal() {
// Kesinlik gerekmiyorsa — istatistik, dashboard vs.
return orderRepository.sumTotalAmount();
}READ_COMMITTED — PostgreSQL Varsayılanı
Sadece commit edilmiş veriyi okursun. Dirty read engellenir. Çoğu uygulama için yeterli.
@Transactional(isolation = Isolation.READ_COMMITTED)
public Account getAccount(Long id) {
// Dirty read yok — sadece commit edilmiş veri okunur
// Ama non-repeatable read olabilir
return accountRepository.findById(id).orElseThrow();
}REPEATABLE_READ — MySQL Varsayılanı
Bir transaction içinde aynı satırı birden fazla kez okursan hep aynı değeri görürsün. Non-repeatable read de engellenir.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferWithVerification(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
// ... çeşitli kontroller ...
// Aynı hesabı tekrar okusak bile aynı değeri görürüz
Account fromAgain = accountRepository.findById(fromId).orElseThrow();
// from.getBalance() == fromAgain.getBalance() garanti
from.setBalance(from.getBalance().subtract(amount));
accountRepository.save(from);
}SERIALIZABLE — En Yüksek İzolasyon
Transaction'lar sanki sırayla çalışıyormuş gibi davranır. Tüm anomaliler engellenir ama performans çok düşer.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalFinancialOperation(Long accountId, BigDecimal amount) {
// Tam izolasyon — en güvenli ama en yavaş
// Phantom read dahil hiçbir anomali olmaz
// Dikkat: Deadlock riski yüksek!
Account account = accountRepository.findById(accountId).orElseThrow();
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);
}Isolation Level Karşılaştırma Tablosu
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Performans |
|---|---|---|---|---|
| READ_UNCOMMITTED | ✅ Olabilir | ✅ Olabilir | ✅ Olabilir | ⚡⚡⚡⚡ En Hızlı |
| READ_COMMITTED | ❌ Engellenir | ✅ Olabilir | ✅ Olabilir | ⚡⚡⚡ Hızlı |
| REPEATABLE_READ | ❌ Engellenir | ❌ Engellenir | ✅ Olabilir | ⚡⚡ Orta |
| SERIALIZABLE | ❌ Engellenir | ❌ Engellenir | ❌ Engellenir | ⚡ Yavaş |
💡 İpucu: Çoğu uygulama için
READ_COMMITTED(PostgreSQL default) yeterlidir.SERIALIZABLEsadece gerçekten kritik finansal işlemlerde kullan. Aradaki fark 10x-100x performans farkı olabilir.
⚠️ Dikkat: Spring'de
@Transactional(isolation = ...)ayarı, veritabanının o isolation level'ı desteklemesine bağlıdır. MySQL ve PostgreSQL'in varsayılanları farklıdır. Veritabanı sürücüsü desteklemiyorsa hata alırsın veya sessizce görmezden gelinir.
5. readOnly Optimizasyonu
Sadece okuma yapan işlemlerde readOnly = true kullanmak önemli bir optimizasyon sağlar:
@Service
public class ProductService {
@Transactional(readOnly = true)
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Transactional(readOnly = true)
public ProductDetailDTO getProductDetail(Long id) {
Product product = productRepository.findById(id).orElseThrow();
// Lazy-loaded ilişkiler de yüklenebilir — transaction hâlâ açık
return ProductDetailDTO.from(product);
}
}readOnly Ne Yapar?
Hibernate Dirty Checking kapatılır: Hibernate normalde her entity'nin değişip değişmediğini kontrol eder (dirty checking).
readOnly = trueile bu kontrol yapılmaz → daha az CPU ve bellek.Flush mode MANUAL olur: Hibernate hiçbir değişikliği veritabanına yazmaz. Yanlışlıkla
entity.setName("yeni")desen bile DB'ye yansımaz.Veritabanı seviyesinde optimizasyon: Bazı veritabanları (PostgreSQL gibi) read-only transaction'larda daha agresif cache kullanır.
Replica routing: Spring'de
AbstractRoutingDataSourceile read-only transaction'ları otomatik olarak read replica'ya yönlendirebilirsin — master'ın yükünü azaltır.
// Read replica routing örneği
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "replica" // readOnly = true → replica'ya git
: "primary"; // readOnly = false → primary'ye git
}
}💡 İpucu: Her read metoduna
@Transactional(readOnly = true)koymak iyi bir pratiktir. Class seviyesinde@Transactional(readOnly = true)koyup, yazma metodlarında@Transactionalile override etmek yaygın bir pattern'dır:
@Service
@Transactional(readOnly = true) // Tüm metodlar default olarak read-only
public class ProductService {
public List<Product> getAll() { ... } // readOnly = true (class'tan)
public Product getById(Long id) { ... } // readOnly = true (class'tan)
@Transactional // Override: readOnly = false (default)
public Product create(ProductDTO dto) { ... } // yazma işlemi
@Transactional
public void delete(Long id) { ... } // yazma işlemi
}6. rollbackFor ve noRollbackFor
Spring'in rollback davranışı varsayılan olarak şöyledir:
Unchecked Exception (RuntimeException ve alt sınıfları) → Rollback
Checked Exception (Exception ve alt sınıfları, RuntimeException hariç) → Rollback YAPMAZ
Bu çok önemli ve çok sık unutulan bir kural!
Varsayılan Davranış
@Transactional
public void riskyOperation() {
orderRepository.save(order);
// RuntimeException → Rollback olur ✅
throw new IllegalStateException("Bir şeyler ters gitti");
}
@Transactional
public void anotherOperation() throws IOException {
orderRepository.save(order);
// Checked Exception → Rollback OLMAZ ❌
throw new IOException("Dosya bulunamadı");
// Sipariş kaydedilir ama dosya hatası var — tutarsız durum!
}rollbackFor ile Kontrol
// Checked exception'da da rollback yap
@Transactional(rollbackFor = Exception.class)
public void safeOperation() throws IOException {
orderRepository.save(order);
throw new IOException("Dosya hatası");
// Artık rollback olur ✅
}
// Birden fazla exception tipi
@Transactional(rollbackFor = {IOException.class, CustomBusinessException.class})
public void multiExceptionOperation() throws Exception {
// IOException veya CustomBusinessException → rollback
}noRollbackFor ile İstisna
// Belirli exception'larda rollback yapma
@Transactional(noRollbackFor = ProductNotFoundException.class)
public void processOrders(List<Long> productIds) {
for (Long productId : productIds) {
try {
processOrder(productId);
} catch (ProductNotFoundException e) {
// Bu ürün bulunamadı ama diğerlerine devam et
// Transaction rollback olmaz
log.warn("Ürün bulunamadı: {}", productId);
}
}
}⚠️ Dikkat: En güvenli yaklaşım her zaman
@Transactional(rollbackFor = Exception.class)kullanmaktır. Böylece checked exception'lar da rollback'e dahil olur. Spring Boot 6 (Spring Framework 7) ile bu davranış değişebilir, ama şu an (Spring Boot 3.x) varsayılan hâlâ sadece unchecked exception'larda rollback'tir.
Kendi Exception Hiyerarşin
// Base business exception — checked
public class BusinessException extends Exception {
public BusinessException(String message) {
super(message);
}
}
// Rollback gerektiren
public class CriticalBusinessException extends BusinessException {
public CriticalBusinessException(String message) {
super(message);
}
}
// Rollback gerektirmeyen
public class WarningBusinessException extends BusinessException {
public WarningBusinessException(String message) {
super(message);
}
}
@Transactional(
rollbackFor = CriticalBusinessException.class,
noRollbackFor = WarningBusinessException.class
)
public void complexOperation() throws BusinessException {
// CriticalBusinessException → rollback
// WarningBusinessException → commit devam eder
}7. Self-Invocation Trap — En Sinsi Tuzak
Bu, Spring Transaction'ın en sık karşılaşılan ve en sinsi hatasıdır. Aynı class içinde bir metod başka bir @Transactional metodu çağırdığında, transaction çalışmaz.
Problem
@Service
public class OrderService {
// ❌ BU ÇALIŞMAZ!
public void processOrder(OrderRequest request) {
// Validasyon vs.
validateOrder(request);
// Aynı class içinde @Transactional metodu çağırıyoruz
createOrderInternal(request); // Transaction UYGULANMAZ!
}
@Transactional
public void createOrderInternal(OrderRequest request) {
orderRepository.save(new Order(request));
stockService.decreaseStock(request.getProductId(), request.getQuantity());
paymentService.processPayment(request.getPaymentInfo());
// Exception olursa rollback OLMAZ — çünkü transaction yok!
}
}Neden Çalışmaz? — Proxy Mekanizması
Spring @Transactional'ı AOP Proxy ile uygular. Bir bean inject edildiğinde, aslında o bean'in proxy'si inject edilir:
Dış dünya (Controller)
│
▼
┌─────────────────┐
│ OrderService │
│ PROXY │ ← Spring'in oluşturduğu proxy
│ ┌─────────────┐ │
│ │ Transaction │ │ ← Before: TX başlat
│ │ Interceptor │ │ ← After: TX commit/rollback
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Gerçek │ │
│ │ OrderService │ │ ← Asıl iş burada
│ └─────────────┘ │
└─────────────────┘Controller orderService.createOrderInternal() çağırdığında:
Çağrı proxy'ye gider
Proxy transaction başlatır
Proxy asıl metoda yönlendirir
Metod biter, proxy commit/rollback yapar
Ama processOrder → createOrderInternal çağrısı aynı obje içinde:
processOrderzaten proxy'den geçtithis.createOrderInternal()çağrısı proxy'yi atlarDoğrudan asıl metoda gider
Transaction interceptor hiç devreye girmez
// Aslında olan şey:
public void processOrder(OrderRequest request) {
validateOrder(request);
this.createOrderInternal(request); // "this" = gerçek obje, proxy DEĞİL
}Çözüm 1: Ayrı Service'e Taşı (Önerilen)
@Service
public class OrderService {
private final OrderCreationService orderCreationService;
public void processOrder(OrderRequest request) {
validateOrder(request);
orderCreationService.createOrder(request); // Başka bean → proxy'den geçer ✅
}
}
@Service
public class OrderCreationService {
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(new Order(request));
stockService.decreaseStock(request.getProductId(), request.getQuantity());
paymentService.processPayment(request.getPaymentInfo());
// Transaction düzgün çalışır ✅
}
}Çözüm 2: Self-Injection (Geçici Çözüm)
@Service
public class OrderService {
@Lazy
@Autowired
private OrderService self; // Kendini inject et (proxy olarak gelir)
public void processOrder(OrderRequest request) {
validateOrder(request);
self.createOrderInternal(request); // Proxy üzerinden çağrılır ✅
}
@Transactional
public void createOrderInternal(OrderRequest request) {
// Transaction çalışır ✅
orderRepository.save(new Order(request));
stockService.decreaseStock(request.getProductId(), request.getQuantity());
}
}⚠️ Dikkat: Self-injection teknik olarak çalışır ama code smell'dir. Ayrı service'e taşımak her zaman daha temiz çözümdür. Self-injection kullanıyorsan
@Lazyeklemeyi unutma, yoksa circular dependency hatası alırsın.
Çözüm 3: ApplicationContext'ten Al (Önerilmez)
@Service
public class OrderService implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.applicationContext = ctx;
}
public void processOrder(OrderRequest request) {
validateOrder(request);
// Proxy'yi ApplicationContext'ten al
OrderService proxy = applicationContext.getBean(OrderService.class);
proxy.createOrderInternal(request); // ✅ Çalışır ama çirkin
}
@Transactional
public void createOrderInternal(OrderRequest request) { ... }
}Bu çözüm çalışır ama kodu çirkinleştirir ve test etmeyi zorlaştırır. Kullanma.
8. Transaction Logging
Transaction'ların doğru çalıştığını görmek çok önemlidir. Özellikle debug aşamasında:
Logging Konfigürasyonu
# application.yml
logging:
level:
# Transaction boundary'lerini görmek için
org.springframework.transaction: TRACE
org.springframework.transaction.interceptor: TRACE
# SQL sorgularını görmek için
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
# JPA transaction yönetimini görmek için
org.springframework.orm.jpa: DEBUG# application.properties alternatifi
logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.hibernate.SQL=DEBUGLog Çıktısı Nasıl Okunur?
TRACE - Getting transaction for [com.example.OrderService.createOrder]
DEBUG - Creating new transaction with name [createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG - Opened new EntityManager for JPA transaction
DEBUG - Exposing JPA transaction as JDBC [...]
...
TRACE - Completing transaction for [com.example.OrderService.createOrder]
DEBUG - Initiating transaction commit
DEBUG - Committing JPA transaction on EntityManager [...]
DEBUG - Closing JPA EntityManager after transactionEğer rollback olursa:
TRACE - Completing transaction for [com.example.OrderService.createOrder] after exception: java.lang.RuntimeException
DEBUG - Initiating transaction rollback
DEBUG - Rolling back JPA transaction on EntityManager [...]Custom Transaction Event Listener
Spring 4.2+ ile transaction event'lerini dinleyebilirsin:
@Component
public class TransactionEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(OrderCreatedEvent event) {
// Transaction başarıyla commit edildikten sonra çalışır
// Email gönder, notification at, cache güncelle
log.info("Sipariş başarıyla oluşturuldu: {}", event.getOrderId());
emailService.sendOrderConfirmation(event.getOrderId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(OrderCreatedEvent event) {
// Transaction rollback edildikten sonra çalışır
log.error("Sipariş oluşturulamadı: {}", event.getOrderId());
}
}
// Event publish etme
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
// Event yayınla — ama listener COMMIT sonrası çalışır
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
return order;
}
}💡 İpucu:
@TransactionalEventListenerçok güçlü bir araçtır. Email gönderimi, cache güncellemesi gibi yan etkileri transaction commit sonrasına taşıyabilirsin. Transaction rollback olursa email gitmez — tutarlılık sağlanır.
9. Bütünleşik Örnek: E-Ticaret Sipariş Sistemi
Şimdi öğrendiğimiz her şeyi birleştiren gerçek bir e-ticaret senaryosu yapalım:
Entity'ler
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long customerId;
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items = new ArrayList<>();
private LocalDateTime createdAt;
// getters, setters
}
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
@Version
private Long version; // Optimistic locking için
// getters, setters
}
@Entity
@Table(name = "payments")
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long orderId;
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private PaymentStatus status;
private LocalDateTime processedAt;
// getters, setters
}Service Katmanı
@Service
@Transactional(readOnly = true) // Default: read-only
public class OrderService {
private final OrderRepository orderRepository;
private final StockService stockService;
private final PaymentService paymentService;
private final AuditService auditService;
private final ApplicationEventPublisher eventPublisher;
// Constructor injection...
@Transactional(rollbackFor = Exception.class) // Yazma: tüm exception'larda rollback
public OrderResponse createOrder(CreateOrderRequest request) {
// 1. Sipariş oluştur
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setStatus(OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
BigDecimal total = BigDecimal.ZERO;
for (OrderItemRequest itemReq : request.getItems()) {
// 2. Her ürün için stok kontrolü ve düşme
Product product = stockService.decreaseStock(
itemReq.getProductId(),
itemReq.getQuantity()
);
OrderItem item = new OrderItem();
item.setOrder(order);
item.setProductId(product.getId());
item.setProductName(product.getName());
item.setQuantity(itemReq.getQuantity());
item.setUnitPrice(product.getPrice());
order.getItems().add(item);
total = total.add(product.getPrice()
.multiply(BigDecimal.valueOf(itemReq.getQuantity())));
}
order.setTotalAmount(total);
order = orderRepository.save(order);
// 3. Ödeme işle
paymentService.processPayment(order.getId(), total, request.getPaymentMethod());
// 4. Siparişi onayla
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
// 5. Event yayınla (commit sonrası çalışacak)
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
return OrderResponse.from(order);
}
public Order getOrder(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException("Sipariş bulunamadı: " + id));
}
public List<Order> getCustomerOrders(Long customerId) {
return orderRepository.findByCustomerId(customerId);
}
}@Service
public class StockService {
private final ProductRepository productRepository;
@Transactional(propagation = Propagation.MANDATORY)
// MANDATORY: Bu metod her zaman bir transaction içinden çağrılmalı
public Product decreaseStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(
"Ürün bulunamadı: " + productId));
if (product.getStock() < quantity) {
throw new InsufficientStockException(
String.format("Yetersiz stok! Ürün: %s, Mevcut: %d, İstenen: %d",
product.getName(), product.getStock(), quantity));
}
product.setStock(product.getStock() - quantity);
return productRepository.save(product);
}
}@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final PaymentGateway paymentGateway; // Harici ödeme sistemi
@Transactional(propagation = Propagation.REQUIRED)
public Payment processPayment(Long orderId, BigDecimal amount,
PaymentMethod method) {
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus(PaymentStatus.PROCESSING);
payment.setProcessedAt(LocalDateTime.now());
payment = paymentRepository.save(payment);
try {
// Harici ödeme sistemiyle iletişim
PaymentResult result = paymentGateway.charge(amount, method);
if (!result.isSuccess()) {
throw new PaymentFailedException(
"Ödeme başarısız: " + result.getErrorMessage());
}
payment.setStatus(PaymentStatus.COMPLETED);
return paymentRepository.save(payment);
} catch (PaymentGatewayException e) {
payment.setStatus(PaymentStatus.FAILED);
paymentRepository.save(payment);
throw new PaymentFailedException("Ödeme sistemi hatası", e);
}
}
}@Service
public class AuditService {
private final AuditLogRepository auditLogRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
// REQUIRES_NEW: Audit log her zaman kaydedilsin — ana TX fail etse bile
public void logOrderAttempt(Long customerId, String action, String details) {
AuditLog log = new AuditLog();
log.setCustomerId(customerId);
log.setAction(action);
log.setDetails(details);
log.setTimestamp(LocalDateTime.now());
auditLogRepository.save(log);
}
}Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final AuditService auditService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid CreateOrderRequest request) {
try {
OrderResponse response = orderService.createOrder(request);
auditService.logOrderAttempt(
request.getCustomerId(), "ORDER_CREATED",
"Order #" + response.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (InsufficientStockException e) {
auditService.logOrderAttempt(
request.getCustomerId(), "ORDER_FAILED_STOCK", e.getMessage());
throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage());
} catch (PaymentFailedException e) {
auditService.logOrderAttempt(
request.getCustomerId(), "ORDER_FAILED_PAYMENT", e.getMessage());
throw new ResponseStatusException(
HttpStatus.PAYMENT_REQUIRED, e.getMessage());
}
}
}İşlem Akışı
Controller.createOrder()
│
▼
OrderService.createOrder() ──── Transaction BAŞLAR ────
│
├─► orderRepository.save(order) [TX-1]
│
├─► stockService.decreaseStock() [TX-1 — MANDATORY, aynı TX]
│ ├─► Stok kontrolü
│ └─► productRepository.save()
│
├─► paymentService.processPayment() [TX-1 — REQUIRED, aynı TX]
│ ├─► paymentRepository.save()
│ └─► paymentGateway.charge() [Harici çağrı]
│
├─► order.setStatus(CONFIRMED)
│
└─► eventPublisher.publishEvent()
│
▼ ──── Transaction COMMIT ────
OrderCreatedEvent listener çalışır
└─► emailService.sendConfirmation()Herhangi bir adımda exception olursa → tüm adımlar rollback olur. Stok eski haline döner, sipariş silinir, ödeme kaydı gider. Ama audit log (REQUIRES_NEW) kalır.
10. Yaygın Hatalar ve Tuzaklar
1. @Transactional Private Method'da
// ❌ ÇALIŞMAZ — sessizce yoksayılır
@Transactional
private void doSomething() {
// Transaction YOK
}
// ✅ Public olmalı
@Transactional
public void doSomething() {
// Transaction VAR
}2. try-catch ile Rollback'i Engellemek
@Transactional
public void dangerousMethod() {
try {
riskyOperation(); // RuntimeException fırlatabilir
} catch (RuntimeException e) {
log.error("Hata!", e);
// Exception yakalandı → Spring rollback yapamaz!
// Transaction commit olur — ama veri tutarsız olabilir
}
}
// ✅ Doğru yaklaşım: Exception'ı yakala ama tekrar fırlat
@Transactional
public void safeMethod() {
try {
riskyOperation();
} catch (RuntimeException e) {
log.error("Hata!", e);
throw e; // Tekrar fırlat → rollback olur
}
}3. Farklı Thread'de Transaction Kaybı
@Transactional
public void asyncOperation() {
orderRepository.save(order);
// ❌ Yeni thread → transaction KAYBOLUR
CompletableFuture.runAsync(() -> {
stockService.decreaseStock(productId, quantity);
// Bu çağrı ana transaction'ın DIŞINDA çalışır
});
}Transaction thread-bound'dır. Yeni bir thread açarsan transaction oraya taşınmaz.
4. Lazy Loading Transaction Dışında
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
// Transaction burada biter
}
// Controller'da:
Order order = orderService.getOrder(1L);
order.getItems().size(); // ❌ LazyInitializationException!
// Transaction kapandı, lazy collection yüklenemezÇözüm: DTO kullan veya @Transactional sınırlarını doğru çiz.
Özet
ACID prensipleri transaction'ın temelidir: Atomicity (ya hep ya hiç), Consistency (kurallar korunur), Isolation (transaction'lar izole), Durability (commit kalıcı)
`@Transactional` Spring'in AOP proxy mekanizmasıyla çalışır; sadece public metodlarda ve dışarıdan çağrılarda aktif olur
Propagation türleri transaction'lar arası ilişkiyi belirler:
REQUIRED(varsayılan, katıl/aç),REQUIRES_NEW(bağımsız),MANDATORY(zorunlu) en çok kullanılanlardırIsolation level eş zamanlılık anomalilerini kontrol eder:
READ_COMMITTEDçoğu uygulama için yeterlidir,SERIALIZABLEsadece kritik işlemlerde kullanılırSelf-invocation trap en sinsi hatadır: aynı class içinde
@Transactionalmetoduthis.method()ile çağırmak proxy'yi atlar — ayrı service'e taşı`readOnly = true` sadece okuma işlemlerinde kullan — Hibernate dirty checking kapatılır, performans artar, read replica routing yapılabilir
AI Asistan
Sorularını yanıtlamaya hazır