Zademy

Spring Transaction Management Programático: Guía Práctica para Principiantes

spring-boot; transactions
words palabras

La anotación @Transactional de Spring es maravillosa para la gestión declarativa de transacciones, pero no siempre es la mejor opción. En este apunte verás cuándo y cómo usar el control programático de transacciones en Spring, ideal para situaciones donde necesitas un manejo fino del ciclo de vida de las transacciones.

¿Por Qué Necesitas Transacciones Programáticas?

El Problema del Agotamiento de Conexiones

Imagina este escenario común: un método que combina llamadas a base de datos con una API externa:

@Transactional
public void procesarPago(PaymentRequest request) {
    guardarSolicitud(request);              // DB
    llamarApiProveedorPago(request);      // API externa (lenta)
    actualizarEstadoPago(request);          // DB
    guardarAuditoria(request);              // DB
}

¿Qué problema tiene esto?

Cuando Spring crea la transacción con @Transactional:

  1. Toma una conexión del pool y la mantiene durante TODO el método
  2. La conexión se queda ocupada mientras espera la respuesta de la API externa
  3. Si la API tarda 5-10 segundos, esa conexión está bloqueada todo ese tiempo
  4. Bajo carga alta, agotas todas las conexiones disponibles esperando APIs lentas

Regla de oro: Nunca mezcles operaciones de base de datos con llamadas a APIs externas dentro de una misma transacción.

Solución 1: TransactionTemplate (Recomendada)

TransactionTemplate proporciona una API basada en callbacks para gestionar transacciones manualmente. Es la forma más limpia y moderna de trabajar con transacciones programáticas.

Configuración Básica

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.stereotype.Component;

@Component
public class PaymentService {
    
    private final TransactionTemplate transactionTemplate;
    
    public PaymentService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
}

Nota: Spring Boot configura automáticamente un PlatformTransactionManager. Solo necesitas inyectarlo.

Ejemplo 1: Transacción con Retorno de Valor

public Long crearPagoExitoso(PaymentRequest request) {
    // Ejecutamos código dentro de una transacción y devolvemos el ID
    Long paymentId = transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(request.getAmount());
        payment.setReferenceNumber(request.getReference());
        payment.setState(Payment.State.SUCCESSFUL);
        
        entityManager.persist(payment);
        
        // El ID se genera automáticamente tras el persist
        return payment.getId();
    });
    
    return paymentId;
}

¿Qué hace esto?

  • Crea una transacción automáticamente
  • Ejecuta el código del lambda dentro de la transacción
  • Si todo va bien, hace commit automáticamente
  • Si hay excepción, hace rollback automáticamente
  • Devuelve el valor que retorna el lambda

Ejemplo 2: Rollback Automático por Excepción

public void crearDosPagosConRollback() {
    try {
        transactionTemplate.execute(status -> {
            Payment first = new Payment();
            first.setReferenceNumber("REF-001");
            first.setAmount(1000L);
            entityManager.persist(first);  // OK
            
            Payment second = new Payment();
            second.setReferenceNumber("REF-001"); // ¡Duplicado!
            second.setAmount(2000L);
            entityManager.persist(second);  // Lanza excepción
            
            return null;
        });
    } catch (Exception e) {
        // El primer pago TAMBIÉN se revierte - atomicidad garantizada
        System.out.println("Transacción revertida: " + e.getMessage());
    }
}

Ejemplo 3: Rollback Manual Explícito

public Long crearPagoConValidacion(PaymentRequest request) {
    return transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setReferenceNumber(request.getReference());
        payment.setAmount(request.getAmount());
        
        entityManager.persist(payment);
        
        // Validación de negocio personalizada
        if (request.getAmount() > 100000) {
            // Marcamos para rollback - la transacción se revertirá
            status.setRollbackOnly();
            return null;  // O lanzar excepción personalizada
        }
        
        return payment.getId();
    });
}

Ejemplo 4: Transacción Sin Valor de Retorno

public void guardarLogAuditoria(AuditEntry entry) {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            auditRepository.save(entry);
            // No devuelve nada, solo ejecuta operaciones
        }
    });
}

Ejemplo 5: Configuración Personalizada por Instancia

Puedes crear múltiples TransactionTemplate con configuraciones diferentes:

@Component
public class TransactionConfig {
    
    @Bean
    public TransactionTemplate readOnlyTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setReadOnly(true);  // Optimización para consultas
        return template;
    }
    
    @Bean
    public TransactionTemplate serializableTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
        template.setTimeout(30);  // Segundos
        return template;
    }
    
    @Bean
    public TransactionTemplate requiresNewTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return template;
    }
}

Configuraciones Disponibles

PropiedadValorDescripción
setIsolationLevel()ISOLATION_READ_UNCOMMITTEDNivel de aislamiento de lectura sucia
ISOLATION_READ_COMMITTEDLee solo datos confirmados
ISOLATION_REPEATABLE_READEvita lecturas no repetibles
ISOLATION_SERIALIZABLEMáximo aislamiento
setPropagationBehavior()PROPAGATION_REQUIREDÚsala si existe, crea si no
PROPAGATION_REQUIRES_NEWSiempre crea una nueva transacción
PROPAGATION_NESTEDTransacción anidada (savepoint)
setTimeout()Segundos (int)Tiempo máximo de ejecución
setReadOnly()true/falseOptimización para solo lectura

Solución 2: PlatformTransactionManager (Bajo Nivel)

