javareactorspringreactiu

Project Reactor: Guia Pràctica de Programació Reactiva en Java

De la contrapressió als virtual threads, Mono/Flux i l'event loop — tot el que vaig aprendre construint sistemes de pagament reactius a BBVA, amb els errors corregits.

Project Reactor: Guia Pràctica de Programació Reactiva en Java

Després de passar anys construint sistemes de pagament cloud-native a BBVA amb Spring WebFlux, vaig escriure tot el que vaig aprendre sobre Project Reactor. Aquest post és una versió refinada d’aquelles notes — amb les concepcions errònies corregides.


Què és Project Reactor?

Project Reactor és una biblioteca de programació reactiva per a Java que serveix com a base per a Spring WebFlux. Permet el processament de dades asíncron i no bloquejant mitjançant streams orientats a esdeveniments (Mono i Flux) amb suport de contrapressió.

El problema central que resol: en una configuració tradicional de Spring MVC, cada petició ocupa un fil fins que es completa. Sota alta càrrega, s’acaben els fils. Reactor permet a un nombre petit i fixe de fils gestionar milers de peticions concurrents sense bloquejar mai — els fils passen a altra feina i reben una notificació quan els resultats estan llests.


El Problema amb el Codi Bloquejant

Un endpoint bloquejant clàssic té aquest aspecte:

@GetMapping("/users/{userId}")
public User getUserDetails(@PathVariable String userId) {
    User user = userService.getUser(userId);
    UserPreferences prefs = userPreferencesService.getPreferences(userId);
    user.setPreferences(prefs);
    return user;
}

Les dues crides al servei són innecessàriament seqüencials. Mentre la primera crida espera una resposta, el fil simplement queda inactiu:

Servidor web bloquejant — un fil per petició, tots inactius

Podries millorar-ho amb CompletableFuture, però aquella API té els seus propis problemes: join() segueix bloquejant, la gestió d’errors és complicada, i fas tot el cablatge manualment.

La solució reactiva:

@GetMapping("/users/{userId}")
public Mono<User> getUserDetails(@PathVariable String userId) {
    return userService.getUser(userId)
            .zipWith(userPreferencesService.getPreferences(userId))
            .map(tuple -> {
                User user = tuple.getT1();
                user.setPreferences(tuple.getT2());
                return user;
            });
}

No bloquejant, les dues crides s’executen en paral·lel, i la pipeline s’expressa de forma declarativa.


Què és la Contrapressió?

La contrapressió és un mecanisme de control de flux que evita que un productor ràpid desbordés un consumidor lent. El consumidor indica quanta dada pot gestionar, i el productor respecta aquell límit.

Pensa-hi com un aixeta que ajusta el cabal d’aigua en funció de la velocitat a la qual pots omplir el got.

onBackpressureBuffer — buffer entre un productor ràpid i un consumidor lent

A la pràctica: si tens un Flux emitent 10.000 esdeveniments per segon però la teva base de dades només pot gestionar 500 escriptures per segon, la contrapressió evita quedar-te sense memòria. El subscriptor demana només el que pot processar:

class MySubscriber<T> extends BaseSubscriber<T> {
    @Override
    public void hookOnSubscribe(Subscription subscription) {
        request(2); // demana només 2 elements alhora
    }

    @Override
    public void hookOnNext(T value) {
        System.out.println("Rebut: " + value);
        request(2); // demana 2 més quan acaba
    }
}

El Manifest Reactiu

Project Reactor es construeix al voltant del Manifest Reactiu, que defineix quatre pilars:

Manifest Reactiu — Responsiu, Resilient, Elàstic, Orientat a Missatges

  • Responsiu — ha de respondre de forma predictible en totes les condicions
  • Resilient — tolerant a fallades, es recupera de forma proactiva
  • Elàstic — escala cap amunt i cap avall en funció de la demanda
  • Orientat a Missatges — els components es comuniquen mitjançant enviament asíncron de missatges amb adreçament explícit

