Introducción
La programación reactiva con Spring WebFlux ha revolucionado la forma de construir aplicaciones modernas en Java. En el corazón de este paradigma encontramos dos protagonistas fundamentales: Mono y Flux, los publishers reactivos que transforman completamente cómo manejamos datos asíncronos en Spring Boot.
Si alguna vez te has preguntado cuándo usar Mono versus Flux, o por qué tu aplicación reactiva no funciona como esperabas, este artículo es para ti. Además, exploraremos desde los conceptos básicos hasta las mejores prácticas avanzadas que necesitas dominar.
La diferencia entre Mono y Flux en Spring Boot no es solo técnica, sino estratégica. Por consiguiente, elegir correctamente entre estos publishers reactivos puede significar la diferencia entre una aplicación eficiente y una que desperdicia recursos. Veremos ejemplos prácticos, casos de uso reales y los errores más comunes que cometen los desarrolladores al trabajar con programación reactiva en Spring WebFlux.
¿Qué son Mono y Flux en Spring Boot?
Definición de Mono
Mono es un Publisher reactivo que emite 0 o 1 elemento y luego completa (con éxito o error). Piénsalo como un Optional asíncrono y no bloqueante. Es perfecto para operaciones que retornan un único resultado, como buscar un usuario por ID o guardar una entidad en la base de datos.
// Ejemplo básico de Mono
Mono<User> userMono = Mono.just(new User("John", "Doe"));
// Mono vacío
Mono<User> emptyMono = Mono.empty();
// Mono con error
Mono<User> errorMono = Mono.error(new RuntimeException("Usuario no encontrado"));
Definición de Flux
Por otro lado, Flux es un Publisher reactivo que emite 0 a N elementos y luego completa. Es el equivalente reactivo a un Stream o List, ideal para manejar colecciones de datos o flujos continuos de información.
// Flux con múltiples elementos
Flux<Integer> numberFlux = Flux.just(1, 2, 3, 4, 5);
// Flux desde una lista
List<String> names = Arrays.asList("Ana", "Carlos", "María");
Flux<String> namesFlux = Flux.fromIterable(names);
// Flux infinito
Flux<Long> infiniteFlux = Flux.interval(Duration.ofSeconds(1));
Características fundamentales de los Publishers Reactivos
Tanto Mono como Flux comparten características esenciales del paradigma reactivo. Primero, son lazy (perezosos), lo que significa que no ejecutan nada hasta que alguien se suscribe. Segundo, soportan backpressure, permitiendo que el consumidor controle la velocidad de emisión de datos. Tercero, son inmutables, cada operación crea una nueva instancia.
Mostrar imagen Alt: Diagrama comparativo de Mono emitiendo un elemento versus Flux emitiendo múltiples elementos en Spring WebFlux
Importancia y Beneficios de Usar Mono y Flux

