Spring Boot con Kotlin frente a Java: Comparativa práctica y guía de integración empresarial

Análisis práctico del desarrollo con Spring Boot usando Kotlin frente a Java, comparando sintaxis, productividad e integración con herramientas como OpenAPI, AsyncAPI y Avro en un entorno empresarial.
Logos de Spring Boot, Kotlin y Java conectados por flechas sobre fondo oscuro

Indice

  1. Introducción

  2. Por que Spring Boot + Kotlin

  3. Playground Project: Customer/Kustomer API con JPA

  4. Proyecto Playground: Modelo y funcionalidad

  5. Comparación Java – Kotlin

  6. Conclusiones

  7. Referencias bibliográficas

1. Introducción

En los últimos años, Kotlin ha ganado terreno como una alternativa moderna y potente a Java para el desarrollo de aplicaciones backend con Spring Boot. Aunque Java sigue siendo un lenguaje sólido y en constante evolución, Kotlin aporta una sintaxis más concisa, expresiva y segura, lo que se traduce en mayor productividad y menos código repetitivo. Aun así, en el contexto de proyectos empresariales reales, donde intervienen múltiples herramientas y dependencias generadas —como OpenAPI, AsyncAPI, Avro o MapStruct—, la decisión de adoptar Kotlin debe ir más allá de la estética del lenguaje.

Este artículo plantea un análisis práctico y detallado basado en un proyecto real desarrollado en paralelo con Java y Kotlin. El objetivo es comparar no solo las diferencias de lenguaje, sino también cómo se integran ambos enfoques con el ecosistema de Spring Boot y herramientas comunes del ciclo de vida del software empresarial. A lo largo del artículo veremos en qué escenarios Kotlin marca realmente la diferencia, qué complejidades añade, y cuándo puede ser una alternativa sólida para proyectos backend modernos.

2. Por que Spring-Boot + Kotlin

Aunque las versiones más recientes de Java han incorporado mejoras significativas en elegancia, expresividad y concisión (como records, pattern matching, sealed classes, text blocks y expresiones switch mejoradas), Kotlin destaca por ofrecer una sintaxis aún más limpia, concisa y expresiva. Esto reduce enormemente el código repetitivo (boilerplate code), a la vez que mantiene una interoperabilidad total con Java, lo que lo convierte en una opción muy interesante para desarrollar aplicaciones modernas con Spring Boot.

Además, Kotlin incorpora características avanzadas que mejoran la productividad y la seguridad del código:

  • Data classes: Simplifican la creación de clases para modelar datos, reduciendo drásticamente el código repetitivo al generar automáticamente getters/setters y métodos como toString(), equals() y hashCode(), eliminando así la necesidad de librerías como Lombok.
  • Null safety: Los tipos son no nulos por defecto, lo que previene errores de tipo NullPointerException en tiempo de compilación y fomenta un código más robusto y seguro.
  • Extension functions: Permiten extender la funcionalidad de clases existentes sin modificar su código fuente ni recurrir a herencia, mejorando la legibilidad y la modularidad.
  • Interpolación de Strings: Similar a las text blocks de Java, pero con la posibilidad de interpolar (sustituir) variables de manera sencilla y natural.
  • Coroutines: Facilitan la programación asíncrona al permitir escribir código secuencial y claro, eliminando la complejidad de los callbacks o las cadenas de promesas.

Otros puntos fuertes de Kotlin frente a Java incluyen su soporte para programación funcional y APIs fluidas:

  • API de Collections/Streams: Más concisa y expresiva que la API de Collections de Java, con soporte nativo para operaciones funcionales como map, filter y reduce.
  • Funciones de orden superior: Facilitan la modularidad al permitir pasar funciones como argumentos, promoviendo un estilo de código más declarativo y reutilizable.
  • Funciones de alcance (Scoping Functions): Como let, apply, with, run y also, que, junto con operadores de null safety como ?. (navegación segura), ?: (Elvis) y !! (aserción no nula), permiten encadenar llamadas, crear ámbitos, evitar variables temporales y simplificar el código, haciéndolo más fluido y expresivo.

Todo esto sin renunciar a la interoperabilidad con Java: cualquier código o librería Java puede ser invocado desde Kotlin y viceversa, de forma totalmente transparente.

En resumen, Kotlin ofrece una sintaxis más limpia, concisa y expresiva, mejorando la productividad y la seguridad del código, y manteniendo una compatibilidad completa con Java. Esto lo convierte en una opción muy válida para desarrollar aplicaciones modernas con Spring Boot.

Ahora bien, cualquier proyecto empresarial con Spring Boot también requiere integrarse con herramientas existentes, como generadores de código API-First (OpenAPI, AsyncAPI, Avro), procesadores de anotaciones (MapStruct, Lombok), herramientas de build como Maven o Gradle, y otras integraciones que pueden suponer un desafío si no se tienen en cuenta desde el principio.

