# Día 19: Programación Reactiva (Project Reactor, WebFlux, RxJava) vs Virtual Threads

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

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 19 de 30
- Fase 4: Seguridad, Concurrencia y Reactividad vs Virtual Threads
- Dedicación: 4h
- Profundidad: visión de conjunto
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/programacion-reactiva-webflux/

## Al terminar este día sabrás

- 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.


## Conceptos

### 1. 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)`).

**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.

*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
```

- **En una línea:** `Flux.range(1, 100).subscribe(new BaseSubscriber<Integer>() { protected void hookOnNext(Integer v) { request(1); } });`
- **Por qué importa:** Evita que un productor rápido desborde y sature la memoria de un consumidor lento.
- **Error típico:** Confundir programación asíncrona con programación reactiva; lo reactivo se centra en flujos de datos y propagación del cambio con contrapresión.
- **Java 17 frente a 21:** Compatible en Java 17 y Java 21.
- **Se conecta con:** Java 9 Flow API, Project Reactor, RxJava

### 2. Project Reactor en Spring WebFlux: Mono<T> y Flux<T>

Project Reactor es la implementación por defecto en Spring WebFlux. `Mono<T>` representa un flujo reactivo asíncrono de 0 o 1 elemento (equivalente a un Future o un Optional diferido). `Flux<T>` representa un flujo de 0 a N elementos (secuencia infinita o lista diferida).

**Cómo funciona por dentro.**

Project Reactor ofrece dos tipos de `Publisher`. `Mono<T>` emite cero o un elemento y termina: es el equivalente reactivo de devolver un valor o un `Optional`. `Flux<T>` emite de cero a N elementos, posiblemente infinitos: el equivalente de una lista o de un stream de eventos.

La regla de oro es que nada ocurre hasta que alguien se suscribe. Declarar un `Mono` que llama a una API no hace la llamada; solo describe qué hacer. En WebFlux, quien se suscribe es el framework al devolver tu `Mono` o `Flux` desde el controlador. Si llamas a `subscribe()` tú mismo dentro de un controlador, o a `block()`, casi siempre es un error.

WebFlux se ejecuta sobre Netty con un número pequeño de hilos de event loop (uno por núcleo). Cada hilo atiende muchas conexiones a la vez, saltando entre ellas cada vez que una espera E/S. Por eso es crucial no bloquear nunca esos hilos.

*Controlador WebFlux con un cliente reactivo*

```java
@RestController
@RequestMapping("/api/tareas")
class TareaReactivaController {
    private final TareaReactiveRepository repo;     // R2DBC: acceso a datos no bloqueante
    private final WebClient usuarios;

    TareaReactivaController(TareaReactiveRepository repo, WebClient.Builder b) {
        this.repo = repo;
        this.usuarios = b.baseUrl("http://usuarios").build();
    }

