# Wawancara Spring Boot: Propagasi Transaksi Dijelaskan > Kuasai propagasi transaksi Spring Boot: REQUIRED, REQUIRES_NEW, NESTED dan lainnya. 12 pertanyaan wawancara dengan contoh kode dan jebakan umum. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- Propagasi transaksi merupakan konsep fundamental dalam Spring Boot yang secara rutin dievaluasi selama wawancara teknis. Memahami bagaimana transaksi berinteraksi antara metode beranotasi `@Transactional` membantu menghindari bug halus di produksi dan memungkinkan perancangan arsitektur yang andal. > **Saran Persiapan** > > Pewawancara menguji kemampuan memilih level propagasi yang tepat berdasarkan konteks bisnis. Dapat menjelaskan mengapa REQUIRES_NEW alih-alih REQUIRED dalam kasus tertentu membuat perbedaan. ## Dasar-dasar propagasi transaksi ### Pertanyaan 1: Apa itu propagasi transaksi di Spring? Propagasi mendefinisikan perilaku metode transaksional ketika dipanggil dalam konteks transaksi yang sudah ada. Ia menjawab pertanyaan: "Apa yang terjadi ketika metode `@Transactional` memanggil metode lain yang juga beranotasi?" ```java // OrderService.java // Mendemonstrasikan konsep propagasi @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Transaksi induk - memulai transaksi baru @Transactional public void createOrder(OrderRequest request) { // Menyimpan order dalam transaksi saat ini Order order = orderRepository.save(new Order(request)); // Memanggil metode @Transactional lain // Propagasi menentukan: transaksi yang sama atau baru? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Propagasi default: REQUIRED // Bergabung ke transaksi yang ada dari createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Berjalan dalam transaksi yang SAMA dengan createOrder() // Jika metode ini gagal, order juga di-rollback } } ``` Spring menyediakan tujuh level propagasi, masing-masing cocok untuk kebutuhan bisnis spesifik. Pilihan ini berdampak langsung pada konsistensi data dan performa. ### Pertanyaan 2: Jelaskan perilaku REQUIRED (propagasi default) REQUIRED adalah propagasi default. Jika ada transaksi, metode bergabung dengannya. Jika tidak, transaksi baru dibuat. Ini adalah perilaku yang paling umum dan intuitif. ```java // UserService.java // REQUIRED: perilaku default @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Memulai transaksi jika tidak ada yang ada User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // Audit bergabung ke transaksi yang sama auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Bergabung ke transaksi updateUser() // Commit atau rollback bersama auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` Diagram berikut menggambarkan alur transaksional: ```text updateUser() memulai TX-1 ├── save(user) → TX-1 └── logUpdate() → bergabung ke TX-1 (REQUIRED) └── save(audit) → TX-1 Jika logUpdate() gagal → rollback TX-1 → user DAN audit dibatalkan ``` ### Pertanyaan 3: Kapan menggunakan REQUIRES_NEW alih-alih REQUIRED? REQUIRES_NEW menangguhkan transaksi yang ada dan membuat transaksi independen baru. Berguna ketika operasi harus di-commit terlepas dari hasil transaksi induk. ```java // PaymentService.java // REQUIRES_NEW: transaksi independen @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); // Audit HARUS disimpan meskipun pembayaran gagal nantinya auditService.logPaymentAttempt(orderId, amount); // Simulasi error setelah audit if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: commit independen dari transaksi induk @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Transaksi baru TX-2 dibuat // TX-1 (processPayment) ditangguhkan auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 commit di sini, independen dari TX-1 } } ``` Alur transaksional dengan REQUIRES_NEW: ```text processPayment() memulai TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 DITANGGUHKAN │ └── memulai TX-2 → transaksi baru │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit disimpan │ └── TX-1 DILANJUTKAN └── throw InvalidAmountException └── ROLLBACK TX-1 → pembayaran dibatalkan, tetapi audit dipertahankan ``` > **Hati-hati dengan deadlock** > > REQUIRES_NEW dapat menyebabkan deadlock jika transaksi baru mengakses sumber daya yang sama yang dikunci oleh transaksi yang ditangguhkan. Hindari menggunakan REQUIRES_NEW untuk memodifikasi tabel yang sama dengan transaksi induk. ### Pertanyaan 4: Jelaskan propagasi NESTED dan bedanya dengan REQUIRES_NEW NESTED membuat savepoint dalam transaksi saat ini. Jika metode gagal, hanya modifikasi sejak savepoint yang di-rollback, bukan seluruh transaksi induk. ```java // BatchProcessingService.java // NESTED: savepoint dalam transaksi induk @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // Setiap item diproses dengan savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback hanya item ini, bukan seluruh batch result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit item yang berhasil } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: membuat savepoint, rollback parsial mungkin @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Validasi bisnis if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback ke savepoint → hanya item ini } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Perbandingan NESTED vs REQUIRES_NEW: ```text NESTED: ├── Menggunakan savepoint dalam TX induk ├── Saat gagal → rollback ke savepoint ├── Jika TX induk rollback → NESTED juga rollback └── Lebih performa (tanpa koneksi baru) REQUIRES_NEW: ├── Membuat transaksi yang sepenuhnya independen ├── Saat gagal → rollback hanya TX anak ├── Jika TX induk rollback → TX anak SUDAH DI-COMMIT └── Memerlukan koneksi baru ``` > **Dukungan NESTED** > > Propagasi NESTED memerlukan dukungan savepoint JDBC. Sebagian besar database modern (PostgreSQL, MySQL, Oracle) mendukung ini. Verifikasi kompatibilitas sebelum digunakan. ## Tipe propagasi lanjutan ### Pertanyaan 5: Kapan menggunakan SUPPORTS dan NOT_SUPPORTED? SUPPORTS dijalankan dalam transaksi yang ada jika tersedia, jika tidak tanpa transaksi. NOT_SUPPORTED menangguhkan transaksi yang ada dan dijalankan tanpa transaksi. ```java // ReportingService.java // SUPPORTS: transaksi opsional @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: berjalan dengan atau tanpa transaksi // Berguna untuk pembacaan yang tidak memerlukan jaminan transaksional @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Jika dipanggil dari metode @Transactional → menggunakan TX-nya // Jika dipanggil langsung → tanpa transaksi (read-only OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: tidak pernah dalam transaksi // Menghindari pemblokiran TX selama panggilan eksternal yang lambat @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX induk ditangguhkan selama panggilan apiClient.send(message); // Panggilan HTTP yang berpotensi lambat // TX induk dilanjutkan setelahnya } } ``` ```java // OrderService.java // Contoh penggunaan gabungan @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); // Generasi laporan dalam TX yang sama (SUPPORTS) Report report = reportingService.generateReport(orderId); // Notifikasi eksternal DI LUAR transaksi (NOT_SUPPORTED) // Menghindari timeout TX jika API eksternal lambat notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Pertanyaan 6: Jelaskan MANDATORY dan NEVER MANDATORY memerlukan transaksi yang ada dan melempar exception jika tidak. NEVER memerlukan ketiadaan transaksi dan melempar exception jika ada. ```java // AuditService.java // MANDATORY: harus dipanggil dari dalam transaksi @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: menolak dijalankan tanpa transaksi yang ada // Menjamin audit selalu atomik dengan operasi yang diaudit @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Jika dipanggil tanpa transaksi → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: tidak boleh berada dalam transaksi @Service public class CacheService { private final CacheManager cacheManager; // NEVER: cache tidak boleh berpartisipasi dalam transaksi // Menghindari ketidakkonsistenan antara cache dan DB setelah rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Jika dipanggil dari @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Penggunaan MANDATORY yang benar @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Operasi sensitif... updatePassword(userId, newPassword); // Audit HARUS dalam transaksi yang sama // MANDATORY menegakkan batasan arsitektural ini auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // KESALAHAN: panggilan langsung tanpa transaksi public void badUsage() { // Melempar IllegalTransactionStateException karena tidak ada TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Jebakan umum dalam wawancara ### Pertanyaan 7: Mengapa @Transactional tidak bekerja pada panggilan internal? Salah satu jebakan yang paling umum. Panggilan metode internal (self-invocation) melewati proxy Spring, menonaktifkan manajemen transaksi. ```java // BrokenService.java // KESALAHAN KLASIK: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // JEBAKAN: panggilan internal → melewati proxy // @Transactional pada processItem() DIABAIKAN processItem(id); } } @Transactional public void processItem(Long itemId) { // Transaksi ini TIDAK PERNAH dibuat dalam panggilan internal Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Solusi untuk menghindari jebakan ini: ```java // Solusi 1: Self-injection @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Self-injection public void processItems(List itemIds) { for (Long id : itemIds) { // Panggilan melalui proxy → @Transactional bekerja self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Transaksi dibuat dengan benar Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Solusi 2: Pisahkan menjadi dua service (direkomendasikan) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Panggilan ke bean lain → proxy bekerja processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Transaksi dikelola dengan benar Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Pertanyaan 8: Bagaimana menangani exception dan rollback? Secara default, Spring hanya rollback pada RuntimeException dan Error. Checked exception TIDAK memicu rollback otomatis. ```java // TransactionRollbackDemo.java // Perilaku rollback berdasarkan tipe exception @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → ROLLBACK otomatis @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → TIDAK rollback secara default @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // Tetap COMMIT! } // Memaksa rollback pada checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK berkat rollbackFor } // Mengecualikan RuntimeException dari rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT meskipun exception } } ``` Konfigurasi yang direkomendasikan untuk kasus bisnis: ```java // BaseTransactionalService.java // Konfigurasi transaksional yang konsisten @Service public abstract class BaseTransactionalService { // Rollback pada semua exception (checked dan 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) { // TIDAK rollback - kita ingin menyimpan catatan upaya throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Pertanyaan 9: Bagaimana isolasi transaksi bekerja dengan propagasi? Isolasi dan propagasi saling melengkapi. Isolasi menentukan visibilitas data antar transaksi konkuren. ```java // IsolationDemo.java // Kombinasi isolasi + propagasi @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: melihat data yang di-commit oleh transaksi lain @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Mungkin melihat nilai berbeda jika dibaca ulang selama transaksi return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: menjamin pembacaan yang sama selama transaksi @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(); // Bahkan jika transaksi lain memodifikasi 'from' di antara waktu, // kita selalu melihat nilai awal (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: isolasi maksimum, tidak ada konkurensi @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Memblokir transaksi lain pada data ini // Gunakan dengan hemat - dampak performa Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Tabel ringkasan level isolasi: ```text | Isolasi | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Mungkin | Mungkin | Mungkin | | READ_COMMITTED | Tidak | Mungkin | Mungkin | | REPEATABLE_READ | Tidak | Tidak | Mungkin | | SERIALIZABLE | Tidak | Tidak | Tidak | ``` > **Dampak performa** > > Semakin ketat isolasi, semakin besar performa dapat terdampak oleh lock. SERIALIZABLE dapat menyebabkan kontensi signifikan dalam produksi dengan trafik tinggi. ## Pola lanjutan ### Pertanyaan 10: Bagaimana mengimplementasikan pola "transactional outbox"? Pola outbox menjamin konsistensi antara modifikasi database dan pengiriman pesan/event, bahkan dalam kasus kegagalan. ```java // OutboxService.java // Pola Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Menyimpan event dalam transaksi yang sama dengan entitas bisnis @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) { // Membuat order Order order = new Order(request); orderRepository.save(order); // Event outbox dalam transaksi yang SAMA (MANDATORY) // Jika commit berhasil → keduanya disimpan // Jika rollback → tidak ada yang disimpan outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Proses terpisah yang menerbitkan event @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Transaksi independen untuk setiap penerbitan @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List 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); } } } ``` ### Pertanyaan 11: Bagaimana menguji perilaku propagasi yang berbeda? Tes propagasi memerlukan perhatian khusus untuk memverifikasi perilaku transaksional yang diharapkan. ```java // TransactionPropagationTest.java // Pengujian perilaku propagasi @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 - pembayaran dalam transaksi yang sama (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - keduanya terlihat sebelum commit 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 - audit dalam REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Transaksi utama melakukan rollback } // Then - audit (REQUIRES_NEW) masih di-commit assertThat(findAuditLog(orderId)).isNotNull(); // Tetapi order di-rollback assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - tidak ada transaksi aktif // When/Then - harus melempar exception assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Tes integrasi dengan rollback dan commit nyata @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 - pemrosesan batch dengan NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - hanya item valid yang disimpan assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Pertanyaan 12: Apa praktik terbaik untuk konfigurasi transaksi? Konfigurasi transaksional yang konsisten di tingkat proyek menghindari kejutan dan memudahkan pemeliharaan. ```java // TransactionConfig.java // Konfigurasi transaksi terpusat @Configuration @EnableTransactionManagement public class TransactionConfig { // Timeout default untuk semua transaksi @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 detik maksimum per transaksi return tm; } } // BaseService.java // Anotasi default untuk service @Service @Transactional( readOnly = true, // Read-only secara default rollbackFor = Exception.class // Rollback pada exception apa pun ) public abstract class BaseService { // Metode pembacaan mewarisi readOnly = true } // OrderService.java // Service dengan konfigurasi konsisten @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Mewarisi readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override untuk penulisan @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Konfigurasi eksplisit untuk kasus kritis @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Operasi dengan isolasi maksimum dan timeout pendek } } ``` Daftar periksa praktik terbaik: ```text Konfigurasi Transaksi - Daftar Periksa ✅ readOnly = true secara default, override eksplisit untuk penulisan ✅ rollbackFor = Exception.class untuk menyertakan checked exception ✅ Timeout sesuai dengan jenis operasi ✅ Hindari panggilan internal (@Transactional diabaikan) ✅ REQUIRES_NEW hanya jika commit independen diperlukan ✅ MANDATORY untuk menjamin konteks transaksional ✅ Tes eksplisit perilaku rollback ✅ Pantau transaksi yang berjalan lama ✅ Dokumentasikan pilihan propagasi non-standar ``` ## Kesimpulan Propagasi transaksi adalah konsep fundamental yang dievaluasi dalam wawancara Spring Boot. Poin penting untuk diingat: **Tipe propagasi umum:** - ✅ REQUIRED (default): bergabung atau membuat transaksi - ✅ REQUIRES_NEW: transaksi independen, commit terpisah - ✅ NESTED: savepoint untuk rollback parsial - ✅ MANDATORY: memerlukan transaksi yang ada **Jebakan yang harus dihindari:** - ✅ Self-invocation: melewati proxy, @Transactional diabaikan - ✅ Checked exception: tidak rollback secara default - ✅ REQUIRES_NEW pada data yang sama: risiko deadlock - ✅ Timeout hilang: transaksi terblokir tanpa batas **Praktik terbaik:** - ✅ readOnly = true secara default - ✅ rollbackFor = Exception.class secara sistematis - ✅ Pisahkan service untuk menghindari self-invocation - ✅ Uji perilaku rollback secara eksplisit Menguasai propagasi transaksi menunjukkan pemahaman mendalam tentang Spring dan manajemen data. Konsep-konsep ini penting untuk merancang aplikasi yang andal dan berhasil melewati wawancara teknis. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/spring-boot/spring-boot-transaction-propagation-interview