Spring Boot Mülakatı: İşlem Yayılımı Açıklandı

Spring Boot işlem yayılımına hakim olun: REQUIRED, REQUIRES_NEW, NESTED ve daha fazlası. Kod örnekleri ve yaygın tuzaklarla 12 mülakat sorusu.

Spring Boot İşlem Yayılımı: mülakat soruları ve pratik örnekler

İşlem yayılımı, teknik mülakatlarda düzenli olarak değerlendirilen Spring Boot'un temel bir kavramıdır. @Transactional ile işaretlenmiş metotlar arasında işlemlerin nasıl etkileşime girdiğini anlamak, üretimde meydana gelebilecek ince hataları önlemeye ve sağlam mimariler tasarlamaya yardımcı olur.

Hazırlık Tavsiyesi

Mülakat yapanlar, iş bağlamına göre doğru yayılım seviyesini seçme yeteneğini test eder. Belirli bir durumda neden REQUIRED yerine REQUIRES_NEW kullanıldığını açıklayabilmek fark yaratır.

İşlem yayılımının temelleri

Soru 1: Spring'de işlem yayılımı nedir?

Yayılım, mevcut bir işlem bağlamında çağrıldığında işlemsel bir metodun davranışını tanımlar. Şu soruyu yanıtlar: "Bir @Transactional metot, aynı zamanda işaretlenmiş başka bir metodu çağırdığında ne olur?"

OrderService.javajava
// Yayılım kavramının gösterimi
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Üst işlem - yeni bir işlem başlatır
    @Transactional
    public void createOrder(OrderRequest request) {
        // Siparişi mevcut işlemde kaydeder
        Order order = orderRepository.save(new Order(request));

        // Başka bir @Transactional metoda çağrı
        // Yayılım belirler: aynı işlem mi yeni işlem mi?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Varsayılan yayılım: REQUIRED
    // createOrder() içindeki mevcut işleme katılır
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // createOrder() ile AYNI işlemde çalışır
        // Bu metot başarısız olursa, sipariş de geri alınır
    }
}

Spring, her biri belirli iş ihtiyaçlarına uygun yedi yayılım seviyesi sunar. Seçim, veri tutarlılığını ve performansı doğrudan etkiler.

Soru 2: REQUIRED'in (varsayılan yayılım) davranışını açıklayın

REQUIRED varsayılan yayılımdır. Bir işlem varsa, metot ona katılır. Aksi takdirde yeni bir işlem oluşturulur. Bu, en yaygın ve sezgisel davranıştır.

UserService.javajava
// REQUIRED: varsayılan davranış
@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Hiçbir işlem yoksa yeni bir işlem başlatır
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // Audit aynı işleme katılır
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // updateUser() işlemine katılır
        // Birlikte commit veya rollback
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Aşağıdaki diyagram işlemsel akışı göstermektedir:

text
updateUser() TX-1'i başlatır
    ├── save(user)          → TX-1
    └── logUpdate()         → TX-1'e katılır (REQUIRED)
           └── save(audit)  → TX-1

logUpdate() başarısız olursa → TX-1 geri alınır → user VE audit iptal edilir

Soru 3: REQUIRED yerine ne zaman REQUIRES_NEW kullanılmalı?

REQUIRES_NEW, mevcut işlemi askıya alır ve yeni bağımsız bir işlem oluşturur. Bir işlemin üst işlemin sonucundan bağımsız olarak commit edilmesi gerektiğinde kullanışlıdır.

PaymentService.javajava
// REQUIRES_NEW: bağımsız işlem
@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final PaymentAuditService auditService;

    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        Payment payment = new Payment(orderId, amount);
        paymentRepository.save(payment);

        // Ödeme daha sonra başarısız olsa bile audit MUTLAKA kaydedilmeli
        auditService.logPaymentAttempt(orderId, amount);

        // Audit'ten sonra hata simülasyonu
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Negative amount");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW: üst işlemden bağımsız commit eder
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Yeni TX-2 işlemi oluşturuldu
        // TX-1 (processPayment) askıya alınmıştır
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 burada commit eder, TX-1'den bağımsız olarak
    }
}