    @GetMapping("/{id}")
    Mono<TareaResponse> obtener(@PathVariable long id) {
        return repo.findById(id)                                   // Mono<Tarea>
            .switchIfEmpty(Mono.error(new TareaNoEncontradaException(id)))
            .map(TareaResponse::desde);
    }

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)      // flujo continuo (SSE)
    Flux<TareaResponse> enVivo() {
        return repo.findAll().map(TareaResponse::desde)
            .delayElements(Duration.ofMillis(200));
    }
}
```

- **En una línea:** `public Mono<UsuarioDto> buscarUsuario(Long id) { return usuarioRepo.findById(id).map(this::toDto); }`
- **Por qué importa:** Todo en WebFlux opera sobre estas dos abstracciones, ejecutándose en un bucle de eventos (Event Loop) sobre Netty con muy pocos hilos de SO (típicamente 1 por núcleo de CPU).
- **Error típico:** Llamar a `.block()` o `.blockFirst()` dentro de un flujo de WebFlux; esto bloquea el Event Loop de Netty y puede congelar el servidor entero.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Spring WebFlux, Netty, Event Loop

### 3. Operadores Reactivos Clave: map, flatMap, filter, zip y switchIfEmpty

Los operadores transforman los flujos: `map` transforma sincrónicamente valor a valor; `flatMap` transforma a otro flujo reactivo y lo aplana (ideal para llamadas a servicios asíncronos concurrentes); `zip` combina dos flujos en paralelo; `switchIfEmpty` proporciona un flujo alternativo si el principal viene vacío.

**Cómo funciona por dentro.**

`map` aplica una función síncrona a cada elemento: entra un `T`, sale un `R`. `flatMap` aplica una función que devuelve otro `Publisher` (por ejemplo, una llamada HTTP reactiva) y se suscribe a ellos en paralelo, mezclando los resultados según llegan; el orden no se conserva. `concatMap` hace lo mismo pero de uno en uno y en orden. `flatMapSequential` paraleliza y reordena al final.

Otros imprescindibles: `filter` para descartar, `zip` para esperar a varios publicadores y combinar sus resultados en una tupla, `switchIfEmpty` para dar un valor o error alternativo si no llegó nada, `onErrorResume` para recuperarse de errores con otro flujo, `timeout` para no esperar indefinidamente y `retryWhen(Retry.backoff(...))` para reintentar con espera exponencial.

`flatMap` acepta un parámetro de concurrencia (`flatMap(f, 8)`) que limita cuántas operaciones internas hay en vuelo: imprescindible para no lanzar 10.000 llamadas a la vez contra un servicio.

*Los operadores que más vas a usar*

```java
Mono<Panel> panel(long usuarioId) {
    Mono<Usuario> usuario = usuarios.buscar(usuarioId)
        .timeout(Duration.ofSeconds(2))
        .retryWhen(Retry.backoff(3, Duration.ofMillis(100)));

    Mono<List<TareaResponse>> tareas = repo.findByResponsable(usuarioId)
        .filter(t -> !t.completada())
        .map(TareaResponse::desde)                       // síncrono, 1 a 1
        .collectList();

    Mono<Estadisticas> stats = analitica.de(usuarioId)
        .onErrorResume(e -> Mono.just(Estadisticas.vacias()));   // degradación elegante

    return Mono.zip(usuario, tareas, stats)              // en paralelo, espera a los tres
        .map(t -> new Panel(t.getT1(), t.getT2(), t.getT3()));
}

