Type something to search...
Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

En el desarrollo de aplicaciones reactivas modernas, especialmente bajo paradigmas como la Arquitectura Hexagonal, surgen desafíos que nos obligan a repensar cómo aplicamos conceptos transversales. Uno de los interrogantes más comunes es: ¿dónde y cómo gestionamos las transacciones de base de datos sin contaminar nuestra lógica de negocio? Este artículo documenta un viaje desde esa pregunta inicial hasta una solución robusta y elegante, utilizando el poder de la Programación Orientada a Aspectos (AOP) en un entorno Spring WebFlux con R2DBC.

La Arquitectura como Punto de Partida

Antes de sumergirnos en el código, es fundamental visualizar la estructura del proyecto. Una organización clara de paquetes, que refleje las capas de la Arquitectura Hexagonal, es la base sobre la que construiremos nuestra solución. El dominio permanece en el centro, puro y sin dependencias externas, mientras que la aplicación y la infraestructura se organizan a su alrededor.

ms_auth/
├── applications/app-service/      # Módulo principal de la aplicación Spring Boot
   ├── build.gradle
   └── src/
       ├── main/java/com/app247/
   ├── MainApplication.java
   └── config/aop/
       └── TransactionalUseCaseAspect.java  # Nuestro Aspecto AOP
       └── test/java/com/app247/config/aop/
           ├── TransactionalUseCaseAspectTest.java  # Test unitario del Aspecto
           └── TransactionalRollbackSelfContainedTest.java # Test de Integración

├── domain/
   ├── model/
   └── usecase/                     # Módulo de la lógica de negocio pura
       └── src/main/java/com/app247/usecase/shared/core/usecase/
           ├── TransactionalWrapperUseCase.java # Anotación personalizada
           └── UseCase.java                     # Interfaz genérica

└── infrastructure/
    ├── r2dbc-postgresql/          # Módulo adaptador para la base de datos
    └── reactive-web/              # Módulo adaptador para los controladores REST

El Dilema Inicial: La Transacción y la Unidad de Trabajo

Todo comienza con una necesidad fundamental: asegurar la atomicidad de las operaciones. Imaginemos un caso de uso de negocio, como procesar una compra, que implica modificar el inventario de productos y crear un registro de orden. Ambas acciones deben tener éxito, o ninguna debe persistir. Esta es la definición de una unidad de trabajo, y la herramienta para garantizarla es la transacción.

La primera intuición podría ser colocar la anotación @Transactional de Spring en los métodos del repositorio. Sin embargo, esto es incorrecto. Una transacción en el repositorio solo cubriría una única operación de base de datos, rompiendo la unidad de trabajo del negocio. La transacción debe envolver la ejecución completa del caso de uso.

Esto nos lleva a la capa de servicio o caso de uso. Pero aquí nos encontramos con el primer gran obstáculo arquitectónico. En una Arquitectura Hexagonal, la capa de dominio (donde residen los casos de uso) debe ser pura. No puede, ni debe, tener dependencias de frameworks externos como Spring. Anotar un caso de uso del dominio con @Transactional viola este principio fundamental, acoplando nuestra lógica de negocio más preciada a un detalle de infraestructura.

La Solución Emerge: Programación Orientada a Aspectos

Si no podemos modificar el dominio, debemos aplicar el comportamiento transaccional desde afuera, de una manera no invasiva. Aquí es donde la Programación Orientada a Aspectos (AOP) brilla. AOP nos permite interceptar la ejecución de nuestros métodos para añadir funcionalidades transversales (como transacciones, seguridad o logging) sin alterar el código original.

La estrategia que emerge es crear un mecanismo declarativo y reutilizable que nos permita “marcar” qué casos de uso deben ser transaccionales, dejando que la magia de AOP haga el resto.

Una Anotación para Declarar la Intención

El primer paso es crear una anotación personalizada. Su único propósito es servir como una señal o marcador. Al ser parte de nuestro código de dominio (usecase), no introduce una dependencia directa de Spring, sino que define un contrato interno.

package com.app247.usecase.shared.core.usecase;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Anotación para marcar clases de Casos de Uso que deben ser
 * envueltas en una transacción reactiva de forma automática.
 */
