Día 19 de 30 Fase 4: Seguridad, Concurrencia y Reactividad vs Virtual Threads

Programación Reactiva (Project Reactor, WebFlux, RxJava) vs Virtual Threads

Mono, Flux, Backpressure, Netty no bloqueante vs el Modelo Imperativo de Java 21

Visión de conjunto. Sales entendiendo cómo funciona y cuándo usarlo; dominarlo pide más práctica en un proyecto real.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 2 katas
Project ReactorSpring WebFluxRxJavaMonoFluxBackpressureVirtual Threads
Al terminar este día sabrás 4
  • Explicar el protocolo de Reactive Streams y cómo funciona la contrapresión.
  • Construir flujos con Mono y Flux y elegir el operador adecuado.
  • Detectar código bloqueante dentro de una aplicación reactiva.
  • Argumentar cuándo elegir WebFlux y cuándo Spring MVC con Virtual Threads.

Fundamentos de la Especificación Reactive Streams: Publisher, Subscriber y Subscription

Reactive Streams es el estándar para el procesamiento asíncrono de flujos de datos con Backpressure (contrapresión) no bloqueante. Un Publisher emite elementos, un Subscriber los consume, y la Subscription permite al consumidor solicitar exactamente cuántos elementos es capaz de procesar (request(n)).

En una línea Flux.range(1, 100).subscribe(new BaseSubscriber<Integer>() { protected void hookOnNext(Integer v) { request(1); } });

Un suscriptor que pide de uno en uno java
Flux.range(1, 1_000_000)
    .doOnRequest(n -> System.out.println("pedidos: " + n))
    .subscribe(new BaseSubscriber<Integer>() {

        @Override
        protected void hookOnSubscribe(Subscription s) {
            request(1);                          // empiezo pidiendo uno
        }

        @Override
        protected void hookOnNext(Integer valor) {
            procesarDespacio(valor);             // consumidor lento
            if (valor < 5) request(1);           // pido el siguiente solo cuando termino
            else cancel();                       // y puedo cancelar cuando quiera
        }
    });

// pedidos: 1
// pedidos: 1
// ... el productor nunca genera un millón de elementos de golpe
Cómo funciona por dentro

Reactive Streams es una especificación mínima (cuatro interfaces, incluidas en el JDK desde Java 9 como java.util.concurrent.Flow) para procesar flujos asíncronos con contrapresión. Un Publisher produce datos. Un Subscriber se suscribe y recibe una Subscription. A través de ella, el suscriptor pide cuántos elementos puede procesar con request(n), y el publicador nunca envía más de lo pedido.

La secuencia de señales es fija: onSubscribe, después cero o más onNext, y como mucho una señal final, onError u onComplete. Esa contrapresión (backpressure) es la diferencia clave con un simple callback o un stream de eventos: un consumidor lento no se ve desbordado por un productor rápido, porque es él quien marca el ritmo.

No se suele implementar estas interfaces a mano. Project Reactor, RxJava o los drivers reactivos las implementan y garantizan sus reglas; tú trabajas con operadores de alto nivel.

Evita que un productor rápido desborde y sature la memoria de un consumidor lento.

Se conecta con Java 9 Flow APIProject ReactorRxJava

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Anatomía de Mono y Flux (0 - 25 min)

    Qué estudiar. Creación de flujos con Mono.just(), Mono.fromCallable(), Flux.fromIterable(), y suscripción con StepVerifier.

    Pregunta a responder. ¿Cómo probar flujos reactivos unitariamente sin esperas reales de tiempo?

    Resultado esperado. Test con StepVerifier verificando emisión ordenada de 3 elementos y señal onComplete.

  2. Bloque 2: Operadores y Composición (25 - 50 min)

    Qué estudiar. Uso de flatMap, filter, zipWith, y manejo de errores con onErrorResume.

    Pregunta a responder. ¿Cómo ejecutar dos llamadas remotas concurrentemente y combinar sus resultados?

    Resultado esperado. Pipeline reactivo que combine datos de un usuario y sus órdenes en paralelo con Mono.zip().

  3. Bloque 3: WebFlux vs Virtual Threads Benchmark (50 - 80 min)

    Qué estudiar. Comparativa de throughput entre un endpoint WebFlux y un endpoint Spring MVC con Virtual Threads activados.

    Pregunta a responder. ¿En qué casos de uso sigue ganando WebFlux y en cuáles gana Virtual Threads?

    Resultado esperado. Tabla de decisión arquitectónica para tu equipo de trabajo.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Repaso de las reglas de oro de la reactividad y prevención de bloqueos.

    Pregunta a responder. ¿Por qué no se debe usar JPA clásico dentro de WebFlux?

    Resultado esperado. Resumen de conclusiones en la aplicación.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué en WebFlux "nada ocurre hasta que alguien se suscribe" (Deferred Execution)