Flux<Precio> precios(List<String> skus) {
    return Flux.fromIterable(skus)
        .flatMap(sku -> catalogo.precio(sku), 8);       // máximo 8 llamadas en vuelo, orden no garantizado
}
```

- **En una línea:** `usuarioRepo.findById(id).flatMap(u -> comprasRepo.findByUsuario(u.id())).switchIfEmpty(Mono.error(new NotFoundException()));`
- **Por qué importa:** Permite orquestar llamadas a múltiples microservicios concurrentes sin callback hell.
- **Error típico:** Usar `map` en lugar de `flatMap` cuando la función interna devuelve otro `Mono`, resultando en un indeseado `Mono<Mono<T>>`.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Composición Funcional, Microservicios, Resilience4j

### 4. El Gran Dilema Arquitectural: Spring MVC + Virtual Threads vs Spring WebFlux

Con la llegada de Java 21 y Virtual Threads, la comunidad se pregunta si WebFlux sigue teniendo sentido. Para peticiones clásicas Request-Response conectadas a bases de datos relacionales, Spring MVC con Virtual Threads es mucho más fácil de escribir, depurar (stack traces legibles) y mantener. WebFlux sigue siendo rey para: (1) Server-Sent Events (SSE) y streaming masivo de datos; (2) WebSockets y sistemas con contrapresión estricta; (3) Pasarelas API de enrutamiento puro (Spring Cloud Gateway).

**Cómo funciona por dentro.**

Hasta Java 21, la única forma de atender decenas de miles de conexiones concurrentes con pocos recursos era la programación reactiva. Con Virtual Threads, el estilo imperativo clásico (Spring MVC, JDBC, JPA) consigue una escalabilidad similar en cargas de E/S, con código más fácil de escribir, leer, depurar y probar.

Spring MVC con Virtual Threads es la opción por defecto para servicios CRUD, APIs de negocio y cualquier sistema que use JPA, que no tiene versión reactiva. El ecosistema de librerías bloqueantes es enorme y el equipo no necesita aprender un paradigma nuevo.

WebFlux sigue teniendo sentido cuando el flujo de datos es el protagonista: streaming continuo (Server-Sent Events, WebSockets), contrapresión de extremo a extremo con un productor rápido, gateways y proxies que solo encaminan tráfico (Spring Cloud Gateway está construido sobre él) o cuando ya tienes un ecosistema reactivo maduro. Mezclar ambos mundos sin cuidado (llamadas bloqueantes dentro de WebFlux) da lo peor de los dos.

*El mismo caso de uso en los dos estilos*

```java
// Spring MVC + Virtual Threads: imperativo, cada petición en un hilo virtual
@GetMapping("/{id}/detalle")
DetalleTarea detalle(@PathVariable long id) {
    Tarea tarea = repo.findById(id).orElseThrow();          // JPA bloqueante: el hilo virtual se desmonta
    Usuario responsable = usuarios.get()                     // RestClient bloqueante
        .uri("/usuarios/{id}", tarea.responsableId())
        .retrieve().body(Usuario.class);
    return new DetalleTarea(tarea, responsable);
}

// Spring WebFlux: declarativo, sin bloquear el event loop
@GetMapping("/{id}/detalle")
Mono<DetalleTarea> detalle(@PathVariable long id) {
    return repoReactivo.findById(id)                         // R2DBC
        .flatMap(tarea -> webClient.get()
            .uri("/usuarios/{id}", tarea.responsableId())
            .retrieve().bodyToMono(Usuario.class)
            .map(u -> new DetalleTarea(tarea, u)));
}
```

- **En una línea:** `// En Java 21 Spring MVC simple: var user = restClient.get().uri("/users/{id}", id).retrieve().body(User.class);`
- **Por qué importa:** Permite tomar decisiones de arquitectura técnica fundamentadas en criterios reales y no en modas.
- **Error típico:** Adoptar WebFlux en un equipo novato creyendo que es "más moderno" y terminar usando librerías bloqueantes (JDBC clásico) dentro de Netty.
- **Java 17 frente a 21:** Dilema central en la transición de Java 17 a 21.
- **Se conecta con:** Spring Cloud Gateway, Throughput vs Latencia, Mantenibilidad de Código

### 5. RxJava 3 y su Coexistencia con Project Reactor

RxJava (creado por Netflix) fue el pionero de las Reactive Extensions en la JVM. Aunque Spring estandarizó Project Reactor para el backend web, RxJava 3 sigue utilizándose en clientes Android, procesamiento de eventos en streaming y librerías especializadas.

**Cómo funciona por dentro.**

RxJava fue la librería que popularizó la programación reactiva en Java, sobre todo en Android. Su versión 3 ofrece `Observable` (sin contrapresión, para eventos de interfaz), `Flowable` (con contrapresión, implementa Reactive Streams), `Single` (exactamente un valor), `Maybe` (cero o uno) y `Completable` (solo éxito o error).

Project Reactor es la base de todo el ecosistema reactivo de Spring, así que en backend con Spring es la elección natural. Como `Flowable` y `Flux` implementan la misma interfaz `Publisher`, se pueden convertir entre sí sin pérdida: `Flux.from(flowable)` y `Flowable.fromPublisher(flux)`. Para `Single`, `Maybe` y `Completable` existe el adaptador `reactor-adapter`.

Esto permite integrar un SDK de terceros escrito en RxJava dentro de una aplicación WebFlux sin reescribirlo.

