Buenas prácticas funcionales en Java con Vavr: Option, Match y Try

Este artículo habla sobre Vavr, una librería de Java que facilita de forma práctica y elegante la programación funcional.

La librería nos aporta colecciones inmutables, tipos funcionales, pattern matching y tuplas entre otras utilidades. Nos aleja de ese código tan verboso que Java estándar, al que llamaremos clásico en este artículo, nos obliga de forma nativa.

Cómo usar Vavr

En Maven:

Este artículo habla sobre Vavr, una librería de Java que facilita de forma práctica y elegante la programación funcional. La librería nos aporta colecciones inmutables, tipos funcionales, pattern matching y tuplas entre otras utilidades. Nos aleja de ese código tan verboso que Java estándar, al que llamaremos clásico en este artículo, nos obliga de forma nativa. Cómo usar Vavr En Maven:

En Gradle:

En Gradle

No necesitas configurar nada más, simplemente importamos las clases y empiezas a usarlas en paralelo a las clases estándar de Java

Contexto inicial

Hablemos primero del porqué de todo esto.

En la programación más clásica existen una serie de inconvenientes cuando intentamos adaptarnos a tecnologías más modernas o a la programación funcional.

Efectos colaterales: 

En Java hay un clásico que es pasar un objeto a un método, modificarlo dentro y que, al volver, ese objeto original también esté modificado. 

¿Y cuál es el problema de esto? 

El problema es que las estructuras mutables necesitan sincronización si son manejadas por varios hilos, además para conseguir una programación “thread-safe” lo ideal es trabajar con estructuras inmutables. 

Dificultad de razonabilidad del código:

Trabajar con estructuras mutables implica que un objeto pueda ser modificado desde algo más que sus parámetros de entrada. 

Esto complica muchísimo entender, depurar y testear el código porque los  resultados no solo dependen de los parámetros de entrada.

También necesitamos conocer todo un contexto (a veces invisible), que obliga no solo a conocer el método o clase que estamos trabajando.

Necesidad de favorecer un enfoque declarativo:

Cuando trabajamos con mutabilidad, pensamos en cómo modificar una estructura. En un enfoque funcional, en cambio, nos enfocamos en qué aplicación aplicar. Esto da lugar a un código más expresivo.

Ejemplo mutable:

No necesitas configurar nada más, simplemente importamos las clases y empiezas a usarlas en paralelo a las clases estándar de Java Contexto inicial Hablemos primero del porqué de todo esto. En la programación más clásica existen una serie de inconvenientes cuando intentamos adaptarnos a tecnologías más modernas o a la programación funcional. Efectos colaterales:  En Java hay un clásico que es pasar un objeto a un método, modificarlo dentro y que, al volver, ese objeto original también esté modificado.  ¿Y cuál es el problema de esto?  El problema es que las estructuras mutables necesitan sincronización si son manejadas por varios hilos, además para conseguir una programación “thread-safe” lo ideal es trabajar con estructuras inmutables.  Dificultad de razonabilidad del código: Trabajar con estructuras mutables implica que un objeto pueda ser modificado desde algo más que sus parámetros de entrada.  Esto complica muchísimo entender, depurar y testear el código porque los  resultados no solo dependen de los parámetros de entrada. También necesitamos conocer todo un contexto (a veces invisible), que obliga no solo a conocer el método o clase que estamos trabajando. Necesidad de favorecer un enfoque declarativo: Cuando trabajamos con mutabilidad, pensamos en cómo modificar una estructura. En un enfoque funcional, en cambio, nos enfocamos en qué aplicación aplicar. Esto da lugar a un código más expresivo. Ejemplo mutable:

Ejemplo inmutable: 

Ejemplo inmutable: 

Ahora que tenemos el conocimiento sobre el porqué es recomendable utilizar estructuras inmutables para programación funcional, vamos a la pregunta que os estaréis haciendo.

«¿Y para qué Vavr, si Java ya tiene List.of() o Collectors.toList()?»

Bueno, lo principal es que las listas inmutables de java son muy limitadas. No ofrecen operaciones funcionales integradas (map, flatMap, filter…).

Para usar esto debemos aplicarle un stream posteriormente, lo que significa que estamos agregando pasos extras.

En cambio las listas de Vavr combinan inmutabilidad con operaciones funcionales directamente en la colección.

Esto provoca justo lo que mencionamos antes, nos obliga a usar un lenguaje mucho más verboso y menos declarativo, encadenando stream()… collect() cada vez que queremos hacer algo. 

Con Vavr usas directamente el map, filter, fold y otras operaciones sin necesidad de conversiones.

Otro punto importante: es que los streams son de un solo uso, mientras que las colecciones Vavr pueden ser reutilizables.