Lo que debes entender

El código reactivo no ejecuta nada cuando llamas a los métodos. Solo construye una tubería (pipeline) de instrucciones. La tubería solo empieza a bombear agua cuando un suscriptor llama a .subscribe() (en una API web, Spring WebFlux hace la suscripción automáticamente al enviar la respuesta al cliente). Si creas un Mono pero nadie se suscribe, la consulta de base de datos JAMÁS se ejecuta.

Analogía para contarlo

Como diseñar un plano de fontanería para una casa: tener los tubos dibujados en el papel no hace que salga agua; el agua solo fluye cuando alguien abre el grifo final.

Lo que suele explicarse mal

Pensar que mono.doOnNext(x -> println(x)) imprime algo en el momento en que se declara la línea de código.

Señal de que aún no lo dominas

Si dices "ejecuté este método reactivo pero no guardó nada en la base de datos" y descubres que olvidaste suscribirte o retornar el Mono al controlador.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué llamar a Thread.sleep(1000) o repository.findById(id) (con Spring Data JPA clásico) dentro de un controlador de Spring WebFlux puede colapsar todo el servidor? Ver respuesta

Respuesta razonada

WebFlux se ejecuta sobre Netty con muy pocos hilos de event loop, normalmente uno por núcleo. Cada hilo atiende cientos o miles de conexiones a la vez, pasando a otra en cuanto una espera E/S. Si dentro de un operador llamas a Thread.sleep o a un repositorio JPA (JDBC es bloqueante), ese hilo se queda parado y todas las conexiones que gestiona se congelan. Con cuatro bloqueos simultáneos en una máquina de cuatro núcleos, el servidor deja de responder por completo.

Si no hay alternativa reactiva, la llamada bloqueante se aísla en un planificador pensado para ello: Mono.fromCallable(() -> repo.findById(id)).subscribeOn(Schedulers.boundedElastic()). En desarrollo, la librería BlockHound detecta llamadas bloqueantes en hilos no bloqueantes y lanza un error.

Qué está evaluando la pregunta

Evalúa la comprensión del modelo Event Loop de Netty. Netty no usa un hilo por petición, sino un pool minúsculo de hilos (típicamente igual al número de cores de CPU, e.g. 8 hilos). Si bloqueas uno de esos 8 hilos con un sleep o con JDBC bloqueante, has inutilizado el 12.5% de la capacidad de todo el servidor para atender a cualquier otro usuario.

Nivel 2 (Intermedio) ¿Cuál es la diferencia fundamental entre map y flatMap en Project Reactor? Ver respuesta

Respuesta razonada

map recibe una función síncrona T -> R y transforma cada elemento uno a uno, en el mismo orden. flatMap recibe una función T -> Publisher<R>: para cada elemento lanza una operación asíncrona (una llamada HTTP, una consulta), se suscribe a todas en paralelo y aplana los resultados en un único flujo según van llegando, así que el orden no se garantiza.

Regla práctica: si tu función devuelve un valor, map; si devuelve un Mono o un Flux, flatMap (o concatMap si necesitas orden y ejecución secuencial). Usar map con una función que devuelve Mono produce un Flux<Mono<R>> en el que las operaciones internas nunca se suscriben ni se ejecutan.

Qué está evaluando la pregunta

map toma un valor T y devuelve un valor sincrónico R (resultado: Mono<R>). flatMap toma un valor T y devuelve un Mono<R> asíncrono, desanidándolo para que el resultado siga siendo Mono<R> en lugar de Mono<Mono<R>>.

Nivel 3 (Entrevista Senior) Si tu empresa va a crear un nuevo microservicio CRUD en Java 21 que se conecta a PostgreSQL, ¿recomendarías Spring Boot MVC con Virtual Threads o Spring WebFlux con R2DBC? Justifica tu respuesta técnica. Ver respuesta

Respuesta razonada

Para un CRUD nuevo sobre PostgreSQL en Java 21 recomendaría Spring MVC con Virtual Threads, JPA o JdbcClient y HikariCP. Los hilos virtuales dan escalabilidad en E/S comparable a la reactiva para este tipo de carga, y el cuello de botella real será la base de datos y su pool de conexiones, no los hilos.