Nota: el quart pilar és Orientat a Missatges, no “orientat a esdeveniments”. Orientat a missatges implica destinataris explícits i transparència de localització. Orientat a esdeveniments és un concepte més ampli que no porta les mateixes garanties.


El Model d’Event Loop

El mecanisme fonamental darrere de Reactor i Netty (el servidor per defecte per a WebFlux):

Model d'Event Loop — Channel, EventLoop, EventQueue

Tres conceptes clau:

  • Channel — representa una connexió del client al servidor
  • Event Loop — el fil únic que processa tasques per a aquell channel. Un event loop per nucli de CPU
  • Event Queue — una cua de tasques FIFO (no una cua de prioritat) on esperen les tasques pendents

La regla crítica: l’event loop mai bloqueja. Quan troba una operació d’I/O bloquejant, la descarrega a un pool de fils separat i immediatament agafa la tasca següent. Quan l’I/O es completa, el resultat torna com a nou esdeveniment.

Seguim un exemple pas a pas:

Pas 1 — dues tasques no-I/O a la cua, arriba una nova petició:

Event loop — nova petició en cua mentre processa la tasca 1

Pas 2 — arriba una altra petició, s’encua com a tasca 4:

Event loop — tasca 4 en cua

Pas 3 — la tasca 3 es detecta com a operació d’I/O bloquejant, es descarrega al pool de fils:

Event loop — tasca d'I/O bloquejant descarregada al pool de fils

Pas 4 — mentre la tasca 3 s’executa al pool de fils, l’event loop respon a la tasca 4:

Event loop — resposta de tasca 4 enviada mentre la tasca 3 s'executa en paral·lel

Pas 5 — l’I/O de la tasca 3 es completa, el resultat es torna a l’event loop per finalitzar:

Event loop — tasca 3 es completa i torna a l'event loop

Conclusions clau:

  • L’event loop mai espera per I/O
  • Les operacions d’I/O van al pool de fils; les tasques no-I/O s’executen directament a l’event loop
  • A Netty, els fils d’event loop apareixen com reactor-http-nio-1, reactor-http-nio-2, etc.

Com Spring WebFlux ho Utilitza

Spring WebFlux — Flux flueix des del Data Repo a través del Controller fins al HTTP Server amb contrapressió al socket

Spring WebFlux s’asseu sobre Netty i Project Reactor. El Flux flueix des del repositori de dades a través del controlador fins al servidor HTTP, amb escriptures no bloquejants i contrapressió de tornada al socket.


Mono i Flux — Llegint Diagrames de Marbles

Mono<T> — un stream de 0 o 1 element:

Diagrama de marble de Mono — element opcional, senyal de completació, transformació d'operador

Flux<T> — un stream de 0 a N elements:

Diagrama de marble de Flux — múltiples elements, senyal de completació, transformació d'operador

Com llegir-los: el temps flueix d’esquerra a dreta. Cada cercle és un element. La línia vertical és el senyal de completació. Una X significa error (terminal). La caixa al mig és l’operador que s’aplica.

Un Flux pot emetre:

  • Qualsevol nombre d’elements (en l’ordre en què es produeixen)
  • Un esdeveniment de completació — terminal, res més vindrà després
  • Un esdeveniment de fallada — terminal, res més vindrà després

La completació i la fallada sempre són l’últim senyal. Mai rebràs un element després d’un esdeveniment terminal.


Res No Passa Fins que et Subscrius

Un Mono o Flux és lazy — descriu una pipeline, però res s’executa fins que algú s’hi subscriu.

Subscribe desencadena la pipeline — sense subscribe, res s'executa

Disposable subscribe = ReactiveSources
        .intNumbersFlux()
        .subscribe(
                number -> System.out.println(number),
                err -> System.out.println(err),
                () -> System.out.println("Completat")
        );

subscribe() retorna un Disposable — crida dispose() per cancel·lar. És idempotent.

L’API de Reactor utilitza el patró d’Interfície Fluent — cada operador retorna un nou Mono o Flux, de manera que els encadenes en una pipeline:

flux.filter(element -> element != null)
    .map(element -> element.toUpperCase())
    .flatMap(element -> externalService.enrich(element))
    .subscribe(element -> System.out.println(element));

Distincions clau d’operadors:

  • map — síncron, un-a-un
  • flatMap — asíncron, ordre no determinista (usa concatMap per preservar l’ordre)
  • doOnNext / doOnError — hooks d’efectes secundaris, no transformen el stream

Els Orígens: Iterator + Observer

La programació reactiva és la combinació de dos patrons de disseny ben coneguts.

Iterator — el consumidor extreu dades d’una col·lecció:

Patró Iterator — el consumidor recorre una col·lecció en arbre

Observer — el productor envia dades als subscriptors:

Patró Observer — el publicador notifica els subscriptors

La connexió entre ells:

Iterator vs Observer — control invertit del flux de dades

// Iterator — tu controles quan extreure
myList.forEach(element -> System.out.println(element));

// Observer — les dades s'envien quan estan disponibles
clicksChannel.addObserver(event -> System.out.println(event));

forEach vs addObserver — la diferència és qui controla el flux

La programació reactiva inverteix l’Iterator: en lloc que el consumidor extregui, el productor envia. Després afegeix contrapressió perquè el consumidor pugui indicar quant pot gestionar. Aquesta combinació — més la composició en cadena de muntatge d’operadors — és el moment en què tot encaixa.


Reactor vs Virtual Threads

Java 21 va introduir els virtual threads (Project Loom) com a alternativa.

Com funcionen els virtual threads:

1. El fil comença en un carrier thread:

Virtual thread inicia — muntat al Worker 1 del fork join pool

2. Es fa una crida bloquejant — es crida Continuation.yield():

Virtual thread bloqueja — Continuation.yield() desencadenada

3. El virtual thread es desmunta i la seva pila es copia al heap:

Continuation.yield() — pila copiada al heap, carrier thread alliberat

4. L’operació bloquejant es completa — Continuation.run() remunta el virtual thread:

Continuation.run() — pila restaurada del heap, virtual thread remuntat

5. Si el carrier thread original està ocupat, un altre roba la tasca (ForkJoinPool work stealing):

Work stealing — un altre worker agafa la tasca del heap

Correcció important sobre el cost de creació: els virtual threads són més barats de crear que els platform threads, no més cars. Un platform thread mapeja a un fil del SO amb una pila fixa de ~1MB — una crida al sistema pesada. Un virtual thread és un objecte JVM lleuger amb una pila dinàmica petita al heap. Pots crear-ne milions.

Project ReactorVirtual Threads
Model de programacióPipeline funcionalImperatiu (sembla síncron)
Corba d’aprenentatgePronunciadaBaixa
ContrapressióNativaManual
Millor per aI/O d’alta concurrència, streamingSimplificar codi d’I/O bloquejant

Usa virtual threads per a: embolcallar APIs bloquejants existents de forma més senzilla.
Usa Reactor per a: nous sistemes d’alt rendiment on la contrapressió i la composició de pipelines importen.


Quan Anar Reactiu

Ves reactiu si:

  • La teva app és intensiva en I/O (APIs, crides a BD, streaming)
  • Necessites control natiu de contrapressió
  • Estàs construint un nou servei sobre Spring WebFlux des de zero

Ves amb compte si:

  • Les tasques intensives en CPU dominen → el càlcul bloquejant deté l’event loop. Solució: publishOn(Schedulers.parallel())
  • Integres biblioteques bloquejants → Mono.fromCallable() + Schedulers.boundedElastic()
  • L’equip no está familiaritzat amb el paradigma — la corba d’aprenentatge té un cost real

Regla d’or: vés reactiu del tot, o no vagis reactiu gens. Barrejar codi bloquejant i no bloquejant a la mateixa pipeline és el pitjor dels dos mons.


Lectures Recomanades