REQUIRES_NEW ile işlemsel akış:

text
processPayment() TX-1'i başlatır
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 ASKIYA ALINIR
    │       └── TX-2'yi başlatır   → yeni işlem
    │       └── save(audit)        → TX-2
    │       └── COMMIT TX-2        → audit kaydedildi
    │       └── TX-1 DEVAM EDER
    └── throw InvalidAmountException
        └── ROLLBACK TX-1          → ödeme iptal, ama audit korundu
Deadlock'lara dikkat

REQUIRES_NEW, yeni işlem askıya alınmış işlem tarafından kilitlenen aynı kaynaklara erişirse deadlock'lara neden olabilir. Üst işlemle aynı tabloları değiştirmek için REQUIRES_NEW kullanmaktan kaçının.

Soru 4: NESTED yayılımını açıklayın ve REQUIRES_NEW'den nasıl farklı olduğunu belirtin

NESTED, mevcut işlem içinde bir savepoint oluşturur. Metot başarısız olursa, yalnızca savepoint'ten sonraki değişiklikler geri alınır, tüm üst işlem değil.

BatchProcessingService.javajava
// NESTED: üst işlem içinde savepoint
@Service
public class BatchProcessingService {

    private final ItemRepository itemRepository;
    private final ItemProcessor itemProcessor;

    @Transactional
    public BatchResult processBatch(List<Item> items) {
        BatchResult result = new BatchResult();

        for (Item item : items) {
            try {
                // Her item bir savepoint ile işlenir
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Tüm batch değil, sadece bu item rollback
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Başarılı item'ların commit'i
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED: savepoint oluşturur, kısmi rollback mümkün
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // İş doğrulaması
        if (!isValid(item)) {
            throw new ProcessingException("Invalid item");
            // Savepoint'e rollback → sadece bu item
        }

        item.setStatus("COMPLETED");
        itemRepository.save(item);
    }
}

NESTED ve REQUIRES_NEW karşılaştırması:

text
NESTED:
├── Üst TX içinde bir savepoint kullanır
├── Hata durumunda → savepoint'e rollback
├── Üst TX rollback yaparsa → NESTED de rollback olur
└── Daha performanslı (yeni bağlantı yok)

REQUIRES_NEW:
├── Tamamen bağımsız bir işlem oluşturur
├── Hata durumunda → sadece alt TX rollback
├── Üst TX rollback yaparsa → alt TX ZATEN COMMIT EDİLMİŞ
└── Yeni bir bağlantı gerektirir
NESTED Desteği

NESTED yayılımı JDBC savepoint desteği gerektirir. Çoğu modern veritabanı (PostgreSQL, MySQL, Oracle) bunu destekler. Kullanmadan önce uyumluluğu doğrulayın.

Gelişmiş yayılım türleri

Soru 5: SUPPORTS ve NOT_SUPPORTED ne zaman kullanılır?

SUPPORTS, varsa mevcut işlem içinde, yoksa işlemsiz çalışır. NOT_SUPPORTED, mevcut işlemi askıya alır ve işlemsiz çalışır.

ReportingService.javajava
// SUPPORTS: opsiyonel işlem
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS: işlemli veya işlemsiz çalışır
    // İşlemsel garantiler gerektirmeyen okumalar için kullanışlı
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // @Transactional metot tarafından çağrılırsa → onun TX'ini kullanır
        // Doğrudan çağrılırsa → işlem yok (sadece okuma OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED: asla bir işlem içinde değil
    // Yavaş bir dış çağrı sırasında TX'i bloklamaktan kaçınır
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // Çağrı sırasında üst TX askıya alınır
        apiClient.send(message); // Potansiyel yavaş HTTP çağrısı
        // Üst TX sonrasında devam eder
    }
}
OrderService.javajava
// Birleşik kullanım örneği
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ReportingService reportingService;
    private final ExternalNotificationService notificationService;

    @Transactional
    public void completeOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("COMPLETED");
        orderRepository.save(order);

        // Aynı TX içinde rapor oluşturma (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // İşlem DIŞINDA dış bildirim (NOT_SUPPORTED)
        // Dış API yavaşsa TX timeout'unu önler
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Soru 6: MANDATORY ve NEVER'i açıklayın