Por eso, en este artículo hemos decidido crear un proyecto sencillo pero no trivial tanto en Java como en Kotlin, con el objetivo de comparar ambas soluciones de forma realista.

Además, el proyecto Kotlin se ha configurado para poder ser construido indistintamente con Gradle o con Maven, lo que permite una comparación completa entre ambas alternativas.

3. Playground Project: Customer/Kustomer API con JPA

Ambos proyectos playground implementan la misma funcionalidad en Java y Kotlin, utilizando los mismos principios y herramientas, con el objetivo de poder compararlos directamente.

3.1. Stack Tecnologico

Hemos elegido un stack tecnológico simple pero completo, que puede servir como base sólida para cualquier microservicio empresarial moderno:

  • Spring Boot 3.5.x
  • Hibernate
  • Spring Data JPA
  • MapStruct / Lombok
  • OpenAPI Generator
  • AsyncAPI Generator (ZenWave SDK)
  • Avro + Avro Compiler
  • Spring Cloud Streams
  • Spring Security
  • TestContainers con Docker Compose

3.2. API-First con OpenAPI Generator

OpenAPI Generator se utiliza para generar las interfaces anotadas de los controladores de Spring MVC y sus DTOs a partir de una definición OpenAPI, en un fichero openapi.yml.

Podemos configurar el plugin para que genere código en Kotlin (usando el generador kotlin-spring) o en Java (generador spring). Ambas implementaciones son equivalentes en tiempo de ejecución, con solo pequeñas diferencias en tiempo de compilación: esencialmente tipos con null safety y ResponseEntity<Unit> en lugar de ResponseEntity<Void> en el caso de Kotlin.

Sin embargo, el hecho de generar código en Kotlin o Java tiene otras implicaciones, ya que en el caso de Java, este debe ser compilado para estar disponible desde el código Kotlin. Y es muy probable que los DTOs generados por OpenAPI sean referenciados desde las anotaciones de MapStruct en el código Kotlin.

Esto tiene implicaciones, como veremos más adelante, en la forma en que se configuran los distintos plugins de build y compilación.

Hemos configurado OpenAPI Generator con Maven para ambos proyectos, y adicionalmente con Gradle en el proyecto Kotlin:

3.3. Procesador de anotaciones y MapStruct

MapStruct es un procesador de anotaciones para Java que genera automáticamente código para mapear objetos (como DTOs a entidades) usando anotaciones como @Mapper y @Mapping. Durante el proceso de compilación, genera el código Java que implementa dichos mappers, por lo que es eficiente y ligero en tiempo de ejecución.

Aunque podría parecer una elección sencilla, presenta retos importantes para funcionar correctamente en proyectos Kotlin, ya que:

  • Lee anotaciones en clases Kotlin,
  • que referencian clases Java generadas por OpenAPI, AsyncAPI o Avro,
  • y genera código Java
  • que tiene que estar disponible para el resto del código Kotlin.

Además, es necesario configurar el procesador de anotaciones para Kotlin (Kapt), en lugar del procesador de anotaciones estándar de Java.

La configuración del compilador de Kotlin y el procesador de anotaciones Kapt es probablemente la parte más crítica para garantizar la interoperabilidad entre el código Java y Kotlin, tanto escrito a mano como autogenerado por herramientas como OpenAPI Generator, AsyncAPI/ZenWave SDK, Avro Compiler o MapStruct.

3.4. API-First con AsyncAPI (ZenWave SDK)

ZenWave SDK es una herramienta que permite generar, a partir de una definición AsyncAPI en un fichero asyncapi.yml, interfaces Java y sus DTOs para producir y consumir eventos, así como una implementación completa usando Spring Cloud Stream.

Funciona de manera similar a OpenAPI Generator, pero orientada a eventos con AsyncAPI, generando un wrapper muy ligero alrededor de Spring Cloud Stream.

Dado que genera código Java, también es necesario que este se genere y compile antes del procesamiento de anotaciones con MapStruct.

Hemos configurado ZenWave SDK Generator con Maven para ambos proyectos, y adicionalmente con Gradle en el proyecto Kotlin:

Para el caso de Gradle hemos utilizado el plugin de JBang, que permite ejecutar cualquier CLI o método main() de cualquier librería publicada en Maven Central.

3.5. Serialización de eventos con Avro

Avro es un formato de serialización de datos que permite definir esquemas y serializar/deserializar objetos en formato binario. El tamaño de los datos serializados es muy reducido y la velocidad de serialización/deserialización es alta en comparación con JSON, por lo que suele utilizarse para la transmisión eficiente de eventos en sistemas como Kafka.

Dado que genera código Java, también es necesario que este se genere y compile antes de que el procesador de anotaciones de MapStruct entre en acción.

Hemos configurado Avro Compiler con Maven para ambos proyectos, y adicionalmente con Gradle en el proyecto Kotlin:

