Skip to content

10. Transacciones en Spring

Una transacción es un conjunto de operaciones que se ejecutan como una unidad atómica: o todas se completan exitosamente, o ninguna se aplica.

PropiedadDescripción
AtomicityTodo o nada
ConsistencyEstado válido antes y después
IsolationTransacciones no interfieren entre sí
DurabilityCambios persistentes tras commit
Necesidad de transacciones
// Transferencia bancaria: DEBE ser atómica
public void transferir(Long origenId, Long destinoId, BigDecimal monto) {
// 1. Debitar de cuenta origen
cuentaOrigen.debitar(monto);
// 2. Acreditar a cuenta destino
cuentaDestino.acreditar(monto);
// Si falla el paso 2, el paso 1 debe revertirse
// ¡No podemos perder dinero!
}

@Transactional básico
import org.springframework.transaction.annotation.Transactional;
@Service
public class CuentaService {
private final CuentaRepository cuentaRepository;
@Transactional // Toda la operación es atómica
public void transferir(Long origenId, Long destinoId, BigDecimal monto) {
Cuenta origen = cuentaRepository.findById(origenId).orElseThrow();
Cuenta destino = cuentaRepository.findById(destinoId).orElseThrow();
origen.debitar(monto);
destino.acreditar(monto);
cuentaRepository.save(origen);
cuentaRepository.save(destino);
// Si algo falla, TODO se revierte
}
}
@Transactional a nivel de clase
@Service
@Transactional // Aplica a todos los métodos públicos
public class PedidoService {
public Pedido crear(PedidoDTO dto) {
// Transaccional
}
public void cancelar(Long id) {
// Transaccional
}
@Transactional(readOnly = true) // Override para lectura
public Pedido buscar(Long id) {
// Solo lectura, optimizado
}
}
Transacción de solo lectura
@Service
public class ReporteService {
@Transactional(readOnly = true) // Optimización para lecturas
public List<Venta> generarReporte(LocalDate desde, LocalDate hasta) {
return ventaRepository.findByFechaBetween(desde, hasta);
}
}

PropagaciónDescripción
REQUIREDUsa existente o crea nueva (default)
REQUIRES_NEWSiempre crea nueva, suspende existente
SUPPORTSUsa existente si hay, sino sin transacción
NOT_SUPPORTEDSin transacción, suspende existente
MANDATORYDebe existir una, sino error
NEVERNo debe existir, sino error
NESTEDTransacción anidada (savepoint)
REQUIRED: misma transacción
@Service
public class PedidoService {
@Autowired
private InventarioService inventarioService;
@Transactional // Propagation.REQUIRED por defecto
public Pedido crear(PedidoDTO dto) {
Pedido pedido = new Pedido(dto);
pedidoRepository.save(pedido);
// Usa la MISMA transacción
inventarioService.reservar(dto.getProductos());
return pedido;
}
}
@Service
public class InventarioService {
@Transactional // Participa en la transacción existente
public void reservar(List<Producto> productos) {
// Si falla aquí, TODO se revierte (pedido incluido)
}
}
REQUIRES_NEW: transacción independiente
@Service
public class PedidoService {
@Autowired
private AuditoriaService auditoriaService;
@Transactional
public Pedido crear(PedidoDTO dto) {
Pedido pedido = new Pedido(dto);
pedidoRepository.save(pedido);
// Transacción INDEPENDIENTE
auditoriaService.registrar("Pedido creado: " + pedido.getId());
return pedido;
}
}
@Service
public class AuditoriaService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrar(String mensaje) {
// Si el pedido falla, la auditoría SE GUARDA igual
// Útil para logs que deben persistir siempre
auditoriaRepository.save(new Auditoria(mensaje));
}
}
NESTED: savepoints
@Service
public class ProcesoService {
@Transactional
public void procesarLote(List<Item> items) {
for (Item item : items) {
try {
procesarItem(item); // Transacción anidada
} catch (Exception e) {
// Si falla un item, continúa con los demás
log.error("Error en item: " + item.getId());
}
}
}
@Transactional(propagation = Propagation.NESTED)
public void procesarItem(Item item) {
// Si falla, solo este item se revierte (savepoint)
// Los demás items y la transacción padre continúan
}
}

NivelDirty ReadNon-Repeatable ReadPhantom Read
READ_UNCOMMITTED✅ Posible✅ Posible✅ Posible
READ_COMMITTED❌ No✅ Posible✅ Posible
REPEATABLE_READ❌ No❌ No✅ Posible
SERIALIZABLE❌ No❌ No❌ No
Problemas de concurrencia
// DIRTY READ: Leer datos no confirmados
// T1: UPDATE cuenta SET saldo = 500 (no commit)
// T2: SELECT saldo FROM cuenta → 500 (dato "sucio")
// T1: ROLLBACK
// T2 leyó un valor que nunca existió
// NON-REPEATABLE READ: Mismo query, diferentes resultados
// T1: SELECT saldo → 1000
// T2: UPDATE saldo = 500; COMMIT
// T1: SELECT saldo → 500 (¡cambió!)
// PHANTOM READ: Filas aparecen/desaparecen
// T1: SELECT COUNT(*) WHERE precio > 100 → 10
// T2: INSERT producto (precio = 150); COMMIT
// T1: SELECT COUNT(*) WHERE precio > 100 → 11 (¡fantasma!)
Configurar aislamiento
@Service
public class ReporteFinancieroService {
// Máximo aislamiento para reportes críticos
@Transactional(isolation = Isolation.SERIALIZABLE)
public ReporteFinanciero generarReporteMensual() {
// Garantiza consistencia total
// Pero menor rendimiento
}
// Default para la mayoría de casos
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<Transaccion> listarTransacciones() {
// Balance entre consistencia y rendimiento
}
}