Ventajas de la Programación Reactiva
La programación reactiva con Mono y Flux en Spring Boot ofrece beneficios significativos. En primer lugar, el manejo eficiente de recursos permite que tu aplicación maneje miles de conexiones concurrentes sin bloquear threads. Esto es especialmente valioso en arquitecturas de microservicios donde la latencia de red es inevitable.
Además, el modelo no bloqueante mejora drásticamente el rendimiento. Mientras que Spring MVC tradicional asigna un thread por request, WebFlux con Mono y Flux puede manejar múltiples requests con pocos threads. Como resultado, reduces el consumo de memoria y aumentas el throughput de tu aplicación.
Casos donde Mono y Flux brillan
Los publishers reactivos son particularmente útiles en escenarios específicos. Por ejemplo, cuando trabajas con bases de datos reactivas como MongoDB Reactive o R2DBC, Mono y Flux se integran naturalmente. También son ideales para streaming de datos en tiempo real, donde necesitas enviar actualizaciones continuas al cliente.
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
// Retorna un único usuario - Uso de Mono
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable String id) {
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException(id)));
}
// Retorna múltiples usuarios - Uso de Flux
@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll()
.delayElements(Duration.ofMillis(100));// Simula procesamiento
}
}
Ejemplo de código: Operadores de transformación
// Mono: transformación de un solo elemento
Mono<String> mono = Mono.just("Daniel")
.map(name -> name.toUpperCase())
.flatMap(name -> Mono.just("Hola " + name));
// Flux: transformación de múltiples elementos
Flux<Integer> flux = Flux.just(1, 2, 3, 4)
.map(i -> i * 2)
.filter(i -> i > 4)
.flatMap(i -> Flux.just(i, i + 1));
Operadores comunes en IDE
Cuando trabajas en un IDE, al escribir Mono.
o Flux.
, verás sugerencias como:
Tipo | Operadores comunes | Descripción breve |
---|---|---|
Mono | map , flatMap , filter , zipWith | Transforman o combinan un solo valor |
Flux | map , flatMap , filter , concat , merge , zip | Operan sobre múltiples valores en secuencia |
Ejemplos Prácticos Paso a Paso
Creando y Transformando Mono
Veamos cómo crear y manipular Mono en situaciones reales. Primero, exploraremos las diferentes formas de crear un Mono y luego aplicaremos transformaciones comunes.
@Service
public class ProductService {
// Creación de Mono desde diferentes fuentes
public Mono<Product> createProduct(ProductDTO dto) {
// Crear Mono desde un valor
return Mono.just(dto)
// Validar datos
.filter(d -> d.getPrice() > 0)
// Transformar DTO a entidad
.map(this::convertToEntity)
// Manejar caso de validación fallida
.switchIfEmpty(Mono.error(new InvalidProductException()))
// Guardar en base de datos
.flatMap(productRepository::save)
// Log del resultado
.doOnSuccess(p -> log.info("Producto creado: {}", p.getId()))
// Manejar errores
.onErrorResume(e -> {
log.error("Error creando producto", e);
return Mono.empty();
});
}
// Encadenamiento de operaciones con Mono
public Mono<ProductResponse> getProductWithDetails(String productId) {
return productRepository.findById(productId)
// Enriquecer con información adicional
.zipWith(inventoryService.getStock(productId))
.map(tuple -> {
Product product = tuple.getT1();
Integer stock = tuple.getT2();
return new ProductResponse(product, stock);
})
// Cache del resultado
.cache(Duration.ofMinutes(5));
}
}
Trabajando con Flux y Operaciones de Stream
Flux ofrece operaciones similares a Java Stream API pero de forma reactiva. Veamos ejemplos prácticos de transformación, filtrado y agregación.
@Component
public class DataProcessor {
// Procesamiento de datos con Flux
public Flux<ProcessedData> processDataStream(Flux<RawData> rawDataFlux) {
return rawDataFlux
// Filtrar datos válidos
.filter(data -> data.isValid())
// Transformar en paralelo
.parallel(4)
.runOn(Schedulers.parallel())
.map(this::heavyProcessing)
// Volver a secuencial
.sequential()
// Agrupar en batches
.buffer(10)
// Procesar cada batch
.flatMap(batch -> processBatch(batch))
// Limitar rate de emisión
.delayElements(Duration.ofMillis(100))
// Reintentar en caso de error
.retry(3)
// Timeout global</em>
.timeout(Duration.ofMinutes(5));
}
// Agregación y reducción con Flux
public Mono<Statistics> calculateStatistics(Flux<Integer> numbers) {
return numbers
.collectList()
.map(list -> {
double average = list.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
int max = list.stream()
.mapToInt(Integer::intValue)
.max()
.orElse(0);
return new Statistics(average, max, list.size());
});
}
}
Combinando Mono y Flux
En aplicaciones reales, frecuentemente necesitas combinar Mono y Flux. Por ejemplo, podrías necesitar enriquecer cada elemento de un Flux con datos de un Mono.
@Service
public class OrderService {
// Combinar Flux de órdenes con Mono de usuario
public Flux<OrderWithUser> getOrdersWithUserInfo(String userId) {
Mono<User> userMono = userService.findById(userId)
.cache(); // Cache para no repetir la llamada
return orderRepository.findByUserId(userId)
.flatMap(order ->
userMono.map(user -> new OrderWithUser(order, user))
);
}
// Convertir Flux a Mono y viceversa
public Mono<List<Product>> getTopProducts(int limit) {
return productRepository.findAll()
.sort((p1, p2) -> p2.getRating().compareTo(p1.getRating()))
.take(limit)
.collectList(); // Flux a Mono<List>
}
public Flux<Character> splitString(Mono<String> stringMono) {
return stringMono
.flatMapMany(s -> Flux.fromArray(s.split("")))
.map(s -> s.charAt(0));// Mono a Flux
}
}
Mostrar imagen Alt: Captura de pantalla de IDE mostrando operadores de transformación para Mono y Flux en Spring Boot
Casos de Uso Comunes en Entornos Reales
API REST Reactivas
Las APIs REST reactivas son uno de los casos de uso más comunes. Con Mono y Flux, puedes construir endpoints que manejan eficientemente tanto respuestas únicas como streams de datos.
@RestController
@RequestMapping("/api/v1/reports")
public class ReportController {
// Endpoint que retorna un único reporte
@GetMapping("/{id}")
public Mono<ResponseEntity<Report>> getReport(@PathVariable String id) {
return reportService.findById(id)
.map(report -> ResponseEntity.ok(report))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
// Server-Sent Events con Flux
@GetMapping(value = "/live", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<ReportUpdate>> getLiveUpdates() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> ServerSentEvent.<ReportUpdate>builder()
.id(String.valueOf(sequence))
.event("report-update")
.data(new ReportUpdate(Instant.now(), getLatestMetrics()))
.build())
.doOnCancel(() -> log.info("Cliente desconectado del stream"));
}
}
Integración con Bases de Datos Reactivas
R2DBC permite trabajar con bases de datos relacionales de forma reactiva. Mono y Flux se integran perfectamente con repositorios reactivos.
@Repository
public interface CustomerRepository extends ReactiveCrudRepository<Customer, Long> {
// Query personalizada retornando Flux
@Query("SELECT * FROM customers WHERE age > :age")
Flux<Customer> findByAgeGreaterThan(int age);
// Query retornando Mono
@Query("SELECT COUNT(*) FROM customers WHERE city = :city")
Mono<Long> countByCity(String city);
}
@Service
@Transactional
public class CustomerService {
public Mono<Customer> createCustomerWithValidation(CustomerDTO dto) {
return validateEmail(dto.getEmail())
.then(checkDuplicates(dto.getEmail()))
.then(Mono.just(dto))
.map(this::convertToEntity)
.flatMap(customerRepository::save)
.doOnError(e -> log.error("Error creando cliente", e));
}
private Mono<Boolean> validateEmail(String email) {
return Mono.fromCallable(() -> EmailValidator.validate(email))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(valid -> valid
? Mono.just(true)
: Mono.error(new InvalidEmailException()));
}
}
WebClient y Llamadas HTTP Reactivas
WebClient es el cliente HTTP reactivo de Spring WebFlux. Naturalmente retorna Mono y Flux, facilitando la composición de llamadas a servicios externos.
@Service
public class ExternalApiService {
private final WebClient webClient;
public ExternalApiService(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
}
// Llamada que retorna un único resultado
public Mono<Weather> getCurrentWeather(String city) {
return webClient.get()
.uri("/weather/{city}", city)
.retrieve()
.onStatus(HttpStatus::is4xxClientError,
response -> Mono.error(new WeatherNotFoundException()))
.bodyToMono(Weather.class)
.timeout(Duration.ofSeconds(5))
.retry(2);
}
// Llamada que retorna múltiples resultados
public Flux<Transaction> getUserTransactions(String userId) {
return webClient.get()
.uri("/users/{id}/transactions", userId)
.retrieve()
.bodyToFlux(Transaction.class)
.filter(t -> t.getAmount() > 0)
.take(100);
}
}
Errores Comunes y Cómo Evitarlos
Error 1: Bloquear en Contexto Reactivo
Uno de los errores más graves es bloquear threads en una aplicación reactiva. Esto anula completamente los beneficios del modelo no bloqueante.
// ❌ INCORRECTO - Bloquea el thread
@GetMapping("/bad-example")
public Mono<User> badExample() {
User user = userService.findById("123").block(); // NUNCA hagas esto
return Mono.just(user);
}
// ✅ CORRECTO - Mantiene el flujo reactivo
@GetMapping("/good-example")
public Mono<User> goodExample() {
return userService.findById("123")
.map(user -> enrichUserData(user));
}
Error 2: No Suscribirse o Suscribirse Múltiples Veces
Los publishers reactivos son lazy. Si no te suscribes, nada sucede. Por otro lado, suscribirse múltiples veces puede causar ejecuciones duplicadas.
// ❌ INCORRECTO - No se suscribe
public void processData() {
dataService.getData()
.map(data -> processData(data)); // Nada sucede sin suscripción
}
// ✅ CORRECTO - Suscripción apropiada
public void processDataCorrectly() {
dataService.getData()
.map(data -> processData(data))
.subscribe(
result -> log.info("Procesado: {}", result),
error -> log.error("Error: ", error)
);
}
// Para evitar múltiples suscripciones, usa cache()
Mono<ExpensiveData> cachedData = expensiveService.getData()
.cache(Duration.ofMinutes(10));
Error 3: Manejo Incorrecto de Errores
El manejo de errores en programación reactiva requiere operadores específicos. No usar try-catch tradicional es fundamental.
// ❌ INCORRECTO - Try-catch no funciona con publishers
public Mono<Result> incorrectErrorHandling() {
try {
return service.riskyOperation();
} catch (Exception e) {
return Mono.error(e);// Este catch nunca se ejecutará
}
}
// ✅ CORRECTO - Usar operadores reactivos
public Mono<Result> correctErrorHandling() {
return service.riskyOperation()
.onErrorResume(SpecificException.class, e -> {
log.error("Error específico manejado", e);
return Mono.just(Result.defaultValue());
})
.onErrorMap(e -> new CustomException("Error procesando", e))
.doOnError(e -> log.error("Error general", e));
}
Mostrar imagen Alt: Infografía de errores comunes al usar Mono y Flux en Spring WebFlux con sus soluciones correctas
Error 4: Ignorar Backpressure
Backpressure es crucial cuando el productor emite datos más rápido de lo que el consumidor puede procesar. Ignorarlo puede causar OutOfMemoryError.
// ❌ INCORRECTO - Sin control de backpressure
public Flux<Data> withoutBackpressure() {
return Flux.interval(Duration.ofMillis(1))
.map(i -> generateHeavyData(i)); // Puede causar OOM
}
// ✅ CORRECTO - Con estrategias de backpressure
public Flux<Data> withBackpressure() {
return Flux.interval(Duration.ofMillis(1))
.onBackpressureDrop() // Descarta elementos si hay presión
.map(i -> generateHeavyData(i))
.limitRate(100); // Limita la tasa de requests</em>
}
Buenas Prácticas y Consejos Avanzados
Optimización de Performance
Para maximizar el rendimiento con Mono y Flux, sigue estas prácticas avanzadas. Primero, usa publishOn
y subscribeOn
estratégicamente para controlar en qué scheduler se ejecutan las operaciones.
@Service
public class OptimizedService {
// Usar schedulers apropiados</em>
public Flux<ProcessedItem> optimizedProcessing(Flux<Item> items) {
return items
// IO intensivo en scheduler elástico
.publishOn(Schedulers.boundedElastic())
.flatMap(item -> fetchFromDatabase(item))
// CPU intensivo en scheduler paralelo
.publishOn(Schedulers.parallel())
.map(data -> computeIntensiveOperation(data))
// Volver al scheduler principal
.publishOn(Schedulers.immediate());
}
// Compartir recursos costosos
private final Mono<Configuration> sharedConfig =
loadConfiguration()
.cache(Duration.ofHours(1))
.share(); // Comparte la suscripción entre múltiples suscriptores
}
Testing de Código Reactivo
StepVerifier es la herramienta esencial para testing de Mono y Flux. Permite verificar elementos emitidos, errores y completitud de forma determinística.
@Test
public void testMonoTransformation() {
Mono<String> mono = Mono.just("Hello")
.map(String::toUpperCase)
.delayElement(Duration.ofSeconds(1));
StepVerifier.create(mono)
.expectNext("HELLO")
.expectComplete()
.verify(Duration.ofSeconds(2));
}
@Test
public void testFluxWithVirtualTime() {
StepVerifier.withVirtualTime(() ->
Flux.interval(Duration.ofHours(1))
.take(3)
)
.expectSubscription()
.thenAwait(Duration.ofHours(3))
.expectNext(0L, 1L, 2L)
.expectComplete()
.verify();
}
Composición Avanzada de Publishers
La composición de publishers es donde realmente brilla la programación reactiva. Aprende a combinar múltiples fuentes de datos eficientemente.
@Component
public class AdvancedComposition {
// Combinación compleja con zip, merge y concat
public Mono<Dashboard> buildDashboard(String userId) {
Mono<UserStats> stats = statsService.getUserStats(userId);
Mono<List<Notification>> notifications =
notificationService.getRecent(userId).collectList();
Flux<Activity> activities = activityService.getLatest(userId);
return Mono.zip(stats, notifications)
.map(tuple -> Dashboard.builder()
.stats(tuple.getT1())
.notifications(tuple.getT2())
.build())
.zipWith(activities.collectList())
.map(tuple -> {
Dashboard dashboard = tuple.getT1();
dashboard.setActivities(tuple.getT2());
return dashboard;
});
}
// Uso de switchMap para cancelación automática
public Flux<SearchResult> searchWithAutoComplete(Flux<String> searchTerms) {
return searchTerms
.debounce(Duration.ofMillis(300))
.distinctUntilChanged()
.switchMap(term -> searchService.search(term)
.take(10)
.onErrorResume(e -> Flux.empty())
);
}
}
Métricas y Monitoring
Monitorear aplicaciones reactivas requiere atención especial. Usa las herramientas de Micrometer integradas en Spring Boot.
@Configuration
public class ReactiveMetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
@Component
public class MetricsAwareService {
private final MeterRegistry registry;
public Mono<Result> monitoredOperation() {
return operation()
.name("custom.operation")
.tag("type", "important")
.metrics() // Activa métricas automáticas
.doOnNext(result ->
registry.counter("operation.success").increment())
.doOnError(error ->
registry.counter("operation.error").increment());
}
}
}
Conclusión
Dominar Mono y Flux en Spring Boot es esencial para construir aplicaciones reactivas eficientes y escalables. A lo largo de este artículo, hemos explorado desde los conceptos fundamentales hasta técnicas avanzadas de optimización.
Recuerda que Mono es ideal para operaciones que retornan un único valor, mientras que Flux brilla cuando trabajas con streams de datos. La clave del éxito está en mantener el flujo reactivo, evitar bloqueos y manejar correctamente los errores con operadores reactivos.
Ahora es tu turno de aplicar estos conocimientos. Comienza migrando endpoints simples a WebFlux, experimenta con los operadores de transformación y observa cómo mejora el rendimiento de tu aplicación. La programación reactiva con Spring Boot no es solo una tendencia, es el futuro del desarrollo de aplicaciones Java de alto rendimiento.
¿Quieres seguir aprendiendo sobre Spring WebFlux?
Únete a la discusión con nuestra comunidad de desarrolladores Spring Boot.