NOTA: Hasta la versión 1.12.0 de Avro, el orden en que se definen los ficheros .avsc es muy importante para respetar las dependencias entre schemas. Por eso hemos decidido utilizar la versión 1.11.1 —muy extendida y estable— para ilustrar esta cuestión en el proyecto.

4. Proyecto Playground: Modelo y funcionalidad

El proyecto de ejemplo es una API REST para gestionar el CRUD del aggregate Customer, que contiene:

  • Una colección de objetos Address, que se almacenan en base de datos como una columna JSON.
  • Una colección de objetos PaymentMethod, gestionada como una relación @OneToMany con JPA/Hibernate.
  • Un enum llamado PaymentMethodType, con un conversor personalizado para persistirlo como un entero en la base de datos.
Text
@startuml
class Customer << aggregate >> {
Customer entity
--
String name
String email
Address[] addresses
--
@OneToMany paymentMethods
}

Customer "one" *--down- "many" Address
class Address  {
String street
String city
}

Customer "one" o--down- "many" PaymentMethod
class PaymentMethod  {
PaymentMethodType type
String cardNumber
--
@ManyToOne customer
}

PaymentMethod o-- PaymentMethodType
enum PaymentMethodType  {
VISA
MASTERCARD
}
@enduml

El servicio expone una API REST con los métodos típicos de un CRUD, así como un método de búsqueda paginada.

Además, cada acción de CRUD genera un evento, que se publica en un bus de eventos (Kafka) para su consumo por otros microservicios.

Text
@startuml

CustomerService -up-> Customer
class Customer << aggregate >> {
    Customer entity
    --
    String name
    String email
    Address[] addresses
    --
    @OneToMany paymentMethods
}
class CustomerService  << service >> {
    getCustomer(id): Customer?
    searchCustomers(CustomerSearchCriteria): Customer[]
    createCustomer(Customer): Customer withEvents CustomerEvent
    updateCustomer(id, Customer): Customer? withEvents CustomerEvent
    deleteCustomer(id):  withEvents CustomerEvent
}

CustomerService -left-> inputs
namespace inputs #DDDDDD {
    class CustomerSearchCriteria << inputs >> {
        String name
        String email
        String city
        String state
    }
}

CustomerService -down-> events
namespace events #DDDDDD {
    class CustomerEvent << event >> {
        Long id
        Integer version
        String name
        String email
        Address[] addresses
        PaymentMethod[] paymentMethods
    }
}

@enduml

Tanto la API REST como el evento CustomerEvent están definidos en ficheros OpenAPI y AsyncAPI, respectivamente, para poder generar el código automáticamente mediante OpenAPI Generator y ZenWave SDK.

4.1. Ejecución

Ambos proyectos pueden arrancarse desde el IDE ejecutando la clase Application con el perfil de Spring Boot local, o desde la línea de comandos utilizando Maven o Gradle:

Bash
mvn spring-boot:run -Dspring-boot.run.profiles=local
Bash
./gradlew bootRun --args='--spring.profiles.active=local'

Antes de ejecutar el código del proyecto, recuerda levantar las dependencias con Docker Compose:

Bash
docker-compose -f docker-compose.yml up -d

4.2. APIs públicas

Las APIs públicas definidas en OpenAPI, AsyncAPI y Avro son idénticas entre ambos proyectos, y pueden consultarse en los siguientes enlaces:

Para la API REST, puedes acceder a la interfaz Swagger en las siguientes URLs locales:

Las credenciales por defecto para acceder a las APIs son:
usuario: admin
contraseña: password

5. Comparación Java – Kotlin

A continuación vamos a comparar distintos aspectos clave entre las implementaciones en Java y Kotlin dentro del mismo contexto funcional y tecnológico.

5.1. Data Classes vs Lombok

Una diferencia notable aparece al definir el modelo de datos anotado con JPA.

5.2 Objetos de datos y getters/setters

El estándar en Java para trabajar con JPA/Hibernate requiere clases con métodos getters y setters. Como estos métodos rara vez contienen lógica, suelen aportar poco valor, y aunque puedan generarse automáticamente desde el IDE, añaden ruido al código. Por esta razón es habitual usar librerías como Lombok, que los genera en tiempo de compilación y elimina ese boilerplate en los ficheros fuente.

Aunque Lombok es muy popular, su funcionamiento se basa en hacks sobre el compilador de Java, lo cual genera cierta fragilidad. De hecho, suele haber un retraso de 1 a 3 semanas en dar soporte a nuevas versiones del JDK, dependiendo de los cambios internos que introduzcan.

5.3 Java Records: Solución deficiente al problema de los getters/setters

Los Java Records se introdujeron como intento de Java para evitar el boilerplate de getters y setters. Aunque eliminan la necesidad de definirlos explícitamente, su diseño obliga a utilizar un constructor con todos los campos obligatorios, lo que complica la creación de objetos complejos.

