← Kursa Dön
📄 Text · 25 min

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:

  1. Metod çağrılmadan önce transaction başlatır

  2. Metod başarılı biterse commit eder

  3. 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: @Transactional sadece 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_NEW ayrı 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: NESTED tüm transaction manager'lar tarafından desteklenmez. JPA/Hibernate ile JpaTransactionManager kullanıyorsan çalışır, ama dikkatli test et. JDBC DataSourceTransactionManager ile 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

PropagationMevcut TX varMevcut TX yok
REQUIREDKatılırYenisini açar
REQUIRES_NEWAskıya alır, yenisini açarYenisini açar
NESTEDSavepoint oluştururYenisini açar
SUPPORTSKatılırTX'siz çalışır
MANDATORYKatılır❌ Exception fırlatır
NOT_SUPPORTEDAskıya alır, TX'siz çalışırTX'siz çalışır
NEVER❌ Exception fırlatırTX'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 LevelDirty ReadNon-Repeatable ReadPhantom ReadPerformans
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. SERIALIZABLE sadece 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?

  1. Hibernate Dirty Checking kapatılır: Hibernate normalde her entity'nin değişip değişmediğini kontrol eder (dirty checking). readOnly = true ile bu kontrol yapılmaz → daha az CPU ve bellek.

  2. 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.

  3. Veritabanı seviyesinde optimizasyon: Bazı veritabanları (PostgreSQL gibi) read-only transaction'larda daha agresif cache kullanır.

  4. Replica routing: Spring'de AbstractRoutingDataSource ile 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 @Transactional ile 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 @TransactionalAOP 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:

  1. Çağrı proxy'ye gider

  2. Proxy transaction başlatır

  3. Proxy asıl metoda yönlendirir

  4. Metod biter, proxy commit/rollback yapar

Ama processOrdercreateOrderInternal çağrısı aynı obje içinde:

  1. processOrder zaten proxy'den geçti

  2. this.createOrderInternal() çağrısı proxy'yi atlar

  3. Doğrudan asıl metoda gider

  4. 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 @Lazy eklemeyi 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=DEBUG

Log Çı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 transaction

Eğ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ır

  • Isolation level eş zamanlılık anomalilerini kontrol eder: READ_COMMITTED çoğu uygulama için yeterlidir, SERIALIZABLE sadece kritik işlemlerde kullanılır

  • Self-invocation trap en sinsi hatadır: aynı class içinde @Transactional metodu this.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