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.

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.
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?"
// 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.
// 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:
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 dibatalkanPertanyaan 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.
// 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:
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 dipertahankanREQUIRES_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.
// NESTED: savepoint dalam transaksi induk
@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 {
// 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:
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 baruPropagasi 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.
// 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
}
}// 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.
// 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);
}
}// 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);
}
}Siap menguasai wawancara Spring Boot Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
// KESALAHAN KLASIK: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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:
// Solusi 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) {
// 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<Long> 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.
// 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:
// 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.
// 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:
| 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 |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.
// 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<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);
}
}
}Pertanyaan 11: Bagaimana menguji perilaku propagasi yang berbeda?
Tes propagasi memerlukan perhatian khusus untuk memverifikasi perilaku transaksional yang diharapkan.
// 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");
}
}// 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.
// 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:
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-standarSiap menguasai wawancara Spring Boot Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

30 Pertanyaan Wawancara Spring Boot: Panduan Lengkap untuk Developer Java
Persiapkan wawancara Spring Boot dengan 30 pertanyaan penting tentang auto-configuration, starter, Spring Data JPA, keamanan, dan testing.

Spring Modulith: Arsitektur Monolit Modular Dijelaskan
Pelajari Spring Modulith untuk membangun monolit modular di Java. Arsitektur, modul, event asinkron, dan testing dengan contoh Spring Boot 3.

Wawancara Spring Batch 5: Partisi, Chunk, dan Toleransi Kegagalan
Kuasai wawancara Spring Batch 5: 15 pertanyaan penting tentang partisi, pemrosesan chunk, dan toleransi kegagalan dengan contoh kode Java 21.