MANDATORY mevcut bir işlem gerektirir ve aksi takdirde bir istisna fırlatır. NEVER işlem olmamasını gerektirir ve varsa bir istisna fırlatır.

AuditService.javajava
// MANDATORY: bir işlem içinden çağrılmalı
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY: mevcut işlem olmadan çalışmayı reddeder
    // Audit'in audit edilen işlemle her zaman atomik olmasını garanti eder
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // İşlem olmadan çağrılırsa → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER: asla bir işlem içinde olmamalı
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER: cache işlemlere katılmamalı
    // Rollback sonrası cache ile DB arasında tutarsızlıkları önler
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // @Transactional'dan çağrılırsa → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// MANDATORY'nin doğru kullanımı
@Service
public class SecurityService {

    private final AuditService auditService;

    @Transactional
    public void changeUserPassword(Long userId, String newPassword) {
        // Hassas işlem...
        updatePassword(userId, newPassword);

        // Audit aynı işlemde OLMALI
        // MANDATORY bu mimari kısıtlamayı zorunlu kılar
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // HATA: işlem olmadan doğrudan çağrı
    public void badUsage() {
        // TX olmadığı için IllegalTransactionStateException fırlatır
        auditService.logCriticalAction("TEST", 1L);
    }
}

Spring Boot mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Yaygın mülakat tuzakları

Soru 7: Neden @Transactional iç çağrılarda çalışmaz?

En yaygın tuzaklardan biridir. İç metot çağrıları (self-invocation) Spring proxy'sini atlayarak işlem yönetimini devre dışı bırakır.

BrokenService.javajava
// KLASİK HATA: self-invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // TUZAK: iç çağrı → proxy'yi atlar
            // processItem() üzerindeki @Transactional GÖZ ARDI EDİLİR
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Bu işlem iç çağrılarda ASLA oluşturulmaz
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Bu tuzaktan kaçınmak için çözümler:

java
// Çözüm 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Self-injection

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Proxy üzerinden çağrı → @Transactional çalışır
            self.processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // İşlem doğru şekilde oluşturuldu
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

// Çözüm 2: İki servise ayır (önerilen)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Başka bean'e çağrı → proxy çalışır
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    @Transactional
    public void processItem(Long itemId) {
        // İşlem doğru şekilde yönetiliyor
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Soru 8: İstisnalar ve rollback nasıl ele alınır?

Varsayılan olarak Spring yalnızca RuntimeException ve Error'da rollback yapar. Checked istisnalar otomatik rollback'i TETİKLEMEZ.

TransactionRollbackDemo.javajava
// İstisna türüne göre rollback davranışı
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → otomatik ROLLBACK
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Error"); // ROLLBACK
    }

    // Checked Exception → varsayılan olarak rollback YOK
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // Yine de COMMIT eder!
    }

    // Checked exception'da rollback'i zorla
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Error"); // rollbackFor sayesinde ROLLBACK
    }

    // Bir RuntimeException'ı rollback'ten hariç tut
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // İstisnaya rağmen COMMIT
    }
}

İş durumları için önerilen yapılandırma:

BaseTransactionalService.javajava
// Tutarlı işlemsel yapılandırma
@Service
public abstract class BaseTransactionalService {

    // Tüm istisnalarda rollback (checked ve unchecked)
    @Transactional(rollbackFor = Exception.class)
    protected void executeInTransaction(Runnable operation) {
        operation.run();
    }
}

// PaymentService.java
@Service
public class PaymentService extends BaseTransactionalService {

    private final PaymentRepository paymentRepository;

    @Transactional(
        rollbackFor = Exception.class,
        noRollbackFor = InsufficientFundsException.class
    )
    public PaymentResult processPayment(PaymentRequest request) {
        Payment payment = new Payment(request);
        paymentRepository.save(payment);

        if (request.getAmount().compareTo(getBalance()) > 0) {
            // Rollback YAPMAZ - deneme kaydını korumak istiyoruz
            throw new InsufficientFundsException("Insufficient balance");
        }

        return new PaymentResult(payment.getId(), "SUCCESS");
    }
}

