Mercado laboral IT 2026: qué habilidades debe aprender un dev
1. Introducción
El mercado laboral IT está cambiando hacia una dirección que exige más de cada developer. Escribir una feature no es suficiente. También necesitas entender cómo esa feature se ejecuta, falla y consume recursos en production.
Este artículo no ofrece estadísticas ni predicciones salariales. En su lugar, analiza cinco grupos de habilidades fundamentales que están ganando atención, explica por qué, e ilustra cada uno con código Java 21 / Spring Boot 3.x. Después de leerlo, sabrás qué priorizar, cómo aplicarlo inmediatamente en tus proyectos y cuándo no debes seguir las tendencias.
2. Por qué los requisitos de habilidades están cambiando
Hay tres fuerzas principales, y puedes observarlas por ti mismo en la mayoría de los anuncios de trabajo:
- Las herramientas de IA para escribir código son omnipresentes. El código “boilerplate” se genera más rápido. El valor del developer se desplaza hacia el diseño, la revisión, las pruebas y la responsabilidad sobre los resultados.
- Las empresas se enfocen en costos operacionales. Un servicio que consume menos recursos y es más fácil diagnosticar ahorrará dinero directamente.
- Equipos más pequeños, responsabilidad más amplia. El modelo “you build it, you run it” hace que los developers tengan que saber sobre empaquetamiento, monitoreo y resolución de incidentes.
De esto se derivan cinco grupos de habilidades que se destacan como inversiones a largo plazo con buen retorno.
3. Integrar IA en aplicaciones de forma controlada
La habilidad más mencionada actualmente no es “saber usar un chatbot”. Es la capacidad de integrar LLMs en sistemas reales manteniendo estabilidad, capacidad de prueba y planes de contingencia.
Principio fundamental: trata un LLM como una dependencia externa no confiable. Su salida puede tener formato incorrecto, ser lenta, o causar errores de red.
3.1. Aislar el LLM detrás de una interfaz
@FunctionalInterface
public interface LlmClient {
String complete(String prompt);
}
public enum TicketCategory { BILLING, TECHNICAL, OTHER }
public class TicketClassifier {
private final LlmClient llm;
public TicketClassifier(LlmClient llm) {
this.llm = llm;
}
public TicketCategory classify(String message) {
String prompt = """
Clasifica la siguiente solicitud de soporte en exactamente una etiqueta: BILLING, TECHNICAL, OTHER.
Solo devuelve la etiqueta, sin explicaciones.
Solicitud: %s
""".formatted(message);
try {
String raw = llm.complete(prompt).trim().toUpperCase();
return TicketCategory.valueOf(raw);
} catch (RuntimeException e) {
return TicketCategory.OTHER;
}
}
}
Explicación:
LlmClientes tu propia interfaz. La implementación real puede llamar a cualquier proveedor a través de HTTP o mediante una biblioteca como Spring AI. La lógica de negocio no depende de un proveedor específico.- El prompt requiere un resultado con una sola etiqueta. La restricción de formato te ayuda a parsear el resultado.
TicketCategory.valueOf(raw)realiza parsing y validación simultáneamente. Si el modelo devuelve una respuesta larga, se captura laIllegalArgumentExceptiony el sistema cae de vuelta aOTHER. Los errores de red (generalmenteRuntimeException) también se manejan aquí.
3.2. Pruebas sin necesidad de llamar al modelo real
Como LlmClient es una interfaz funcional, puedes pasar un lambda como fake:
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class TicketClassifierTest {
@Test
void givenValidLabel_whenClassify_thenReturnCategory() {
TicketClassifier classifier = new TicketClassifier(prompt -> " billing\n");
assertThat(classifier.classify("Me cobraron dos veces"))
.isEqualTo(TicketCategory.BILLING);
}
@Test
void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
TicketClassifier classifier = new TicketClassifier(prompt -> "Tal vez es BILLING");
assertThat(classifier.classify("Me cobraron dos veces"))
.isEqualTo(TicketCategory.OTHER);
}
@Test
void givenClientFailure_whenClassify_thenFallbackToOther() {
TicketClassifier classifier = new TicketClassifier(prompt -> {
throw new IllegalStateException("timeout");
});
assertThat(classifier.classify("La app se bloqueó")).isEqualTo(TicketCategory.OTHER);
}
}
La primera prueba demuestra que el código tolera espacios en blanco y minúsculas. Las dos siguientes prueban que el sistema no se desmorona cuando el modelo “habla demasiado” o la conexión falla. Esto es exactamente lo que los reclutadores quieren ver: sabes que los LLMs fallarán y has diseñado para ello.
Nota: No uses LLM para tareas que una regla, regex o consulta SQL pueden hacer con precisión y de forma más económica. Evita también LLMs para decisiones que requieren resultados deterministas y auditables, como calcular tarifas o asignar permisos.
4. Concurrency moderno con virtual threads
La mayoría del backend es I/O-bound: llamadas a base de datos, llamadas a otras APIs. Java 21 convierte los virtual threads en una característica oficial, permitiendo escribir código blocking familiar mientras sirves muchas solicitudes concurrentes. Entender cuándo usar y cuándo no usar virtual threads es un punto a favor claro en una entrevista.
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class PriceAggregator {
public static void main(String[] args) throws Exception {
List<String> suppliers = List.of("SupplierA", "SupplierB", "SupplierC");
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = suppliers.stream()
.map(s -> executor.submit(() -> fetchPrice(s)))
.toList();
for (Future<String> f : futures) {
System.out.println(f.get());
}
}
System.out.println("Tiempo total: " + (System.currentTimeMillis() - start) + " ms");
}
static String fetchPrice(String supplier) throws InterruptedException {
Thread.sleep(Duration.ofSeconds(1)); // simula una llamada API lenta
return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
}
}
newVirtualThreadPerTaskExecutor()crea un virtual thread para cada tarea. No necesitas ajustar el tamaño del pool.- El bloque
try-with-resourcesasegura que el executor espere a que todas las tareas terminen antes de cerrarse. - Las tres llamadas, cada una lenta por un segundo, se ejecutan en paralelo, por lo que el tiempo total es aproximadamente un segundo:
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
Tiempo total: 1015 ms
En Spring Boot 3.2 o superior, habilitas los virtual threads para el manejo de solicitudes con una sola línea:
spring.threads.virtual.enabled=true
Nota: Los virtual threads no hacen más rápido el código CPU-bound. Ten cuidado con bloques
synchronizedalrededor de operaciones I/O largas, ya que pueden fijar un virtual thread a un carrier thread. El impacto depende de la versión de JDK. Además, ejecutar más tareas concurrentemente puede agotar el pool de conexiones de la base de datos. Aún necesitas limitar los recursos en capas posteriores.
5. Observability: saber qué está haciendo tu sistema
Cuando eres responsable de la operación, la pregunta “¿por qué es lento?” debe responderse con datos. Métricas, logs y traces son los tres pilares. En el ecosistema Spring, Micrometer y Actuator son el punto de partida natural.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Agrega una métrica de negocio personalizada:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Counter placedOrders;
public OrderService(MeterRegistry registry) {
this.placedOrders = Counter.builder("orders.placed")
.description("Cantidad de órdenes colocadas exitosamente")
.tag("channel", "web")
.register(registry);
}
public void placeOrder(String orderId) {
// ... guardar orden, llamar a PaymentService ...
placedOrders.increment();
}
}
management.endpoints.web.exposure.include=health,metrics
MeterRegistryes configurado automáticamente por Spring Boot e inyectado en el bean.- La etiqueta
channelpermite segmentar las métricas por canal de ventas en el dashboard. - La línea de configuración solo expone dos endpoints,
healthymetrics, no todos.
Después de algunas órdenes, llamando a GET /actuator/metrics/orders.placed:
{
"name": "orders.placed",
"description": "Cantidad de órdenes colocadas exitosamente",
"measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
"availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}
Nota: No uses valores con cardinalidad alta e ilimitada (como
orderIdocustomerId) como etiquetas. Cada valor de etiqueta crea una nueva serie de tiempo, por lo que tu sistema de métricas crece muy rápido. Tampoco expongas todos los endpoints del actuator a Internet.
6. Testing con infraestructura real usando Testcontainers
Cuando la IA puede generar código rápidamente, las pruebas son lo que demuestra que el código es correcto. Las pruebas de integración que se ejecutan en una base de datos real, en lugar de una base de datos en memoria con un dialecto diferente, ayudan a detectar errores que los mocks no pueden.
Las siguientes dependencias están versionadas por Spring Boot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
Entity y repositorio mínimos:
@Entity
public class Customer {
@Id @GeneratedValue
private Long id;
private String email;
protected Customer() {}
public Customer(String email) { this.email = email; }
public Long getId() { return id; }
public String getEmail() { return email; }
}
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
}
Prueba de integración:
@SpringBootTest
@Testcontainers
class CustomerRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
CustomerRepository repository;
@Test
void givenSavedCustomer_whenFindByEmail_thenReturnCustomer() {
repository.save(new Customer("lan@example.com"));
assertThat(repository.findByEmail("lan@example.com"))
.isPresent()
.get()
.extracting(Customer::getEmail)
.isEqualTo("lan@example.com");
}
}
@Testcontainersy@Containerinician un PostgreSQL real en Docker antes de que se ejecuten las pruebas.@ServiceConnection(disponible desde Spring Boot 3.1) inyecta automáticamente la URL, nombre de usuario y contraseña del contenedor en elDataSource. No necesitas@DynamicPropertySource.- La consulta
findByEmailes generada por Spring Data y se ejecuta en el tipo de base de datos exacto de production.
Nota: Testcontainers requiere Docker en tu máquina de desarrollo y en CI. Las pruebas también son más lentas que las unit tests. Mantén la mayoría de la lógica en unit tests puros, usa Testcontainers solo para la capa de integración como repositorios, mensajería y migraciones.
7. Empaquetamiento y operación de contenedores
Entender cómo se empaquetan y ejecutan las aplicaciones es una habilidad mínima al trabajar con Kubernetes o plataformas en la nube. Un Dockerfile multi-stage bien estructurado demuestra que te importan el tamaño de la imagen y la seguridad:
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]
- La etapa
buildcontiene JDK y el Maven wrapper para compilar. - La etapa final solo contiene JRE y el archivo JAR, haciendo la imagen más pequeña y con menos superficie de ataque.
USER 1000asegura que el contenedor no se ejecute como root, un error muy común.
Puedes combinar esto con el endpoint /actuator/health de la sección 5 como liveness/readiness probe al hacer deploy.
Nota: Si tu equipo ya usa Buildpacks (por ejemplo con
./mvnw spring-boot:build-image), no necesitas escribir tu propio Dockerfile. Lo importante es entender qué contiene la imagen, con qué usuario se ejecuta y cómo los límites de memoria afectan a la JVM.
8. Cuándo no seguir las tendencias
Invertir en habilidades también requiere claridad:
- No ignores los fundamentos. Estructuras de datos, SQL, HTTP, transacciones, manejo de errores. Estos siguen siendo preguntas en toda entrevista. La IA y los frameworks cambian mucho más rápido que este conocimiento.
- No aprendas de forma dispersa. Elige una o dos habilidades relacionadas con tu trabajo actual, aplícalas en un proyecto real, luego expande. Un PR que agregue métricas y pruebas de integración vale más que diez certificaciones sin usar.
- No introduzcas tecnología solo para ponerla en tu CV. Un sistema pequeño con pocos usuarios no necesita Kubernetes, microservicios o LLMs. Saber decir “no es necesario” con razones es una habilidad valorada.
- No delegues completamente a herramientas generativas de código. Aún eres responsable. Lee, entiende y escribe pruebas para todo código generado.
9. Conclusión
El mercado IT recompensa a los developers que dominan el ciclo de vida completo de una feature: diseño, codificación, pruebas, empaquetamiento y operación. Este artículo ha cubierto cinco grupos de habilidades que valen la pena desarrollar:
- Integrar LLMs detrás de una interfaz, con validación y planes de contingencia.
- Usar virtual threads para cargas I/O-bound.
- Agregar observability con Micrometer y Actuator.
- Escribir pruebas de integración sobre infraestructura real con Testcontainers.
- Empaquetar contenedores de forma segura.
Cada habilidad tiene sus propios límites, y conocer esos límites es tan importante como saber cómo usarla. Elige un tema, aplícalo inmediatamente en el servicio en el que trabajas esta semana. Esa es la forma más rápida de convertir una tendencia en experiencia real.