Este tipo de constructor con múltiples parámetros (especialmente si son del mismo tipo, como String) es una fuente potencial de errores, difícil de mantener, y alejado de las buenas prácticas de diseño. Además, modificar el orden, añadir o eliminar campos puede romper fácilmente el código existente.

En la práctica, he visto más errores por uso de constructores con muchos parámetros que por trabajar con objetos mutables.

Y lo más relevante: los Java Records no son compatibles con JPA, por lo que no pueden usarse como entidades persistentes.

5.4 Data Classes en Kotlin: La alternativa a Lombok y Java Records

Kotlin introduce el concepto de Data Classes, que proporcionan:

  • Getters y setters automáticos, con posibilidad de sobrescribirlos si se necesita código adicional.
  • Métodos toString(), equals() y hashCode() automáticos.
  • Sintaxis de acceso y modificación a las propiedades sin necesidad de getters (customer.name en lugar de customer.getName()) pero que internamete usa los getters/setters.
  • Un único constructor que puede ser invocado con un número variable de parámetros, gracias a la sintaxis de Kotlin que permite nombrar los parámetros en la invocación del constructor.
  • Si se necesita un constructor sin parámetros, se puede conseguir simplemente definiendo valores por defecto a las propiedades no nullables.

Con la sintaxis de Kotlin para objetos de datos no hay problema de confusión entre parámetros y los parámetros con nombre proporcionan la flexibilidad de un patrón Builder sin necesidad de código adicional ni librerías externas como Lombok.

5.4.1. Domain Entity con JPA y Lombok en Java

Ejemplo de una entidad de dominio Customer definida en Java utilizando JPA, Hibernate y Lombok. En este caso, se aprovechan las anotaciones de Lombok (@Getter, @Setter) para eliminar el código repetitivo de getters y setters, y se utilizan anotaciones de validación (@NotNull, @Size, @Email) junto con configuración avanzada de persistencia:

Clase Customer en Java con JPA y anotaciones Lombok
Definición de entidad en Java con Lombok, JPA y anotaciones Hibernate para relaciones y persistencia en JSON.

Ver en GitHub

Algunos aspectos destacados:

  • Uso de @Entity y @Table para mapear la entidad a la tabla customer.
  • Persistencia del campo addresses como una columna JSON con JdbcTypeCode(SqlTypes.JSON).
  • Relación @OneToMany con paymentMethods, gestionada con @JsonManagedReference y orphanRemoval.
  • Uso de @Cache, @Version y anotaciones de auditoría (@EntityListeners) para un entorno empresarial realista.
  • Lombok elimina la necesidad de escribir manualmente métodos de acceso, manteniendo el código limpio pero introduciendo una dependencia adicional del compilador.

5.4.2. Domain Entity con Data Classes y JPA en Kotlin

Ejemplo equivalente al modelo Java, pero utilizando Kotlin y data class:

Clase Customer en Kotlin usando data class y anotaciones JPA
Implementación de la entidad Customer en Kotlin, con sintaxis compacta y anotaciones JPA, sin necesidad de Lombok.

Ver en GitHub

Esta implementación aprovecha las ventajas del lenguaje Kotlin:

  • Uso de data class, que genera automáticamente métodos como equals(), hashCode() y toString().
  • Propiedades definidas con var y valores por defecto (= null o colecciones vacías) que permiten instanciación flexible.
  • Declaración compacta y clara de campos con anotaciones de validación y persistencia.
  • Persistencia JSON para la colección addresses usando @JdbcTypeCode(SqlTypes.JSON).
  • Relación @OneToMany con paymentMethods, incluyendo @JsonManagedReference y configuración de cascada.
  • La ausencia de Lombok elimina dependencia del compilador Java y mejora la interoperabilidad nativa en Kotlin.

En conjunto, esta versión ofrece una mayor expresividad y simplicidad con menos código y sin perder capacidades avanzadas de mapeo JPA.

5.4.3. Poblando objetos en Java

Una manera muy practica de evitar variables temporales al instanciar y poblar objeto es utilizar los setters encadenados de Lombok, activados con la propiedad lombok.accessors.chain=true, , o utilizar el patron Builder de Lombok. Esto da lugar a una sintaxis mas fluida:

Java
Customer customer = new Customer()
    .setName("Jane Smith")
    .setEmail("jane.smith@example.com")
    .setAddresses(List.of(new Address()
        .setStreet("456 Elm St")
        .setCity("Othertown")
    ))
    .addPaymentMethods(new PaymentMethod()
        .setType(PaymentMethodType.VISA)
        .setCardNumber("6543210987654321")
    );

5.4.4. Poblando objetos en Kotlin