Soru 9: İşlem izolasyonu yayılımla nasıl çalışır?

İzolasyon ve yayılım birbirini tamamlar. İzolasyon, eşzamanlı işlemler arasındaki veri görünürlüğünü belirler.

IsolationDemo.javajava
// İzolasyon + yayılım kombinasyonu
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED: diğer işlemlerce commit edilen verileri görür
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // İşlem sırasında yeniden okunursa farklı değerler görebilir
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ: işlem sırasında aynı okumayı garanti eder
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        propagation = Propagation.REQUIRED
    )
    public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        // Başka bir işlem 'from'u bu sırada değiştirse bile,
        // her zaman ilk değeri görürüz (snapshot)
        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);
    }

    // SERIALIZABLE: maksimum izolasyon, eşzamanlılık yok
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Bu veriler üzerindeki diğer tüm işlemleri bloklar
        // Tutumlu kullanın - performans etkisi
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

İzolasyon seviyeleri özet tablosu:

text
| İzolasyon        | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Mümkün     | Mümkün         | Mümkün  |
| READ_COMMITTED   | Hayır      | Mümkün         | Mümkün  |
| REPEATABLE_READ  | Hayır      | Hayır          | Mümkün  |
| SERIALIZABLE     | Hayır      | Hayır          | Hayır   |
Performans etkisi

İzolasyon ne kadar sıkı olursa, performans kilitlerden o kadar etkilenir. SERIALIZABLE, yüksek trafikli üretim ortamlarında önemli rekabete neden olabilir.

Gelişmiş desenler

Soru 10: "Transactional outbox" deseni nasıl uygulanır?

Outbox deseni, hata durumunda bile veritabanı değişiklikleri ile mesaj/olay gönderimi arasında tutarlılığı garanti eder.

OutboxService.javajava
// Transactional Outbox Deseni
@Service
public class OutboxService {

    private final OutboxRepository outboxRepository;

    // Olayı iş varlığı ile aynı işlemde kaydeder
    @Transactional(propagation = Propagation.MANDATORY)
    public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) {
        OutboxEvent event = OutboxEvent.builder()
            .aggregateType(aggregateType)
            .aggregateId(aggregateId)
            .eventType(eventType)
            .payload(payload)
            .status("PENDING")
            .createdAt(Instant.now())
            .build();
        outboxRepository.save(event);
    }
}

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxService outboxService;

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Siparişi oluştur
        Order order = new Order(request);
        orderRepository.save(order);

        // AYNI işlemde outbox olayı (MANDATORY)
        // Commit başarılı olursa → her ikisi de kaydedilir
        // Rollback olursa → hiçbiri kaydedilmez
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Olayları yayınlayan ayrı süreç
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Her yayın için bağımsız işlem
    @Scheduled(fixedDelay = 1000)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void publishPendingEvents() {
        List<OutboxEvent> events = outboxRepository
            .findByStatusOrderByCreatedAt("PENDING");

        for (OutboxEvent event : events) {
            try {
                messageBroker.publish(event.getEventType(), event.getPayload());
                event.setStatus("PUBLISHED");
                event.setPublishedAt(Instant.now());
            } catch (Exception e) {
                event.setStatus("FAILED");
                event.setError(e.getMessage());
            }
            outboxRepository.save(event);
        }
    }
}

Soru 11: Farklı yayılım davranışları nasıl test edilir?

Yayılım testleri, beklenen işlemsel davranışı doğrulamak için özel dikkat gerektirir.

TransactionPropagationTest.javajava
// Yayılım davranışlarını test etme
@SpringBootTest
@Transactional
class TransactionPropagationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void required_shouldShareTransaction() {
        // Given
        Order order = orderService.createOrder(new OrderRequest());

