Zademy

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

spring-boot; transactions
words palabras

@Transactional es una de esas anotaciones que te cambian la vida cuando descubres Spring. La pones, y ya tienes transacciones sin tocar una línea de JDBC. Pero un día llega un bug de producción, el pool de conexiones se agota, y te das cuenta de que esa anotación tan cómoda tiene un coste que no habías visto. Aquí va lo que aprendí cuando me tocó lidiar con eso.

¿Por qué necesitas transacciones programáticas?

El problema del agotamiento de conexiones

El escenario clásico: un método que guarda cosas en la base de datos y también llama a una API externa.

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

Parece inofensivo. No lo es. Cuando Spring abre la transacción con @Transactional, toma una conexión del pool y la retiene durante toda la ejecución del método. Si la API del proveedor de pagos tarda cinco o diez segundos, esa conexión se queda bloqueada esperando, sin hacer nada útil. Bajo carga alta, todas las conexiones del pool terminan esperando a APIs lentas y las peticiones nuevas se encolan hasta que el sistema colapsa.

La regla que te ahorra dolores de cabeza: no mezcles operaciones de base de datos con llamadas a APIs externas dentro de la misma transacción.

Solución 1: TransactionTemplate (la recomendada)

TransactionTemplate te da una API basada en callbacks para manejar transacciones manualmente. Es la forma más limpia que conozco de separar lo que necesita transacción de lo que no.

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);
    }
}

Spring Boot ya configura un PlatformTransactionManager automáticamente. Solo necesitas inyectarlo y construir el template.

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;
}

El callback se ejecuta dentro de una transacción. Si el lambda termina sin excepción, Spring hace commit. Si lanza, hace rollback automático. Y lo que devuelva el lambda es lo que recibe el método. Así de simple.

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());
    }
}

El segundo persist revienta porque el reference number está duplicado. Spring hace rollback de toda la transacción, incluido el primer pago. Eso es la atomicidad funcionando: o entra todo, o no entra nada.

Rollback manual explícito

A veces no quieres lanzar una excepción sino marcar la transacción para rollback basándote en una regla de negocio:

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();
    });
}

status.setRollbackOnly() le dice a Spring que haga rollback cuando termine el callback. Útil cuando una validación de negocio falla y quieres revertir sin usar excepciones como mecanismo de control de flujo.

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
        }
    });
}

Configuración personalizada por instancia

Puedes definir varios TransactionTemplate como beans, cada uno con su propia configuración. Esto es útil cuando partes del código necesitan isolation level distinto o timeouts 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;
    }
}

En cuanto a lo que puedes configurar: setIsolationLevel() controla el nivel de aislamiento, desde ISOLATION_READ_UNCOMMITTED (permite lecturas sucias) hasta ISOLATION_SERIALIZABLE (máximo aislamiento, peor rendimiento). setPropagationBehavior() decide qué hace Spring si ya existe una transacción activa: PROPAGATION_REQUIRED la reutiliza, PROPAGATION_REQUIRES_NEW suspende la actual y crea una nueva, PROPAGATION_NESTED crea una transacción anidada con savepoint. setTimeout() mata la transacción si pasa demasiado tiempo, y setReadOnly(true) le permite a la base de datos optimizar la consulta.

Solución 2: PlatformTransactionManager (bajo nivel)

Para control absoluto del ciclo de vida, puedes usar PlatformTransactionManager directamente. Es la API que usan internamente @Transactional y TransactionTemplate, sin el azúcar.

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);
        }
    }
}

La diferencia con TransactionTemplate es que aquí tú manejas todo: creas la definición, inicias la transacción, haces commit o rollback manualmente, y si te olvidas del catch, la conexión se queda colgada. Es verboso pero te da control que el callback no permite, como ejecutar lógica entre el inicio y el commit, o manejar varias transacciones en un mismo método.

Mi regla general: TransactionTemplate para el 90% de los casos, PlatformTransactionManager solo cuando necesitas algo que el callback no te deja hacer.

Caso práctico: separando DB de API externa

El problema original que mencioné arriba, resuelto:

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

Con TransactionTemplate, separas las operaciones de base de datos en transacciones cortas y dejas las llamadas externas fuera:

@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");
        });
    }
}

La conexión de base de datos se toma solo durante el guardado inicial, se libera, y luego las llamadas a las APIs externas corren sin retener nada. La actualización final coge otra conexión, hace su trabajo, y la suelta. El pool respira.

Cuándo usar cada enfoque

@Transactional te sirve para operaciones CRUD simples, sin llamadas externas, sin lógica condicional de rollback. Es lo más legible y lo que deberías usar por defecto.

TransactionTemplate entra cuando mezclas base de datos con I/O externa, cuando necesitas rollback condicional sin lanzar excepciones, o cuando quieres diferentes configuraciones de transacción en el mismo servicio.

PlatformTransactionManager reservalo para cuando necesitas control total del ciclo de vida: múltiples transacciones en un mismo método, integración con sistemas no estándar, o logging detallado del estado de cada operación.

El objetivo no es reemplazar @Transactional. Es complementarlo cuando el enfoque declarativo se queda corto.


Referencias y recursos

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