En Kotlin, hay múltiples formas idiomáticas de poblar objetos tipo data class, adaptándose tanto a estilos imperativos como funcionales. Estas opciones permiten un código más expresivo, menos verboso y sin necesidad de librerías externas.

Opción 1: setters y función apply

Se pueden establecer propiedades directamente, o usar funciones de alcance como apply para agrupar inicializaciones de forma limpia:

Kotlin
val customer = Customer()
customer.name = "Jane Smith"
customer.email = "jane.smith@example.com"
customer.addresses = mutableListOf(Address().apply {
    street = "456 Elm St"
    city = "Othertown"
})

// OneToMany paymentMethods owner: true
val paymentMethod = PaymentMethod()
paymentMethod.type = PaymentMethodType.VISA
paymentMethod.cardNumber = "6543210987654321"
customer.addPaymentMethods(paymentMethod)
Opción 2: constructor con parámetros nombrados (estilo builder)

Otra alternativa aún más concisa consiste en utilizar el constructor de la data class con parámetros por nombre. Esta sintaxis actúa como un builder implícito, eliminando la necesidad de invocar apply o modificar propiedades una a una:

Kotlin
val customer = Customer(
    name = "Jane Smith",
    email = "jane.smith@example.com",
    addresses = mutableListOf(
        Address(street = "456 Elm St", city = "Othertown")
    )
).addPaymentMethods(PaymentMethod(
    type = PaymentMethodType.VISA,
    cardNumber = "6543210987654321"
))

Ambos enfoques son compatibles con JPA y permiten construir objetos complejos de forma clara, concisa y segura. La combinación de null safety, funciones de alcance y constructores nombrados convierte a Kotlin en una excelente opción para definir y manipular entidades de dominio.

5.4.5. Poblando Records en Java

Para comparar, veamos cómo se poblaría un objeto utilizando Java Records. Dado que los records son inmutables y no permiten parámetros por defecto, es obligatorio pasar todos los campos en el constructor, en el orden exacto en que fueron definidos:

Java
Customer customer = new Customer(
    null, // id
    null, // version
    "Jane Smith", // name
    "jane.smith@example.com", // email
    List.of(new Address("456 Elm St", "Othertown")), // addresses
    List.of() // paymentMethods
).addPaymentMethod(
    new PaymentMethod(
        null, // id
        null, // version
        PaymentMethodType.VISA, "6543210987654321"
    )
);
Customer customer = new Customer(
    null, // id
    null, // version
    "Jane Smith", // name
    "jane.smith@example.com", // email
    List.of(new Address("456 Elm St", "Othertown")), // addresses
    List.of() // paymentMethods
).addPaymentMethod(
    new PaymentMethod(
        null, // id
        null, // version
        PaymentMethodType.VISA, "6543210987654321"
    )
);

Como se puede observar, este enfoque obliga a proporcionar explícitamente incluso valores que no se conocen (null para id o version), y cuando hay múltiples campos del mismo tipo —como String o int—, resulta fácil equivocarse en el orden.

Este patrón se vuelve rápidamente difícil de leer y propenso a errores, especialmente en objetos con más de tres o cuatro campos. La ausencia de nombres de parámetro visibles en el constructor agrava aún más la legibilidad.

Piensa en cómo sería este código si quitásemos los comentarios. 🤦

5.5. Java Optional vs Kotlin Null Safety

En Kotlin, los tipos son non-nullable por defecto, lo que introduce una capa de seguridad en tiempo de compilación. Cualquier acceso a una propiedad que pueda ser null debe tratarse explícitamente, lo que reduce drásticamente la aparición de errores como NullPointerException.

En cambio, en Java es habitual utilizar Optional<T> —especialmente en APIs públicas como Spring Data— para indicar que un valor puede estar presente o no. Aunque Optional proporciona una alternativa segura frente a los nulos directos, requiere un estilo de código más verboso y no impide que otras partes del sistema trabajen directamente con valores null.

Java: uso de Optional

Java
// Java - usando Optional
public Optional<Customer> findCustomer(Long id) {
    return customerRepository.findById(id);
}

public String getCustomerEmail(Long id) {
    Optional<Customer> customer = findCustomer(id);
    if (customer.isPresent()) {
        return customer.get().getEmail();
    }
    return "No email found";
}

// O usando programación funcional
public String getCustomerEmailFunctional(Long id) {
    return findCustomer(id)
        .map(Customer::getEmail)
        .orElse("No email found");
}

Kotlin: null safety integrada en el sistema de tipos

Kotlin
// Kotlin - null safety integrado en el sistema de tipos
fun findCustomer(id: Long): Customer? {
    return customerRepository.findByIdOrNull(id)
}

fun getCustomerEmail(id: Long): String {
    val customer = findCustomer(id)
    return customer?.email ?: "No email found"
}

// O de forma más concisa
fun getCustomerEmailConcise(id: Long): String {
    return findCustomer(id)?.email ?: "No email found"
}