@Target(ElementType.TYPE) // Se aplica a nivel de clase
@Retention(RetentionPolicy.RUNTIME) // Disponible en tiempo de ejecución para que Spring la lea
public @interface TransactionalWrapperUseCase {
}

Junto a esta, podemos definir una interfaz genérica para estandarizar nuestros casos de uso, promoviendo un diseño limpio y consistente.

package com.app247.usecase.shared.core.usecase;

// Interfaz genérica (opcional pero recomendada)
public interface UseCase<Request, Response> {
    Response execute(Request request);
}

El Aspecto: El Motor de la Transacción

Con la anotación en su lugar, construimos el componente que buscará esta marca y aplicará la lógica transaccional. Este es nuestro Aspecto, una clase de infraestructura que vive en la capa de aplicación.

Este Aspecto tiene dos partes clave:

  1. Pointcut: Una expresión que actúa como un selector. Le dice a Spring: “Encuentra todos los métodos públicos en cualquier clase que esté anotada con @TransactionalWrapperUseCase”.
  2. Advice: La lógica que se ejecuta cuando el Pointcut encuentra una coincidencia. Usaremos un advice de tipo @Around, que nos permite envolver completamente la ejecución del método original.

La lógica del advice es simple pero poderosa: toma el Mono o Flux devuelto por el caso de uso y lo compone con el TransactionalOperator reactivo de Spring. Este operador se encarga de iniciar la transacción antes de la suscripción y de realizar commit o rollback al finalizar.

package com.app247.config.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Aspect
@Component
public class TransactionalUseCaseAspect {

    private final TransactionalOperator transactionalOperator;

    public TransactionalUseCaseAspect(TransactionalOperator transactionalOperator) {
        this.transactionalOperator = transactionalOperator;
    }
    
    @Pointcut("@within(com.app247.usecase.shared.core.usecase.TransactionalWrapperUseCase) && execution(public * *(..))")
    public void transactionalUseCase() {
        // Método vacío para nombrar el pointcut.
    }

    @Around("transactionalUseCase()")
    public Object wrapInTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();

        if (result instanceof Mono) {
            return ((Mono<?>) result).as(transactionalOperator::transactional);
        } else if (result instanceof Flux) {
            return ((Flux<?>) result).as(transactionalOperator::transactional);
        }

        return result;
    }
}

Con estos dos elementos, hemos creado un sistema donde simplemente anotando una clase de caso de uso con @TransactionalWrapperUseCase, garantizamos que su ejecución será atómica, sin haber escrito una sola línea de código transaccional dentro del propio caso de uso.

Probando la Solución: De la Confianza a la Certeza

Una solución no está completa hasta que se prueba rigurosamente. Para este mecanismo, necesitamos dos niveles de prueba para tener una confianza total.

Nivel 1: El Test de Cableado (Unitario)

El primer test debe responder a la pregunta: ¿Nuestro aspecto AOP está correctamente configurado para interceptar la llamada y usar el TransactionalOperator? Este test valida tanto respuestas Mono como Flux.

Este test no necesita una base de datos. Utiliza un contexto de Spring para activar el mecanismo AOP, pero reemplaza todas las dependencias externas (TransactionalOperator, repositorios) con Mocks. El objetivo no es probar el rollback, sino verificar la interacción: que el método transactional() del operador sea invocado.

package com.app247.config.aop;

import com.app247.usecase.shared.core.usecase.TransactionalWrapperUseCase;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;


@SpringBootTest(classes = TransactionalUseCaseAspectTest.TestConfig.class)
class TransactionalUseCaseAspectTest {

    @Autowired
    private PurchaseProductUseCasePort purchaseUseCase;
    @Autowired
    private FindProductsUseCasePort findProductsUseCase; // Caso de uso que devuelve Flux
    @Autowired
    private NonReactiveUseCasePort nonReactiveUseCase;

    @MockitoBean
    private ProductRepository productRepository;
    @MockitoBean
    private OrderRepository orderRepository;
    @MockitoBean
    private TransactionalOperator transactionalOperator;
    @InjectMocks
    private TransactionalUseCaseAspect transactionalUseCaseAspect;

