# Entrevista Spring Boot: Propagación de Transacciones > Domina la propagación de transacciones en Spring Boot: REQUIRED, REQUIRES_NEW, NESTED y más. 12 preguntas de entrevista con código y trampas comunes. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- La propagación de transacciones representa un concepto fundamental en Spring Boot, evaluado regularmente en entrevistas técnicas. Comprender cómo interactúan las transacciones entre métodos anotados con `@Transactional` permite evitar bugs sutiles en producción y diseñar arquitecturas robustas. > **Consejo de preparación** > > Los entrevistadores evalúan la capacidad de elegir el nivel de propagación adecuado según el contexto de negocio. Saber explicar por qué REQUIRES_NEW en lugar de REQUIRED en un caso específico marca la diferencia. ## Fundamentos de la propagación de transacciones ### Pregunta 1: ¿Qué es la propagación de transacciones en Spring? La propagación define el comportamiento de un método transaccional cuando es invocado dentro del contexto de una transacción existente. Responde a la pregunta: «¿Qué ocurre cuando un método `@Transactional` llama a otro método también anotado?» ```java // OrderService.java // Demostración del concepto de propagación @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Transacción padre - inicia una nueva transacción @Transactional public void createOrder(OrderRequest request) { // Guarda la orden en la transacción actual Order order = orderRepository.save(new Order(request)); // Llamada a otro método @Transactional // La propagación determina: ¿misma transacción o nueva? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Propagación por defecto: REQUIRED // Se une a la transacción existente de createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Se ejecuta en la MISMA transacción que createOrder() // Si este método falla, la orden también se revierte } } ``` Spring proporciona siete niveles de propagación, cada uno adaptado a necesidades de negocio específicas. La elección impacta directamente en la consistencia de datos y el rendimiento. ### Pregunta 2: Describe el comportamiento de REQUIRED (propagación por defecto) REQUIRED es la propagación por defecto. Si existe una transacción, el método se une a ella. De lo contrario, se crea una nueva. Es el comportamiento más común e intuitivo. ```java // UserService.java // REQUIRED: comportamiento por defecto @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Inicia una transacción si no existe ninguna User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // La auditoría se une a la misma transacción auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Se une a la transacción de updateUser() // Commit o rollback juntos auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` El diagrama siguiente ilustra el flujo transaccional: ```text updateUser() inicia TX-1 ├── save(user) → TX-1 └── logUpdate() → se une a TX-1 (REQUIRED) └── save(audit) → TX-1 Si logUpdate() falla → rollback TX-1 → user Y audit cancelados ``` ### Pregunta 3: ¿Cuándo usar REQUIRES_NEW en lugar de REQUIRED? REQUIRES_NEW suspende la transacción existente y crea una nueva transacción independiente. Útil cuando una operación debe persistirse independientemente del resultado de la transacción padre. ```java // PaymentService.java // REQUIRES_NEW: transacción independiente @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); // La auditoría DEBE persistirse aunque el pago falle después auditService.logPaymentAttempt(orderId, amount); // Simulación de un error después de la auditoría if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: hace commit independientemente de la transacción padre @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Nueva transacción TX-2 creada // TX-1 (processPayment) está suspendida auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 hace commit aquí, independientemente de TX-1 } } ``` El flujo transaccional con REQUIRES_NEW: ```text processPayment() inicia TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 SUSPENDIDA │ └── inicia TX-2 → nueva transacción │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit persistido │ └── TX-1 REANUDA └── throw InvalidAmountException └── ROLLBACK TX-1 → pago cancelado, pero audit conservado ``` > **Cuidado con los deadlocks** > > REQUIRES_NEW puede provocar deadlocks si la nueva transacción accede a los mismos recursos bloqueados por la transacción suspendida. Evita usar REQUIRES_NEW para modificar las mismas tablas que la transacción padre. ### Pregunta 4: Explica la propagación NESTED y en qué se diferencia de REQUIRES_NEW NESTED crea un savepoint dentro de la transacción actual. Si el método falla, solo las modificaciones desde el savepoint se revierten, no toda la transacción padre. ```java // BatchProcessingService.java // NESTED: savepoint dentro de la transacción padre @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 { // Cada item se procesa con un savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback solo de este item, no de todo el batch result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit de los items exitosos } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: crea un savepoint, rollback parcial posible @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Validación de negocio if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback al savepoint → solo este item } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Comparación de NESTED vs REQUIRES_NEW: ```text NESTED: ├── Usa un savepoint dentro de la TX padre ├── En caso de fallo → rollback al savepoint ├── Si la TX padre hace rollback → NESTED también revierte └── Mejor rendimiento (sin nueva conexión) REQUIRES_NEW: ├── Crea una transacción totalmente independiente ├── En caso de fallo → rollback solo de la TX hija ├── Si la TX padre hace rollback → la TX hija YA HIZO COMMIT └── Requiere una nueva conexión ``` > **Soporte de NESTED** > > La propagación NESTED requiere soporte de savepoints JDBC. La mayoría de bases de datos modernas (PostgreSQL, MySQL, Oracle) lo soportan. Verifica la compatibilidad antes de usarla. ## Tipos de propagación avanzados ### Pregunta 5: ¿Cuándo usar SUPPORTS y NOT_SUPPORTED? SUPPORTS se ejecuta dentro de la transacción existente si la hay, y sin transacción en caso contrario. NOT_SUPPORTED suspende cualquier transacción existente y se ejecuta sin transacción. ```java // ReportingService.java // SUPPORTS: transacción opcional @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: funciona con o sin transacción // Útil para lecturas que no necesitan garantías transaccionales @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Si se llama desde un método @Transactional → usa su TX // Si se llama directamente → sin transacción (lectura OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: nunca dentro de una transacción // Evita bloquear la TX durante una llamada externa lenta @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX padre suspendida durante la llamada apiClient.send(message); // Llamada HTTP potencialmente lenta // TX padre se reanuda después } } ``` ```java // OrderService.java // Ejemplo de uso combinado @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); // Generación del reporte en la misma TX (SUPPORTS) Report report = reportingService.generateReport(orderId); // Notificación externa FUERA de la transacción (NOT_SUPPORTED) // Evita timeout de la TX si la API externa es lenta notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Pregunta 6: Explica MANDATORY y NEVER MANDATORY exige una transacción existente y lanza una excepción en caso contrario. NEVER exige la ausencia de transacción y lanza una excepción si existe una. ```java // AuditService.java // MANDATORY: debe ser invocado desde una transacción @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: rechaza ejecutarse sin una transacción existente // Garantiza que la auditoría sea siempre atómica con la operación auditada @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Si se llama sin transacción → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: no debe estar nunca en una transacción @Service public class CacheService { private final CacheManager cacheManager; // NEVER: el caché no debe participar en transacciones // Evita inconsistencias entre caché y BD tras un rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Si se llama desde un @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Uso correcto de MANDATORY @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Operación sensible... updatePassword(userId, newPassword); // La auditoría DEBE estar en la misma transacción // MANDATORY impone esta restricción arquitectónica auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // ERROR: llamada directa sin transacción public void badUsage() { // Lanza IllegalTransactionStateException porque no hay TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Trampas comunes en entrevistas ### Pregunta 7: ¿Por qué @Transactional no funciona en llamadas internas? Una de las trampas más comunes. Las llamadas internas a métodos (self-invocation) no pasan por el proxy de Spring, lo que desactiva la gestión transaccional. ```java // BrokenService.java // ERROR CLÁSICO: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // TRAMPA: llamada interna → no pasa por el proxy // @Transactional en processItem() es IGNORADO processItem(id); } } @Transactional public void processItem(Long itemId) { // Esta transacción NUNCA se crea en llamadas internas Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Soluciones para evitar esta trampa: ```java // Solución 1: Auto-inyección @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Auto-inyección public void processItems(List itemIds) { for (Long id : itemIds) { // Llamada a través del proxy → @Transactional funciona self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Transacción correctamente creada Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Solución 2: Separar en dos servicios (recomendado) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Llamada a otro bean → el proxy funciona processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Transacción correctamente gestionada Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Pregunta 8: ¿Cómo gestionar excepciones y rollback? Por defecto, Spring solo hace rollback en RuntimeException y Error. Las excepciones checked NO disparan rollback automático. ```java // TransactionRollbackDemo.java // Comportamiento de rollback según el tipo de excepción @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → ROLLBACK automático @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → SIN rollback por defecto @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // ¡Hace COMMIT igualmente! } // Forzar rollback en checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK gracias a rollbackFor } // Excluir una RuntimeException del rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT a pesar de la excepción } } ``` Configuración recomendada para casos de negocio: ```java // BaseTransactionalService.java // Configuración transaccional consistente @Service public abstract class BaseTransactionalService { // Rollback en todas las excepciones (checked y 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) { // NO hace rollback - se quiere mantener el registro del intento throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Pregunta 9: ¿Cómo funciona el aislamiento de transacciones con la propagación? Aislamiento y propagación son complementarios. El aislamiento determina la visibilidad de los datos entre transacciones concurrentes. ```java // IsolationDemo.java // Combinación aislamiento + propagación @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: ve datos commitados por otras transacciones @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Puede ver valores diferentes si se relee durante la transacción return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: garantiza la misma lectura durante la transacción @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(); // Aunque otra transacción modifique 'from' mientras tanto, // siempre vemos el valor inicial (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: aislamiento máximo, sin concurrencia @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Bloquea cualquier otra transacción sobre estos datos // Usar con moderación - impacto en rendimiento Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Tabla resumen de niveles de aislamiento: ```text | Aislamiento | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Posible | Posible | Posible | | READ_COMMITTED | No | Posible | Posible | | REPEATABLE_READ | No | No | Posible | | SERIALIZABLE | No | No | No | ``` > **Impacto en el rendimiento** > > Cuanto más estricto es el aislamiento, más se ve afectado el rendimiento por los locks. SERIALIZABLE puede provocar contención significativa en producción de alto tráfico. ## Patrones avanzados ### Pregunta 10: ¿Cómo implementar el patrón «transactional outbox»? El patrón outbox garantiza la consistencia entre las modificaciones en base de datos y el envío de mensajes/eventos, incluso en caso de fallo. ```java // OutboxService.java // Patrón Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Guarda el evento en la misma transacción que la entidad de negocio @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) { // Crear la orden Order order = new Order(request); orderRepository.save(order); // Evento outbox en la MISMA transacción (MANDATORY) // Si el commit es exitoso → ambos se persisten // Si hay rollback → ninguno se persiste outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Proceso separado que publica los eventos @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Transacción independiente para cada publicación @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); } } } ``` ### Pregunta 11: ¿Cómo testear los diferentes comportamientos de propagación? Los tests de propagación requieren atención especial para verificar el comportamiento transaccional esperado. ```java // TransactionPropagationTest.java // Testeo de comportamientos de propagación @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 - pago en la misma transacción (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - ambos visibles antes del 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 en REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Transacción principal hace rollback } // Then - audit (REQUIRES_NEW) sigue commitado assertThat(findAuditLog(orderId)).isNotNull(); // Pero la orden hace rollback assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - ninguna transacción activa // When/Then - debe lanzar una excepción assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Test de integración con rollback y commit reales @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 - procesamiento batch con NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - solo los items válidos se persisten assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Pregunta 12: ¿Cuáles son las mejores prácticas para la configuración transaccional? Una configuración transaccional consistente a nivel de proyecto evita sorpresas y facilita el mantenimiento. ```java // TransactionConfig.java // Configuración transaccional centralizada @Configuration @EnableTransactionManagement public class TransactionConfig { // Timeout por defecto para todas las transacciones @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 segundos máximo por transacción return tm; } } // BaseService.java // Anotaciones por defecto para los servicios @Service @Transactional( readOnly = true, // Solo lectura por defecto rollbackFor = Exception.class // Rollback en cualquier excepción ) public abstract class BaseService { // Los métodos de lectura heredan readOnly = true } // OrderService.java // Servicio con configuración consistente @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Hereda readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override para escrituras @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Configuración explícita para casos críticos @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Operación con aislamiento máximo y timeout corto } } ``` Checklist de mejores prácticas: ```text Configuración Transaccional - Checklist ✅ readOnly = true por defecto, override explícito para escrituras ✅ rollbackFor = Exception.class para incluir checked exceptions ✅ Timeout adecuado según el tipo de operación ✅ Evitar llamadas internas (@Transactional ignorado) ✅ REQUIRES_NEW solo cuando un commit independiente es necesario ✅ MANDATORY para garantizar contexto transaccional ✅ Tests explícitos de comportamientos de rollback ✅ Monitorizar transacciones de larga duración ✅ Documentar elecciones de propagación no estándar ``` ## Conclusión La propagación de transacciones es un concepto fundamental evaluado en entrevistas Spring Boot. Puntos clave a recordar: **Tipos de propagación comunes:** - ✅ REQUIRED (por defecto): se une o crea una transacción - ✅ REQUIRES_NEW: transacción independiente, commit separado - ✅ NESTED: savepoint para rollback parcial - ✅ MANDATORY: requiere una transacción existente **Trampas a evitar:** - ✅ Self-invocation: no pasa por el proxy, @Transactional ignorado - ✅ Checked exceptions: sin rollback por defecto - ✅ REQUIRES_NEW sobre los mismos datos: riesgo de deadlock - ✅ Ausencia de timeout: transacciones bloqueadas indefinidamente **Mejores prácticas:** - ✅ readOnly = true por defecto - ✅ rollbackFor = Exception.class de manera sistemática - ✅ Servicios separados para evitar self-invocation - ✅ Testear explícitamente los comportamientos de rollback Dominar la propagación de transacciones demuestra una comprensión profunda de Spring y de la gestión de datos. Estos conceptos son esenciales para diseñar aplicaciones robustas y superar entrevistas técnicas con éxito. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/spring-boot/spring-boot-transaction-propagation-interview