Kotlin convierte automáticamente los Optional<T> devueltos desde código Java en tipos nullable (T?). Esto permite integrarlos directamente con sus propios operadores seguros (?., ?:) y funciones de alcance (let, run, etc.), lo que resulta en un código más expresivo, conciso y fácil de leer.

5.6. Optional fluid APIs vs Kotlin fluid APIs

Las APIs fluidas permiten encadenar transformaciones y validaciones de datos sin necesidad de estructuras condicionales explícitas. Tanto Java como Kotlin ofrecen mecanismos para esto, aunque con enfoques distintos:

  • Java utiliza Optional con métodos como map(), filter() y orElse().
  • Kotlin combina operadores seguros (?., ?:) con funciones de alcance como let, also, apply, etc.

Ejemplo 1: Capa de servicios

En Kotlin, el flujo de transformación es fluido, legible y seguro:

Kotlin
@Transactional
override fun updateCustomer(id: Long, input: Customer): Customer? {
    log.debug("Request updateCustomer: {} {}", id, input)

    return customerRepository
        .findByIdOrNull(id) // convierte Optional en tipo nullable
        ?.let { customerServiceMapper.update(it, input) }
        ?.let { customerRepository.save(it) }
        ?.also { eventsProducer.onCustomerEvent(eventsMapper.asCustomerEvent(it)) }
}

En Java, aunque se puede lograr un flujo similar con Optional, el resultado es más verboso y menos directo:

Java
@Transactional
public Optional<Customer> updateCustomer(Long id, Customer input) {
    log.debug("Request updateCustomer: {} {}", id, input);

    var customer = customerRepository
        .findById(id)
        .map(existingCustomer -> customerServiceMapper.update(existingCustomer, input))
        .map(customerRepository::save);

    if (customer.isPresent()) {
        var customerEvent = eventsMapper.asCustomerEvent(customer.get());
        eventsProducer.onCustomerEvent(customerEvent);
    }

    return customer;
}

Ejemplo 2: Capa de controladores

El enfoque fluido también es muy útil en la capa de controladores para devolver respuestas HTTP en función de la existencia del recurso.

Kotlin:

Kotlin
override fun updateCustomer(customerId: Long, reqBody: CustomerDTO): ResponseEntity<CustomerDTO> {
    log.debug("REST request to updateCustomer: {}, {}", customerId, reqBody)

    return mapper
        .asCustomer(reqBody)
        .let { customerService.updateCustomer(customerId, it) }
        ?.let(mapper::asCustomerDTO)
        ?.let { ResponseEntity.status(200).body(it) }
        ?: ResponseEntity.notFound().build()
}

Java:

Java
@Override
public ResponseEntity<CustomerDTO> getCustomer(Long id) {
    log.debug("REST request to getCustomer: {}", id);
    var customer = customerService.getCustomer(id);
    if (customer.isPresent()) {
        CustomerDTO responseDTO = mapper.asCustomerDTO(customer.get());
        return ResponseEntity.status(200).body(responseDTO);
    } else {
        return ResponseEntity.notFound().build();
    }
}

Ambos lenguajes permiten un estilo funcional, pero Kotlin lo facilita de manera más natural y menos verbosa. El uso de operadores nativos y funciones de alcance hace que incluso flujos complejos sean compactos, seguros frente a nulos, y fáciles de leer.

5.7. API Colecciones/Streams Java vs Kotlin

La verbosidad de la API de Streams en Java, junto con la necesidad constante de invocar stream() y collect(...), hace que trabajar con colecciones sea más engorroso y menos legible en comparación con Kotlin.

En Java, operar sobre listas requiere encadenar múltiples llamadas y utilizar collectors específicos incluso para operaciones comunes como map, filter o groupingBy.

Kotlin, en cambio, proporciona una API de colecciones mucho más directa y expresiva, con soporte nativo para funciones de orden superior sobre listas, mapas y secuencias, sin necesidad de invocar métodos adicionales ni crear flujos intermedios.

5.7.1. Ejemplo en Kotlin

Kotlin proporciona una API de colecciones más expresiva que la de Java, con soporte nativo para funciones de orden superior como filter, map o find. Esto permite construir transformaciones complejas de forma concisa y legible.

Por ejemplo, para obtener una lista de correos electrónicos no nulos de una lista de clientes:

Kotlin
val emails = customers
    .filter { it.email != null }
    .map { it.email!! }

Otro ejemplo típico es el uso de find para buscar una coincidencia concreta dentro de una enumeración:

Kotlin
fun fromValue(value: Int): PaymentMethodType? {
    return entries.find { it.value == value }
}

Este enfoque es idiomático en Kotlin y evita la necesidad de bucles explícitos o estructuras auxiliares. La combinación de expresividad, claridad y seguridad ante nulos hace que el trabajo con colecciones sea una de las áreas donde Kotlin brilla especialmente frente a Java.