Además también la propia librería nos ofrece métodos más avanzados para evitar boilerplate (exists, forAll, distincBy, zip).

Ejemplo con Zip en Java Clásic:

Ejemplo con Zip en Java Clásic:

Ejemplo con Zip en Vavr:

Ejemplo con Zip en Vavr

Esto nos prepara muy bien por si en algún momento se necesita adaptar el código a Webflux, ya que allí el uso de zip es habitual para combinar resultados de distintos flujos.

Además de List, Vavr ofrece Map y Set inmutables con las mismas ventajas y un conjunto de operaciones funcionales que no encontramos en las colecciones estándar de Java.

Option un super optional.

Otro de los grandes aportes de Vavr es Option, que nos ayuda a olvidarnos de los NullPointerException.

En Java clásico siempre nos exponemos a NullPointerException. A partir de Java 8 contamos con Optional pero sigue siendo limitado:

No se integra de manera natural en colecciones ni operaciones funcionales.

Su API es reducida y se queda corta.

Ejemplo con Optional de Java Clásico:

Ejemplo con Optional de Java Clásico

Ejemplo con Option de Vavr:

Ejemplo con Option de Vavr

Más allá de un simple Optional

La diferencia más importante es que Optional de Java vive aislado:

No se integra fácilmente con Stream, CompletableFuture ni con otras estructuras. Su uso se limita a devolver valores de métodos donde puede no haber resultado.

En cambio, Option en Vavr forma parte de una filosofía funcional coherente: Se integra con List, Set y otras colecciones inmutables.

Puede transformarse fácilmente en Either o Try para manejar errores o ramificaciones de lógica.

Funciona bien con Future, lo que facilita la composición en programación reactiva o asíncrona.

Ejemplo transformando un Option en un Try

Uso del Optional de java tratando dos excepciones:

Uso del Optional de java tratando dos excepciones

Uso del Option de Vavr tratando el mismo escenario:

Uso del Option de Vavr tratando el mismo escenario

El mensaje “No value” solo aparecerá si Option no contiene valor. En caso de que el resultado exista pero fuese un NumberFormatException se encapsulará dentro de un Failure que veremos más adelante en este artículo.

Match: el switch en el siguiente nivel.

En Java tradicional contamos con switch, el cual además de ser muy limitado previo a Java 17 también era considerada poco elegante en términos de clean code. A partir de Java 17 se mejoró con pattern matching, pero aún solo soporta int, String y enum.

Ejemplo switch básico en Java clásico:

Ejemplo switch básico en Java clásico:

Ejemplo switch básico en Java 17+:

Ejemplo switch básico en Java 17+:

Ejemplo switch básico en Vavr:

Ejemplo switch básico en Vavr:

Ahora que tenemos unas comparativas simples, vamos a ver casos donde destaque la potencia de Vavr:

Match con Tuplas

Imaginemos que trabajamos con tuplas, una sencilla, (status, code) y queremos traducirla.

Match con Tuplas:

Esto nos ahorra los clásicos if(status.equals(“OK”) && code == 200)

Match con condicionales (Predicados)

En este caso estamosmatcheando”un valor primitivo:

En este caso estamos “matcheando”un valor primitivo:

Y con otro caso con funciones (vamos a categorizar cadenas)

Aquí estamos “matcheando” un objeto más complejo “String”

Aquí estamos “matcheando” un objeto más complejo “String”

Try: el Option de las excepciones

En java clásico: las checked exceptions ensucian el código con try/catch por todas partes.

Con streams o lambdas es aún más complicado meter fácilmente un método que lance por ejemplo una IOException.

Con Vavr.Try encapsulamos las operaciones que pueden fallar dentro de una estructura funcional:

  • Success(value) ? si la operación fue correcta.
  • Failure(exception) ? si algo lanzó una excepción.

Al igual que el Option encapsula null, el Try encapsula las  excepciones.

Ejemplo en Java Clásico (IOException en un stream)

Ejemplo en Java Clásico (IOException en un stream)

Ejemplo con Vavr.Try

Ejemplo con Vavr.Try

Es una herramienta muy potente que tiene distintos mecanismos para manejar los errores, por ejemplo podemos poner un .recover() Para obtener un valor por defecto en caso de error.

por ejemplo podemos poner un .recover() Para obtener un valor por defecto en caso de error

Un ejemplo un poco más práctico que lanzar una lista de archivos y recopilar los errores y éxitos:

Un ejemplo un poco más práctico que lanzar una lista de archivos y recopilar los errores y éxitos:

Aquí Try nos permite mantener el flujo vivo: ningún error rompe la ejecución, los fallos se encapsulan y podemos posponer su trato con:

  • Descartarlos con filter
  • Darles un valor con recover
  • Registrarlos en un sistema de logs
  • Hacer un throw detallado

