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.

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.
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?»
// 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.
// 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:
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 canceladosPregunta 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.
// 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:
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 conservadoREQUIRES_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.
// 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<Item> 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:
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ónLa 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.
// 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
}
}// 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.
// 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);
}
}// 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);
}
}¿Listo para aprobar tus entrevistas de Spring Boot?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
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.
// ERROR CLÁSICO: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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:
// 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<Long> 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<Long> 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.
// 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:
// 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.
// 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:
| 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 |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.
// 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<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);
}
}
}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.
// 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");
}
}// 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.
// 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:
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¿Listo para aprobar tus entrevistas de Spring Boot?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
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.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

30 Preguntas de Entrevista sobre Spring Boot: Guía Completa para Desarrolladores Java
Prepárate para tus entrevistas de Spring Boot con estas 30 preguntas esenciales sobre auto-configuración, starters, Spring Data JPA, seguridad y testing.

Spring Modulith: Arquitectura de Monolito Modular Explicada
Aprende Spring Modulith para construir monolitos modulares en Java. Arquitectura, módulos, eventos asíncronos y testing con ejemplos en Spring Boot 3.

Entrevista Spring Batch 5: Particionamiento, Chunks y Tolerancia
Domina las entrevistas de Spring Batch 5: 15 preguntas esenciales sobre particionamiento, procesamiento por chunks y tolerancia a fallos con ejemplos en Java 21.