5.7.2. Ejemplo equivalente en Java

Java
List<String> emails = customers.stream()
    .map(Customer::getEmail)
    .filter(Objects::nonNull)
    .collect(Collectors.toList());

Este ejemplo sencillo ilustra cómo Kotlin reduce el boilerplate al mínimo y mejora la legibilidad sin sacrificar funcionalidad. A mayor complejidad en la transformación de datos, mayor será la diferencia en claridad y expresividad entre ambas aproximaciones.

5.8. Interpolación de Strings y Text Blocks

Java introdujo los text blocks en la versión 13 (como preview feature) y los estabilizó en Java 15. Estos permiten definir cadenas multilínea más legibles, aunque sin interpolación directa. Kotlin, por su parte, ha ofrecido desde su primera versión una interpolación de strings nativa, concisa y expresiva.

Ejemplo clásico:

Java – Text blocks sin interpolación:

Java
// Java - Text blocks sin interpolación
public String generateCustomerReport(Customer customer) {
    return """
        Customer Report
        ===============
        Name: %s
        Email: %s
        Addresses: %d
        Payment Methods: %d
        "
"".formatted(
            customer.getName(),
            customer.getEmail(),
            customer.getAddresses().size(),
            customer.getPaymentMethods().size()
        );
}

// Java - Concatenación tradicional
public String getCustomerSummary(Customer customer) {
    return "Customer " + customer.getName() +
           " (" + customer.getEmail() + ") has " +
           customer.getAddresses().size() + " addresses";
}

Kotlin – Interpolación de strings nativa:

Kotlin
// Kotlin - String interpolation nativa
fun generateCustomerReport(customer: Customer): String {
    return """
        Customer Report
        ===============
        Name: ${customer.name}
        Email: ${customer.email}
        Addresses: ${customer.addresses.size}
        Payment Methods: ${customer.paymentMethods.size}
    "
"".trimIndent()
}

// Kotlin - Interpolación simple y expresiones
fun getCustomerSummary(customer: Customer): String {
    return "Customer ${customer.name} (${customer.email}) has ${customer.addresses.size} addresses"
}

// Kotlin - Interpolación con expresiones complejas
fun getCustomerStatus(customer: Customer): String {
    return "Customer ${customer.name} is ${if (customer.paymentMethods.isNotEmpty()) "active" else "inactive"}"
}

Kotlin permite interpolar directamente con $variable o expresiones complejas con ${expression}, eliminando la necesidad de usar StringBuilder, formatted() o concatenaciones explícitas, lo que mejora la legibilidad y menos propenso a errores, especialmente cuando se construyen strings complejos con múltiples variables.

5.8.1. Interpolación de Strings en Java 24 (Preview Feature)

Java 24 introduce string interpolation como preview feature (JEP 459), acercándose finalmente a las capacidades que Kotlin ha ofrecido desde el principio, aunque aún requiere activar preview features para su uso.

Java 24 – Interpolación nativa (preview):