        // When - aynı işlemde ödeme (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - ikisi de commit'ten önce görünür
        entityManager.flush();
        assertThat(entityManager.find(Order.class, order.getId())).isNotNull();
        assertThat(entityManager.find(Payment.class, order.getId())).isNotNull();
    }

    @Test
    void requiresNew_shouldCommitIndependently() {
        // Given
        Long orderId = null;

        try {
            // When - REQUIRES_NEW içinde audit
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Ana işlem rollback yapar
        }

        // Then - audit (REQUIRES_NEW) hâlâ commit edilmiş
        assertThat(findAuditLog(orderId)).isNotNull();
        // Ama sipariş geri alındı
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - aktif işlem yok

        // When/Then - bir istisna fırlatmalı
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Gerçek rollback ve commit ile entegrasyon testi
@SpringBootTest
class PropagationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private AuditLogRepository auditLogRepository;

    @Test
    void nested_shouldRollbackOnlyNestedOnFailure() {
        // Given
        int initialCount = orderRepository.findAll().size();

        // When - NESTED ile batch işleme
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - sadece geçerli item'lar kaydedilir
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Soru 12: İşlem yapılandırması için en iyi uygulamalar nelerdir?

Proje düzeyinde tutarlı bir işlemsel yapılandırma sürprizleri önler ve bakımı kolaylaştırır.

TransactionConfig.javajava
// Merkezi işlem yapılandırması
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Tüm işlemler için varsayılan timeout
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // İşlem başına maksimum 30 saniye
        return tm;
    }
}

// BaseService.java
// Servisler için varsayılan anotasyonlar
@Service
@Transactional(
    readOnly = true, // Varsayılan olarak salt okunur
    rollbackFor = Exception.class // Herhangi bir istisnada rollback
)
public abstract class BaseService {
    // Okuma metotları readOnly = true'yu devralır
}

// OrderService.java
// Tutarlı yapılandırmaya sahip servis
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

    // readOnly = true'yu devralır
    public Order findById(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // Yazma için override
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Kritik durumlar için açık yapılandırma
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Maksimum izolasyon ve kısa timeout ile işlem
    }
}

En iyi uygulamalar kontrol listesi:

text
İşlem Yapılandırması - Kontrol Listesi

✅ readOnly = true varsayılan, yazma için açık override
✅ Checked istisnaları dahil etmek için rollbackFor = Exception.class
✅ İşlem türüne göre uygun timeout
✅ İç çağrılardan kaçının (@Transactional göz ardı edilir)
✅ REQUIRES_NEW yalnızca bağımsız commit gerektiğinde
✅ İşlemsel bağlamı garanti etmek için MANDATORY
✅ Rollback davranışları için açık testler
✅ Uzun süren işlemleri izleyin
✅ Standart olmayan yayılım seçimlerini belgeleyin

Spring Boot mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Sonuç

İşlem yayılımı, Spring Boot mülakatlarında değerlendirilen temel bir kavramdır. Hatırlanması gereken kilit noktalar:

Yaygın yayılım türleri:

  • ✅ REQUIRED (varsayılan): bir işleme katılır veya oluşturur
  • ✅ REQUIRES_NEW: bağımsız işlem, ayrı commit
  • ✅ NESTED: kısmi rollback için savepoint
  • ✅ MANDATORY: mevcut bir işlem gerektirir

Kaçınılması gereken tuzaklar:

  • ✅ Self-invocation: proxy'yi atlar, @Transactional göz ardı edilir
  • ✅ Checked istisnalar: varsayılan olarak rollback yok
  • ✅ Aynı verilerde REQUIRES_NEW: deadlock riski
  • ✅ Eksik timeout: süresiz olarak bloklanmış işlemler

En iyi uygulamalar:

  • ✅ readOnly = true varsayılan olarak
  • ✅ rollbackFor = Exception.class sistematik olarak
  • ✅ Self-invocation'dan kaçınmak için ayrı servisler
  • ✅ Rollback davranışlarını açıkça test edin

İşlem yayılımına hakim olmak, Spring ve veri yönetimi konusunda derin bir anlayışı gösterir. Bu kavramlar, sağlam uygulamalar tasarlamak ve teknik mülakatları başarıyla geçmek için elzemdir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#spring boot
#transactions
#propagation
#java
#interview

Paylaş

İlgili makaleler