Rollback por defecto
@Service
public class PedidoService {
@Transactional
public Pedido crear(PedidoDTO dto) {
Pedido pedido = pedidoRepository.save(new Pedido(dto));
// RuntimeException → ROLLBACK automático
if (dto.getTotal().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Total inválido");
}
// Checked Exception → NO hace rollback por defecto
try {
enviarEmail(pedido);
} catch (MessagingException e) {
// La transacción NO se revierte
throw e;
}
return pedido;
}
}
Configurar rollback
@Service
public class PedidoService {
// Rollback también para checked exceptions
@Transactional(rollbackFor = Exception.class)
public Pedido crear(PedidoDTO dto) throws Exception {
// Cualquier excepción causa rollback
}
// Rollback para excepciones específicas
@Transactional(rollbackFor = {StockInsuficienteException.class, PagoRechazadoException.class})
public Pedido procesar(PedidoDTO dto) {
// Solo estas excepciones causan rollback
}
// NO hacer rollback para ciertas excepciones
@Transactional(noRollbackFor = NotificacionException.class)
public Pedido crearConNotificacion(PedidoDTO dto) {
Pedido pedido = pedidoRepository.save(new Pedido(dto));
try {
notificar(pedido); // Si falla, NO revierte el pedido
} catch (NotificacionException e) {
log.warn("No se pudo notificar");
}
return pedido;
}
}

⚠️ 10.6 Manejo de Errores en Transacciones

Section titled “⚠️ 10.6 Manejo de Errores en Transacciones”
Manejo de errores
@Service
public class TransferenciaService {
@Transactional
public ResultadoTransferencia transferir(TransferenciaDTO dto) {
try {
Cuenta origen = cuentaRepository.findById(dto.getOrigenId())
.orElseThrow(() -> new CuentaNoEncontradaException(dto.getOrigenId()));
Cuenta destino = cuentaRepository.findById(dto.getDestinoId())
.orElseThrow(() -> new CuentaNoEncontradaException(dto.getDestinoId()));
if (origen.getSaldo().compareTo(dto.getMonto()) < 0) {
throw new SaldoInsuficienteException(origen.getId());
}
origen.debitar(dto.getMonto());
destino.acreditar(dto.getMonto());
return ResultadoTransferencia.exitoso();
} catch (CuentaNoEncontradaException | SaldoInsuficienteException e) {
// Estas excepciones causan rollback
throw e;
}
}
}
Rollback programático
@Service
public class ProcesoService {
@Autowired
private TransactionTemplate transactionTemplate;
public void procesarConRollbackManual() {
transactionTemplate.execute(status -> {
try {
// Operaciones...
if (algunaCondicion) {
status.setRollbackOnly(); // Marcar para rollback
}
return resultado;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
}

Problema de transacciones distribuidas
// Escenario: Microservicios
// Servicio A: Crear pedido
// Servicio B: Procesar pago
// Servicio C: Actualizar inventario
// Si el pago falla después de crear el pedido,
// ¿cómo revertimos el pedido en otro servicio?
Patrón SAGA básico
@Service
public class PedidoSagaService {
public void crearPedidoSaga(PedidoDTO dto) {
String pedidoId = null;
String pagoId = null;
try {
// Paso 1: Crear pedido
pedidoId = pedidoService.crear(dto);
// Paso 2: Procesar pago
pagoId = pagoService.procesar(dto.getPago());
// Paso 3: Reservar inventario
inventarioService.reservar(dto.getProductos());
} catch (Exception e) {
// Compensaciones (rollback manual)
if (pagoId != null) {
pagoService.revertir(pagoId);
}
if (pedidoId != null) {
pedidoService.cancelar(pedidoId);
}
throw e;
}
}
}
Outbox Pattern
@Service
public class PedidoService {
@Transactional
public Pedido crear(PedidoDTO dto) {
// 1. Guardar pedido
Pedido pedido = pedidoRepository.save(new Pedido(dto));
// 2. Guardar evento en tabla outbox (misma transacción)
OutboxEvent evento = new OutboxEvent(
"PedidoCreado",
pedido.getId(),
objectMapper.writeValueAsString(pedido)
);
outboxRepository.save(evento);
return pedido;
// Un proceso separado lee la tabla outbox y publica eventos
}
}
🐝