Para control total, usa directamente PlatformTransactionManager. Es la API que usan internamente tanto @Transactional como TransactionTemplate.

Ejemplo: Control Manual Completo

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Component
public class ManualTransactionService {
    
    private final PlatformTransactionManager transactionManager;
    
    public ManualTransactionService(PlatformTransactionManager txManager) {
        this.transactionManager = txManager;
    }
    
    public void procesarConControlTotal() {
        // 1. Definir configuración de la transacción
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        definition.setTimeout(5);  // 5 segundos
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        
        // 2. Iniciar la transacción
        TransactionStatus status = transactionManager.getTransaction(definition);
        
        try {
            // 3. Ejecutar operaciones de negocio
            Payment payment = new Payment();
            payment.setAmount(500L);
            payment.setReferenceNumber("MANUAL-001");
            entityManager.persist(payment);
            
            // 4. Confirmar la transacción
            transactionManager.commit(status);
            
        } catch (Exception ex) {
            // 5. Revertir en caso de error
            transactionManager.rollback(status);
            throw new RuntimeException("Error en transacción manual", ex);
        }
    }
}

Comparación: TransactionTemplate vs PlatformTransactionManager

AspectoTransactionTemplatePlatformTransactionManager
Nivel de abstracciónAlto (callbacks)Bajo (manual)
Manejo de erroresAutomáticoManual (try-catch)
Rollback automáticoNo (debes llamarlo)
Código resultanteMás limpioMás verboso
FlexibilidadLimitada por callbackTotal control
Uso recomendadoLa mayoría de casosCuando necesitas control fino

Caso Práctico: Separando DB de API Externa

Problema original (con riesgo de agotamiento):

@Transactional
public void procesarOrden(Orden orden) {
    ordenRepository.save(orden);              // DB
    apiEnvio.crearEnvio(orden);               // API lenta
    notificacionService.notificarCliente(orden); // API lenta
}

Solución con TransactionTemplate:

@Component
public class OrdenService {
    
    private final TransactionTemplate txTemplate;
    private final OrdenRepository ordenRepository;
    private final ApiEnvioService apiEnvio;
    private final NotificacionService notificacionService;
    
    public void procesarOrdenSegura(Orden orden) {
        // Paso 1: Solo la parte de DB en transacción
        Long ordenId = txTemplate.execute(status -> {
            orden.setEstado("PROCESANDO");
            ordenRepository.save(orden);
            return orden.getId();
        });
        // ¡La conexión ya se liberó!
        
        // Paso 2: API externa (sin conexión ocupada)
        String trackingNumber = apiEnvio.crearEnvio(orden);
        
        // Paso 3: Otra API externa (sin conexión ocupada)
        notificacionService.notificarCliente(orden);
        
        // Paso 4: Actualizar estado final (nueva transacción corta)
        txTemplate.executeWithoutResult(status -> {
            Orden actualizada = ordenRepository.findById(ordenId).orElseThrow();
            actualizada.setTrackingNumber(trackingNumber);
            actualizada.setEstado("ENVIADO");
        });
    }
}

Cuándo Usar Cada Enfoque

Usa @Transactional (Declarativo) cuando:

  • Operaciones simples CRUD
  • No hay llamadas a APIs externas
  • No necesitas control condicional complejo
  • Quieres mantener el código limpio y legible

Usa TransactionTemplate (Programático) cuando:

  • Necesitas mezclar operaciones DB con I/O externa
  • Lógica condicional que determina rollback
  • Diferentes configuraciones de transacción en el mismo servicio
  • Necesitas retornar valores de la transacción

Usa PlatformTransactionManager cuando:

  • Necesitas control total del ciclo de vida
  • Múltiples transacciones en un mismo método
  • Integración con sistemas de transacción no estándar
  • Logging o auditoría detallada de transacciones

Conclusión

Las transacciones programáticas te dan el control que @Transactional no puede ofrecer. TransactionTemplate es tu mejor aliado para la mayoría de casos donde necesitas separar operaciones de base de datos de llamadas externas, evitando así el agotamiento del pool de conexiones.

Recuerda: El objetivo no es reemplazar @Transactional, sino complementarlo cuando las restricciones del enfoque declarativo limitan tu diseño.


Referencias y Recursos Adicionales

Documentación Oficial

  • Spring Framework - Programmatic Transaction Management: Guía completa del equipo de Spring sobre gestión programática de transacciones. docs.spring.io
  • Spring Data Access Documentation: Documentación oficial sobre acceso a datos y transacciones. docs.spring.io

Artículos Recomendados

  • Vlad Mihalcea - Spring Transaction and Connection Management: Análisis profundo sobre cómo Spring maneja las conexiones a base de datos y las transacciones, incluyendo optimizaciones para la adquisición lazy de conexiones. vladmihalcea.com
  • Baeldung - Programmatic Transaction Management in Spring: Tutorial práctico con ejemplos de TransactionTemplate y PlatformTransactionManager. baeldung.com
  • Marco Behler - Spring Transaction Management @Transactional In-Depth: Guía detallada sobre el funcionamiento interno de las transacciones en Spring. marcobehler.com

Mejores Prácticas Adicionales

  1. Configura auto-commit=false en tu pool de conexiones para permitir la adquisición lazy de conexiones
  2. Establece hibernate.connection.provider_disables_autocommit=true cuando uses Hibernate
  3. Considera DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION para maximizar la reutilización de conexiones
  4. Diseña la capa de servicios para que los métodos transaccionales se llamen lo más tarde posible en la ejecución