Java
// Java 24 - String interpolation (preview feature)
public String generateCustomerReport(Customer customer) {
    return STR."""
        Customer Report
        ===============
        Name: \{customer.getName()}
        Email: \{customer.getEmail()}
        Addresses: \{customer.getAddresses().size()}
        Payment Methods: \{customer.getPaymentMethods().size()}
        "
"";
}

// Java 24 - Interpolación simple
public String getCustomerSummary(Customer customer) {
    return STR."Customer \{customer.getName()} (\{customer.getEmail()}) has \{customer.getAddresses().size()} addresses";
}

// Java 24 - Interpolación con expresiones
public String getCustomerStatus(Customer customer) {
    return STR."Customer \{customer.getName()} is \{customer.getPaymentMethods().isEmpty() ? "inactive" : "active"}";
}

Este nuevo enfoque mejora sustancialmente la legibilidad y mantenimiento del código, aunque aún está sujeto a cambios antes de su estabilización en versiones futuras.

Conclusiones

Después de analizar en detalle las diferencias entre Java y Kotlin en el contexto de aplicaciones Spring Boot empresariales, podemos extraer las siguientes conclusiones:

  • Sintaxis y expresividad: Kotlin ofrece una sintaxis más concisa, limpia y flexible, incluso frente a las últimas versiones de Java. La reducción de boilerplate es significativa, lo que se traduce en mayor productividad, código más legible y más fácil de mantener.

  • APIs fluidas y DSLs: Gracias a sus funciones de alcance (let, apply, run…) y operadores como ?. o ?:, Kotlin permite escribir código expresivo que se comporta casi como un DSL. Esto facilita un estilo más declarativo, sin sacrificar potencia.

  • Curva de aprendizaje y estilo: Esta flexibilidad sintáctica también implica una mayor variabilidad. Es fácil que dos desarrolladores escriban la misma lógica de formas muy distintas. Por eso, es recomendable definir convenciones de equipo para mantener coherencia y legibilidad a largo plazo.

  • Interoperabilidad Java-Kotlin: La convivencia entre código Java y Kotlin en un mismo proyecto es totalmente viable, incluso si parte de ese código es generado por herramientas como OpenAPI Generator, AsyncAPI, Avro o MapStruct. Una configuración adecuada del sistema de build, especialmente en el uso de Kapt, es clave para garantizar esta interoperabilidad.

  • Soporte en Spring Boot: Spring Boot ofrece un soporte maduro y bien integrado para Kotlin, que seguirá mejorando con la próxima versión mayor (Spring Boot 4, prevista para noviembre de 2025). La experiencia de desarrollo es fluida y estable.

  • Herramientas de build: Tanto Maven como Gradle funcionan correctamente con Kotlin. Sin embargo, Gradle (especialmente con Kotlin DSL) ofrece una experiencia más natural, sin que eso implique tener que abandonar Maven si ya forma parte del flujo de trabajo del equipo.

En resumen, Kotlin es una opción plenamente válida y recomendable para desarrollar microservicios backend con Spring Boot, especialmente para equipos que valoran la elegancia del código, la expresividad y la productividad. Siempre que exista un nivel técnico suficiente y una disciplina en las convenciones de equipo, Kotlin permite construir soluciones limpias, seguras y altamente mantenibles.

7. Referencias bibliográficas

  1. JetBrains. (2024). Kotlin Language Documentation.
    https://kotlinlang.org/docs/home.html
    Documentación oficial del lenguaje Kotlin, con detalles sobre data classes, null safety, coroutines y otras características clave mencionadas en el artículo.

  2. Pivotal/VMware. (2024). Spring Boot Reference Documentation.
    https://docs.spring.io/spring-boot/docs/current/reference/html/
    Guía oficial de Spring Boot, marco base del proyecto analizado en el artículo.

  3. OpenAPI Generator. (2024). Documentation and Generator Options.
    https://openapi-generator.tech/docs/usage/
    Recurso de referencia para la generación de código basado en definiciones OpenAPI en proyectos Java y Kotlin.

  4. ZenWave SDK. (2024). AsyncAPI Spring Cloud Streams Plugin.
    https://www.zenwave360.io/zenwave-sdk/plugins/asyncapi-spring-cloud-streams3/
    Documentación oficial del plugin usado para generar código a partir de definiciones AsyncAPI.

  5. Apache. (2024). Apache Avro™ 1.12.0 Documentation.
    https://avro.apache.org/docs/1.12.0/
    Referencia técnica sobre el sistema de serialización Avro utilizado para los eventos en el proyecto.

  6. MapStruct. (2024). MapStruct: Java Bean Mapping.
    https://mapstruct.org/
    Herramienta clave en la conversión de DTOs y entidades, usada en ambos enfoques (Java/Kotlin).

  7. Lombok Project. (2024). Project Lombok.
    https://projectlombok.org/
    Documentación del popular procesador de anotaciones en Java, comparado con Kotlin en el artículo.

  8. Gradle Inc. (2024). Gradle Kotlin DSL Reference.
    https://docs.gradle.org/current/userguide/kotlin_dsl.html
    Guía oficial sobre la configuración de proyectos Gradle usando Kotlin DSL.

  9. Maven Project. (2024). Apache Maven Project.
    https://maven.apache.org/
    Sitio oficial del sistema de construcción Maven, utilizado en la configuración dual del proyecto.

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad

Información básica acerca de la protección de datos

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Iván García Sáinz es un Arquitecto de Software con amplia experiencia en tecnologías como Java, Spring Boot y Spring Cloud. Especializado en Domain-Driven Design (DDD) y arquitecturas dirigidas por eventos, Iván se dedica a crear soluciones de software que destacan por su claridad y eficiencia. Además de su labor profesional, Iván forma parte del Comité Técnico de Dirección de la especificación AsyncAPI, contribuyendo activamente al desarrollo y promoción de estándares para APIs asíncronas. Es el fundador de ZenWave 360º, una plataforma orientada a facilitar la adopción de enfoques API-First mediante herramientas innovadoras como el ZenWave SDK.

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

30/10/2025

Benjamín Suárez Menéndez

El Complex Problem Solving (CPS) es un proceso estructurado basado en herramientas, técnicas y actitudes que nos facilita la resolución de problemas complejos.

03/10/2025

Miguel García Rodríguez

Descubre cómo el diseño y la psicología del comportamiento utilizan sesgos cognitivos para influir en la toma de decisiones de los usuarios y potenciar la persuasión.

30/09/2025

Iván García Sainz-Aja

En este artículo exploraremos cómo utilizar ZenWave360 para generar un proyecto completo de Spring Boot con Kotlin a partir de un modelo DSL de Lenguaje Ubicuo.