A cambio, el código es imperativo, legible y fácil de depurar (trazas de pila completas), hay muchas más librerías compatibles, JPA con su caché, dirty checking y relaciones está disponible (no existe JPA reactivo) y el equipo no necesita dominar Reactor. Elegiría WebFlux con R2DBC solo si el servicio necesita streaming con contrapresión, conexiones de larga duración como SSE o WebSockets en gran número, o si se integra en una plataforma que ya es reactiva de extremo a extremo.

Qué está evaluando la pregunta

Evalúa la madurez de arquitectura. En Java 21, Spring Boot MVC + Virtual Threads + Spring Data JPA (HikariCP) es la recomendación técnica superior para un CRUD relacional estándar: (1) R2DBC no soporta el estándar JPA completo y carece de muchas funcionalidades maduras de Hibernate; (2) La pila imperativa de MVC con Virtual Threads ofrece la misma capacidad de escalabilidad que WebFlux pero con stack traces comprensibles, soporte nativo de herramientas de depuración y menor coste cognitivo para el equipo.

Reto del tutor

Un desarrollador en un proyecto Spring WebFlux escribe este código en un servicio: ``java public UsuarioDto obtenerYValidar(Long id) { Usuario u = usuarioRepository.findById(id).block(); // Mono<Usuario> if (u == null) throw new NotFoundException(); return new UsuarioDto(u.id(), u.nombre()); } `` Explica por qué este código es inaceptable en WebFlux y cómo reescribirlo idiomáticamente.

Lo que una respuesta suele tener bien

Señala que usar .block() está prohibido en WebFlux porque detiene el hilo del Event Loop.

Lo que suele quedarse en la superficie

Decir que debe devolver Mono<UsuarioDto> en vez del objeto directo.

El concepto de fondo que suele faltar

No explicar que .block() bloquea el hilo reactor de Netty, rompiendo la promesa no bloqueante de la arquitectura reactiva y provocando IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread....

Cómo lo respondería alguien senior en una entrevista

El método viola la regla cardinal de la programación reactiva: JAMÁS bloquear el Event Loop de Netty. Al hacer .block(), el hilo que atiende a cientos de peticiones queda congelado esperando la base de datos. Se debe transformar el flujo reactivamente devolviendo Mono<UsuarioDto> y usando map y switchIfEmpty: ``java public Mono<UsuarioDto> obtenerYValidar(Long id) { return usuarioRepository.findById(id) .map(u -> new UsuarioDto(u.id(), u.nombre())) .switchIfEmpty(Mono.error(new NotFoundException())); } `` Así el hilo nunca se detiene y la respuesta fluye asíncronamente.

Proyecto «Pedidos» · paso 19 de 30

Seguimiento del pedido en tiempo real

Que el cliente reciba los cambios de estado de su pedido sin preguntar una y otra vez.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Crea GET /pedidos/{id}/eventos que emita cada cambio de estado como Server-Sent Events.
  2. Impleméntalo con un Flux de Project Reactor alimentado por los cambios de estado del servicio.
  3. Prueba el flujo con StepVerifier.

Está listo cuando

  • Al confirmar y pagar un pedido, un cliente conectado al endpoint recibe los dos eventos en orden.
  • Cerrar la conexión del cliente libera la suscripción.

Katas: ejercicios cortos

Reglas de oro

  • En Spring WebFlux, la llamada a .block() es un pecado capital; nunca bloquees el Event Loop de Netty.
  • Recuerda que en flujos reactivos nada ocurre hasta que alguien se suscribe (Nothing happens until you subscribe).
  • Usa map para transformaciones sincrónicas directas y flatMap para operaciones que devuelvan otro Mono o Flux.
  • Si usas Java 21 para APIs CRUD conectadas a bases de datos relacionales, prefiere Spring MVC con Virtual Threads antes que la complejidad de WebFlux + R2DBC.
  • Reserva Spring WebFlux para WebSockets, Server-Sent Events (SSE), procesamiento de flujos infinitos o API Gateways de alto tráfico.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Paradigma de Código Funcional declarativo con Mono/Flux Secuencial imperativo con Virtual Threads
Servidor Web Subyacente Netty no bloqueante (Event Loop) Tomcat tradicional pero sobre Virtual Threads
Stack Traces en Errores Complejos (asíncronos y desacoplados) Clásicos y directos (línea exacta del código)
Soporte Streaming SSE Nativo excelente con Flux Soportado, pero WebFlux sigue siendo más idóneo
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown