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:

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.

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:

- 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):

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ó:

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

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

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

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

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

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

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.

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-unflatMap— asíncron, ordre no determinista (usaconcatMapper 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ó:

Observer — el productor envia dades als subscriptors:

La connexió entre ells:

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

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:

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

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

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

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

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 Reactor | Virtual Threads | |
|---|---|---|
| Model de programació | Pipeline funcional | Imperatiu (sembla síncron) |
| Corba d’aprenentatge | Pronunciada | Baixa |
| Contrapressió | Nativa | Manual |
| Millor per a | I/O d’alta concurrència, streaming | Simplificar 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.