    @Test
    void whenUseCaseReturnsMono_thenItShouldBeWrappedInTransaction() {
        // ARRANGE
        Product fakeProduct = new Product("prod-123", 10);
        Order fakeOrder = new Order("user-007", "prod-123");

        when(productRepository.findById(any())).thenReturn(Mono.just(fakeProduct));
        when(orderRepository.save(any())).thenReturn(Mono.just(fakeOrder));
        when(productRepository.updateStock(any(), any(Integer.class))).thenReturn(Mono.empty());
        when(transactionalOperator.transactional(any(Mono.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));

        // ACT
        Mono<Order> result = purchaseUseCase.execute("user-007", "prod-123");

        // ASSERT
        StepVerifier.create(result).expectNext(fakeOrder).verifyComplete();
        verify(transactionalOperator).transactional(any(Mono.class));
        verify(productRepository).updateStock("prod-123", 9);
    }

    @Test
    void whenUseCaseReturnsFlux_thenItShouldBeWrappedInTransaction() {
        // ARRANGE
        Product fakeProduct1 = new Product("prod-001", 5);
        Product fakeProduct2 = new Product("prod-002", 3);

        when(productRepository.findAll()).thenReturn(Flux.just(fakeProduct1, fakeProduct2));
        // Configuramos el mock para que el operador transaccional simplemente devuelva el Flux original
        when(transactionalOperator.transactional(any(Flux.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));

        // ACT
        Flux<Product> result = findProductsUseCase.execute(null); // `null` porque no requiere parámetros

        // ASSERT
        StepVerifier.create(result)
                .expectNext(fakeProduct1)
                .expectNext(fakeProduct2)
                .verifyComplete();

        // La verificación clave: ¿Se llamó al operador con un Flux?
        verify(transactionalOperator).transactional(any(Flux.class));
    }

    /**
     * Test para el caso no reactivo.
     */
    @Test
    void whenUseCaseIsNotReactive_thenItShouldNotBeWrappedInTransaction() {
        // --- ARRANGE (Preparar) ---
        String expectedResult = "Este es un resultado síncrono";

        // --- ACT (Actuar) ---
        // Ejecutamos el caso de uso que devuelve un String simple.
        String actualResult = nonReactiveUseCase.execute(null);

        // --- ASSERT (Verificar) ---

        // 1. Verificamos que el resultado devuelto es el original, sin cambios.
        assertThat(actualResult).isEqualTo(expectedResult);

        // 2. La verificación MÁS IMPORTANTE: nos aseguramos de que el operador transaccional
        //    NUNCA fue invocado, ya que la respuesta no era ni Mono ni Flux.
        verify(transactionalOperator, never()).transactional(any(Mono.class));
        verify(transactionalOperator, never()).transactional(any(Flux.class));
    }

    @Test
    void transactionalUseCasePointcut_shouldExecuteForCoverage() {
        // --- ACT ---
        // Simplemente llamamos al método vacío.
        // La herramienta de cobertura registrará que se ha entrado en este método.

        // --- ASSERT ---
        // Como el método no hace nada, la única aserción posible es
        // que la llamada no lance ninguna excepción.
        assertDoesNotThrow(() -> {
            transactionalUseCaseAspect.transactionalUseCase();
        });
    }


    @Configuration
    @EnableAspectJAutoProxy
    @Import(TransactionalUseCaseAspect.class)
    static class TestConfig {
        @Bean
        public PurchaseProductUseCasePort purchaseProductUseCase(ProductRepository productRepo, OrderRepository orderRepo) {
            return new PurchaseProductUseCase(productRepo, orderRepo);
        }

        @Bean
        public FindProductsUseCasePort findProductsUseCase(ProductRepository productRepo) {
            return new FindProductsUseCase(productRepo);
        }
        @Bean
        public NonReactiveUseCasePort nonReactiveUseCase() {
            return new NonReactiveUseCase();
        }
    }

    // --- Definiciones Fakes ---
    record Product(String id, int stock) {}
    record Order(String userId, String productId) {}

    interface ProductRepository {
        Mono<Product> findById(String productId);
        Flux<Product> findAll(); // Añadido para el test de Flux
        Mono<Void> updateStock(String productId, int newStock);
    }
    interface OrderRepository { Mono<Order> save(Order order); }

    interface PurchaseProductUseCasePort { Mono<Order> execute(String userId, String productId); }
    interface FindProductsUseCasePort { Flux<Product> execute(Void request); } // Nuevo caso de uso para Flux

    interface NonReactiveUseCasePort { String execute(Void request); }

    @TransactionalWrapperUseCase
    static class PurchaseProductUseCase implements PurchaseProductUseCasePort {
        private final ProductRepository productRepository;
        private final OrderRepository orderRepository;
        public PurchaseProductUseCase(ProductRepository p, OrderRepository o) { this.productRepository = p; this.orderRepository = o; }
        public Mono<Order> execute(String userId, String productId) {
            return productRepository.findById(productId)
                    .flatMap(product -> productRepository.updateStock(product.id(), product.stock() - 1)
                            .then(orderRepository.save(new Order(userId, productId))));
        }
    }

    @TransactionalWrapperUseCase
    static class FindProductsUseCase implements FindProductsUseCasePort {
        private final ProductRepository productRepository;
        public FindProductsUseCase(ProductRepository p) { this.productRepository = p; }
        public Flux<Product> execute(Void request) {
            return productRepository.findAll();
        }
    }

    @TransactionalWrapperUseCase
    static class NonReactiveUseCase implements NonReactiveUseCasePort {
        @Override
        public String execute(Void request) {
            return "Este es un resultado síncrono";
        }
    }
}

Nivel 2: El Test de Comportamiento (Integración)

El segundo test debe responder a una pregunta más importante: si una operación falla, ¿la transacción realmente hace rollback?

Para esto, necesitamos un test de integración que utilice una base de datos real (en memoria, como H2, para velocidad y aislamiento) y el TransactionalOperator real de Spring. La clave aquí es usar @SpyBean para envolver un repositorio real y forzar un fallo en una de sus operaciones. La validación final consiste en consultar la base de datos después del fallo y verificar que el estado de los datos ha sido revertido a su estado original.

Este test es completamente autocontenido: define su propia configuración, su esquema de base de datos y sus implementaciones de dominio e infraestructura, pero lo más importante es que importa y prueba el Aspecto de AOP de producción real.

package com.app247.config.aop;

import com.app247.usecase.shared.core.usecase.TransactionalWrapperUseCase;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Repository;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.springframework.data.relational.core.query.Criteria.where;
import static org.springframework.data.relational.core.query.Query.query;

@SpringBootTest(classes = TransactionalRollbackSelfContainedTest.TestConfig.class)
@ImportAutoConfiguration({
        R2dbcAutoConfiguration.class,
        TransactionAutoConfiguration.class,
        PropertyPlaceholderAutoConfiguration.class
})
@TestPropertySource(properties = {
        "spring.r2dbc.url=r2dbc:h2:mem:///finaltestdb;DB_CLOSE_DELAY=-1;",
        "spring.r2dbc.username=sa",
        "spring.r2dbc.password=",
        "spring.sql.init.mode=never"
})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TransactionalRollbackSelfContainedTest {

    @Autowired private PurchaseProductUseCasePort purchaseUseCase;
    @Autowired private DatabaseClient databaseClient;
    @Autowired private R2dbcEntityTemplate template;
    @MockitoSpyBean
    private OrderRepository orderRepository;

    private final String PRODUCT_ID = "prod-123";
    private final int INITIAL_STOCK = 10;

    @BeforeAll
    void setupDatabaseSchema() {
        String createProductsTable = "CREATE TABLE PRODUCTS (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), stock INT);";
        String createOrdersTable = "CREATE TABLE ORDERS (id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255), product_id VARCHAR(255));";
        databaseClient.sql(createProductsTable).then().block();
        databaseClient.sql(createOrdersTable).then().block();
    }

    @BeforeEach
    void setupTestData() {
        databaseClient.sql("DELETE FROM PRODUCTS").then().block();
        template.insert(new ProductEntity(PRODUCT_ID, "Test Product", INITIAL_STOCK)).block();
    }

    @Test
    void whenSecondOperationFails_thenRealAspectRollsBackTransaction() {
        doReturn(Mono.error(new RuntimeException("DB Error"))).when(orderRepository).save(any());
        Mono<Void> result = purchaseUseCase.execute("user-007", PRODUCT_ID);
        StepVerifier.create(result).expectError(RuntimeException.class).verify();
        ProductEntity productAfter = template.selectOne(query(where("id").is(PRODUCT_ID)), ProductEntity.class).block();
        assertThat(productAfter.stock()).isEqualTo(INITIAL_STOCK);
    }

    @Configuration
    @EnableAspectJAutoProxy
    @Import(TransactionalUseCaseAspect.class)
    static class TestConfig {
        @Bean
        public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) {
            return new R2dbcEntityTemplate(connectionFactory);
        }

        @Bean
        public R2dbcTransactionManager transactionManager(ConnectionFactory connectionFactory) {
            return new R2dbcTransactionManager(connectionFactory);
        }

        @Bean
        public PurchaseProductUseCasePort purchaseProductUseCase(ProductRepository productRepo, OrderRepository orderRepo) {
            return new PurchaseProductUseCase(productRepo, orderRepo);
        }
        @Bean
        public ProductRepository productRepository(R2dbcEntityTemplate template) {
            return new R2dbcProductRepositoryAdapter(template);
        }
        @Bean
        public OrderRepository orderRepository(R2dbcEntityTemplate template) {
            return new R2dbcOrderRepositoryAdapter(template);
        }
    }

    record Product(String id, int stock) {}
    record Order(String userId, String productId) {}
    interface ProductRepository { Mono<Product> findById(String id); Mono<Void> updateStock(String id, int stock); }
    interface OrderRepository { Mono<Order> save(Order order); }
    interface PurchaseProductUseCasePort { Mono<Void> execute(String userId, String productId); }

    @Table("PRODUCTS")
    record ProductEntity(@Id String id, String name, int stock) {}
    @Table("ORDERS")
    record OrderEntity(@Id Integer id, String userId, String productId) {}

    @Repository
    static class R2dbcProductRepositoryAdapter implements ProductRepository {
        private final R2dbcEntityTemplate template;
        public R2dbcProductRepositoryAdapter(R2dbcEntityTemplate t) { this.template = t; }
        public Mono<Product> findById(String id) { return template.selectOne(query(where("id").is(id)),ProductEntity.class).map(e -> new Product(e.id(), e.stock())); }
        public Mono<Void> updateStock(String id, int stock) { return template.getDatabaseClient().sql("UPDATE PRODUCTS SET stock = :s WHERE id = :i").bind("s", stock).bind("i", id).fetch().rowsUpdated().then(); }
    }
    @Repository
    static class R2dbcOrderRepositoryAdapter implements OrderRepository {
        private final R2dbcEntityTemplate template;
        public R2dbcOrderRepositoryAdapter(R2dbcEntityTemplate t) { this.template = t; }
        public Mono<Order> save(Order o) { return template.insert(new OrderEntity(null, o.userId(), o.productId())).map(e -> o); }
    }

    @TransactionalWrapperUseCase
    static class PurchaseProductUseCase implements PurchaseProductUseCasePort {
        private final ProductRepository pRepo;
        private final OrderRepository oRepo;
        public PurchaseProductUseCase(ProductRepository p, OrderRepository o) { this.pRepo = p; this.oRepo = o; }
        public Mono<Void> execute(String userId, String productId) {
            return pRepo.findById(productId).flatMap(p -> pRepo.updateStock(p.id(), p.stock() - 1)).then(oRepo.save(new Order(userId, productId))).then();
        }
    }
}

Conclusión y Próximos Pasos

Hemos construido una solución completa, limpia y robusta para un problema complejo. Al mantener nuestro dominio puro y delegar las responsabilidades transversales a la capa de aplicación mediante AOP, logramos un código desacoplado, mantenible y altamente testeable. Las dependencias del proyecto reflejan esta arquitectura limpia, utilizando starters de Spring Boot para AOP y R2DBC, y librerías de prueba para H2 y ArchUnit.

// build.gradle
dependencies {
    implementation 'org.reactivecommons.utils:object-mapper:0.1.0'
    implementation project(':r2dbc-postgresql')
    implementation project(':reactive-web')
    implementation project(':model')
    implementation project(':usecase')
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    runtimeOnly('org.springframework.boot:spring-boot-devtools')
    testImplementation 'com.tngtech.archunit:archunit:1.4.1'
    testImplementation 'com.fasterxml.jackson.core:jackson-databind'
    testImplementation 'com.h2database:h2'
    testImplementation 'io.r2dbc:r2dbc-h2'
}

Este patrón no se limita a las transacciones. El mismo mecanismo de anotación y aspecto puede extenderse para manejar otras responsabilidades, como la autorización de seguridad, la auditoría o el registro de métricas, consolidándose como una base sólida para el desarrollo de futuras funcionalidades en cualquier aplicación reactiva que aspire a una arquitectura limpia y escalable.

Related Posts

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

En el desarrollo de aplicaciones Java, el manejo de respuestas a solicitudes —especialmente aquellas que involucran operaciones asincrónicas, procesamiento de datos o comunicación con servicios extern

Leer más
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo t

Leer más
El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

En el vertiginoso universo del desarrollo de software, donde la seguridad es un pilar innegociable y la agilidad es la moneda de cambio, nos enfrentamos a desafíos que van más allá de la simple lógica

Leer más
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

En el desarrollo de software moderno, escribir código que funciona perfectamente en escenarios ideales es solo el primer paso. La verdadera fortaleza de un sistema se manifiesta cuando debe enfrentar

Leer más
Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

En el desarrollo de APIs, el manejo de errores suele ser un ciudadano de segunda clase. Con frecuencia, nos conformamos con respuestas de error genéricas, inconsistentes o, en el peor de los casos, co

Leer más
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

En el universo del desarrollo de software empresarial, nos enfrentamos a un dilema constante: cómo manejar información transversal —ese rastro de datos vitales como IDs de correlación, información del

Leer más
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC Imagina a un desarrollador frontend consumiendo tu API. En un endpoint, recibe un objeto JSON. En otro, una simple lista. S

Leer más
Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

En entornos modernos de microservicios, la consistencia en las respuestas de una API es más que una cuestión de estética: es un factor crítico para la mantenibilidad, observabilidad y experiencia del

Leer más
Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

En proyectos de gran escala, identificar rápidamente el tipo de una clase facilita la comprensión del código, la colaboración y la depuración. Sin un esquema de nombres estandarizado, es común que los

Leer más
Estructuración de Carpetas en Proyectos de Software

Estructuración de Carpetas en Proyectos de Software

En proyectos de software de gran escala, la falta de una estructura de carpetas bien definida puede generar desorden, dificultando la mantenibilidad, escalabilidad y comprensión del código. Muchas vec

Leer más
Desarrollo de Software Implementando Gitflow

Desarrollo de Software Implementando Gitflow

Introducción El desarrollo de software requiere metodologías y flujos de trabajo que permitan un control eficiente del código fuente. Uno de los enfoques más utilizados para la gestión de versiones

Leer más
Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Introducción En el mundo de Java, trabajar con colecciones de datos solía ser sinónimo de bucles interminables, condicionales anidados y código repetitivo. Pero con la llegada de Java Streams

Leer más
Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

El Costo Oculto de Traer Demasiada Información En el desarrollo de aplicaciones que interactúan con bases de datos, una tarea fundamental es la recuperación de datos. Al usar Object-Relational Map

Leer más
Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

En el ámbito del desarrollo de software, la elección de una estrategia de versionamiento eficiente y estable es fundamental para el éxito de un equipo. El Trunk Based Development (TBD) tradicional, do

Leer más
Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

En el ecosistema de aplicaciones web modernas, la arquitectura de microservicios distribuidos se ha consolidado como un paradigma dominante. Su flexibilidad, escalabilidad y resiliencia son innegables

Leer más
Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

En el vertiginoso universo del desarrollo de software, la eficiencia no es un lujo, es una necesidad. Dedicar tiempo a tareas repetitivas como compilar código, ejecutar pruebas, empaquetar la aplicaci

Leer más
Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

En el artículo anterior, alcanzamos un hito crucial: construimos un plugin binario funcional en Java, completo con su propia configuración y tarea. Nuestro plugin "saludador" demostró que dominamos la

Leer más
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

En nuestro artículo anterior, desmitificamos Gradle y sentamos las bases para entender su funcionamiento. Aprendimos a crear proyectos, ejecutar tareas y comprendimos el rol fundamental de los plugins

Leer más
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente

Leer más