*Convertir entre RxJava 3 y Reactor*

```java
// SDK de terceros basado en RxJava 3
Flowable<Evento> eventosSdk = sdk.eventos();
Single<Cuenta> cuentaSdk = sdk.cuenta("ES00");

// A Reactor vía la interfaz común Publisher
Flux<Evento> eventos = Flux.from(eventosSdk);
Mono<Cuenta> cuenta = Mono.from(cuentaSdk.toFlowable());

// Con reactor-adapter (io.projectreactor.addons:reactor-adapter)
Mono<Cuenta> cuenta2 = RxJava3Adapter.singleToMono(cuentaSdk);

// Y de vuelta, si una librería exige RxJava
Flowable<TareaResponse> paraSdk = Flowable.fromPublisher(repoReactivo.findAll().map(TareaResponse::desde));
```

- **En una línea:** `Observable.just("evento").subscribe(System.out::println);`
- **Por qué importa:** Ambas librerías implementan el estándar `org.reactivestreams.Publisher`, permitiendo interoperabilidad fluida con `Flux.from(rxObservable)`.
- **Error típico:** Mezclar clases de RxJava (`io.reactivex.rxjava3.core.Observable`) con clases de Reactor (`reactor.core.publisher.Flux`) en las firmas de los servicios sin conversor explícito.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** ReactiveX, Android, Interoperabilidad


## Sesión de 90 minutos

### 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?
- **Entregable:** Test con `StepVerifier` verificando emisión ordenada de 3 elementos y señal `onComplete`.

### 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?
- **Entregable:** Pipeline reactivo que combine datos de un usuario y sus órdenes en paralelo con `Mono.zip()`.

### 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?
- **Entregable:** Tabla de decisión arquitectónica para tu equipo de trabajo.

### 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?
- **Entregable:** Resumen de conclusiones en la aplicación.


## Explícalo desde cero

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

- **Qué 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.
- **Qué 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.
- **Ejemplo sencillo:** 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.
- **Señal de alerta:** 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

### 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?

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.

**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.

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

`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>>`.

**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.

### 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.

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.

**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.


## Reto del tutor

**Reto:** 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 está bien:** Señala que usar `.block()` está prohibido en WebFlux porque detiene el hilo del Event Loop.
- **Respuesta superficial:** Decir que debe devolver `Mono<UsuarioDto>` en vez del objeto directo.
- **Lo que falta de fondo:** 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...`.
- **Respuesta senior:** 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.


## Katas

### Kata 19.1: Pipeline Reactivo con Mono, flatMap y StepVerifier

Implementa una función que reciba un ID, consulte un servicio reactivo simulado con `Mono.delay()`, aplique `flatMap` para obtener detalles secundarios, y verifica su comportamiento usando `reactor.test.StepVerifier` asegurando que el flujo emite el resultado esperado.

### Kata 19.2: Combinación Concurrente con Mono.zip

Crea dos métodos que devuelvan `Mono<PerfilDto>` y `Mono<ConfiguracionDto>` respectivamente simulando latencias de red, combínalos concurrentemente usando `Mono.zip()` en un `record DashboardDto`, y comprueba que el tiempo total de ejecución es el del más lento (ejecución paralela) y no la suma de ambos.


## 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ística | Java 17 | Java 21 |
| --- | --- | --- |
| 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 |


## Proyecto guiado «Pedidos»: Seguimiento del pedido en tiempo real

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

**Tareas:**

- Crea `GET /pedidos/{id}/eventos` que emita cada cambio de estado como Server-Sent Events.
- Impleméntalo con un `Flux` de Project Reactor alimentado por los cambios de estado del servicio.
- 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.


## Fuentes oficiales

- [Project Reactor: referencia](https://projectreactor.io/docs/core/release/reference/)
- [Spring Framework: WebFlux](https://docs.spring.io/spring-framework/reference/web/webflux.html)
- [Reactive Streams](https://www.reactive-streams.org/)