Aquí Try nos permite mantener el flujo vivo:

Limitaciones y puntos a considerar

Hemos visto muchas bondades de Vavr, pero también debemos llevar esta información a la vida real de un desarrollador. No todo son ventajas, y es importante valorar los siguientes puntos:

¿Siempre necesitamos programación funcional?

La programación funcional es un paradigma relativamente moderno, pero no siempre resulta necesaria. En proyectos legacy con mucho código nos va forzar inconsistencias  y dificultades de mantenimiento, la  mutabilidad es suficiente en contexto sencillos, si tenemos claro que el proyecto no va a ser concurrente (varios hilos) ni complejo.

Curva de aprendizaje

Implementar esta librería implica aprender conceptos como inmutabilidad, Option, Try, Match y un manejo relativamente avanzado de lambdas. Si el equipo no está acostumbrado a esto puede frenar el desarrollo considerablemente.

Un ejemplo sobre esto común es que estamos acostumbrados a editar objetos mutables, no transformar estructuras inmutables y trabajar con el nuevo resultado. Al intentar agregar o eliminar un elemento vamos a forzar una UnsupportedOperationException.

Al intentar agregar o eliminar un elemento vamos a forzar una UnsupportedOperationException.

Dependencia externa

Vavr no forma parte de las librerías oficiales de Java. Igual que ocurre con MapStruct o Lombok, esto implica añadir una dependencia externa y asumir riesgos de mantenimiento a largo plazo aunque sea estable.

Integración con Java clásico

En proyectos ya existentes, las clases de Vavr conviven con las de Java estándar. Esto obliga a conversiones frecuentes  (Option – Optional, List – java.util.List…) usando utilidades como toJavaList.

Este “doble lenguaje” puede añadir mucho ruido y confusión si no se trata con cuidado. 

Pongamos un ejemplo de cómo se trataría este ruido en un proyecto con arquitectura hexagonal (lo cual no siempre es posible de tener)

En nuestra capa de dominio vamos a trabajar con la librería vavr,   porque nos beneficia adoptar las funcionalidades mencionadas anteriormente, así que definimos el puerto:

En nuestra capa de dominio vamos a trabajar con la librería vavr,   porque nos beneficia adoptar las funcionalidades mencionadas anteriormente, así que definimos el puerto:

Pero nuestra conexión con base de datos se realiza con JPA, jpa nos va a devolver las listas de java.util.List:

 Pero nuestra conexión con base de datos se realiza con JPA, jpa nos va a devolver las listas de java.util.List:

Pues para tenemos todo limpio y ordenado lo mejor es no dejar que  salga del adaptador de la  capa de infraestructura:

 Pues para tenemos todo limpio y ordenado lo mejor es no dejar que  salga del adaptador de la  capa de infraestructura:

Este enfoque con hexagonal permite encapsular la fricción y mantener el dominio limpio, pero no siempre es viable. 

En proyectos legacy o con arquitecturas menos moduladas, esta conversión puede propagarse por varias capas y añadir ruido por lo que no siempre es trivial.

Vavr en perspectiva

Aunque muchas de estas utilidades parecen nuevas, la realidad es que no lo son.

En otros lenguajes como Scala,  Kotlin, F# o Haskell, trabajan con estructuras inmutables y pattern matching desde hace años de manera nativa.

Incluso en .NET encontramos conceptos similares como Option/Maybe, colecciones inmutables, LINQ para operaciones declarativas.

Lo que en realidad hace Vavr  no es reinventar la rueda, es acercar a Java, que naturalmente es  imperativo y orientado a objetos a ese “estándar funcional” que muchos ecosistemas modernos ya han llegado.

Resumen

En este artículo hemos hablado sobre:

  • Por qué la programación funcional en Java clásico es limitada
  • Cómo Vavr resuelve estas limitaciones
  • Option como super Optional
  • Match como evolución del switch
  • Try como el Option de las excepciones
  • Limitaciones y  puntos a considerar
  • Vavr en perspectiva

En definitiva, Vavr es una gran utilidad si buscamos mejorar un enfoque funcional en Java, pero requiere medir bien en qué proyectos aplicarla y cómo integrar sus tipos con el ecosistema estándar.

<span style="font-size:80%">Autor </span><a href="https://blog.kairosds.com/author/adrianfernandezcardenal/" target="_self">Adrián Fernández Cardenal</a>

Autor Adrián Fernández Cardenal

Sep 30, 2025

Otros artículos

Test de mutación

Test de mutación

Introducción Como desarrolladores de software siempre queremos entregar la máxima calidad que podamos dar. Creamos tests, medimos la cobertura y confiamos en que son fiables… hasta que aparece un bug en producción justo en una zona que se supone que está cubierta....