Mercado de TI 2026: em quais habilidades devs devem investir

1. Visão geral

O mercado de trabalho de TI está mudando no sentido de exigir mais de cada desenvolvedor. Apenas escrever código não é suficiente. Você também precisa entender como a funcionalidade é executada, como os erros ocorrem e quanto custa manter em produção.

Este artigo não traz estatísticas ou previsões de salários. Em vez disso, analisa cinco grupos de habilidades fundamentais que estão em alta demanda, explica por que, e ilustra cada grupo com código Java 21 / Spring Boot 3.x. Depois de ler, você saberá o que priorizar aprender, como aplicar imediatamente em seus projetos e quando não seguir as tendências cegamente.

2. Por que os requisitos de habilidades estão mudando

Há três forças principais, e você pode observá-las na maioria dos anúncios de emprego:

  • Ferramentas de IA para escrever código se tornaram comuns. Código “boilerplate” é gerado muito mais rapidamente. O valor do desenvolvedor muda para design, review, testes e responsabilidade pelos resultados.
  • Empresas focam nos custos operacionais. Um serviço que usa menos recursos e é mais fácil de diagnosticar economiza dinheiro diretamente.
  • Times menores, maior propriedade. O modelo “you build it, you run it” faz com que o desenvolvedor precise entender empacotamento, monitoramento e resolução de incidentes.

Dessas forças, os cinco grupos de habilidades abaixo emergem como investimentos de longo prazo com bom retorno.

3. Integração de IA em aplicações com controle

A habilidade mais comentada atualmente não é “saber usar chatbot”. É a capacidade de integrar LLMs em sistemas reais mantendo estabilidade, testabilidade e planos de fallback.

Princípio central: trate o LLM como uma dependência externa não confiável. Sua saída pode estar em formato errado, ser lenta, ou falhar na conexão.

3.1. Separar o LLM atrás de uma interface

@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 = """
            Classifique a seguinte solicitação de suporte em exatamente um rótulo: BILLING, TECHNICAL, OTHER.
            Retorne apenas o rótulo, sem explicação.
            Solicitação: %s
            """.formatted(message);
        try {
            String raw = llm.complete(prompt).trim().toUpperCase();
            return TicketCategory.valueOf(raw);
        } catch (RuntimeException e) {
            return TicketCategory.OTHER;
        }
    }
}

Explicação:

  • LlmClient é uma interface sua. A implementação real pode chamar qualquer provedor via HTTP ou através de uma biblioteca como Spring AI. A lógica de negócio não depende de um provedor específico.
  • O prompt exige que a saída seja um único rótulo. A restrição de formato ajuda você a fazer parse do resultado.
  • TicketCategory.valueOf(raw) faz parsing e validação. Se o modelo retornar uma sentença longa, IllegalArgumentException é capturada e o sistema faz fallback para OTHER. Erros de rede (geralmente RuntimeException) também caem neste branch.

3.2. Testar sem chamar o modelo real

Como LlmClient é uma interface funcional, você pode passar uma 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("Fui cobrado duas vezes"))
            .isEqualTo(TicketCategory.BILLING);
    }

    @Test
    void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> "Talvez seja BILLING");

        assertThat(classifier.classify("Fui cobrado duas vezes"))
            .isEqualTo(TicketCategory.OTHER);
    }

    @Test
    void givenClientFailure_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> {
            throw new IllegalStateException("timeout");
        });

        assertThat(classifier.classify("App caiu")).isEqualTo(TicketCategory.OTHER);
    }
}

O primeiro teste mostra que o código tolera espaços em branco e letras minúsculas. Os dois testes seguintes comprovam que o sistema não quebra quando o modelo “fala muito” ou quando há erro de conexão. Isso é exatamente o que os empregadores querem ver: você sabe que o LLM vai falhar e projetou para isso.

Nota: Não use LLM para tarefas que uma regra, regex ou consulta SQL faria com precisão e menor custo. Também evite LLM para decisões que exigem resultados determinísticos e auditáveis, como cálculo de valores ou atribuição de permissões.

4. Concorrência moderna com virtual threads

A maioria do backend é I/O-bound: consultar banco de dados, chamar outras APIs. Java 21 torna virtual threads uma funcionalidade oficial, permitindo escrever código blocking familiar enquanto ainda atende muitas requisições simultâneas. Saber quando usar e quando não usar é um diferencial claro em entrevistas.

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("Tempo total: " + (System.currentTimeMillis() - start) + " ms");
    }

    static String fetchPrice(String supplier) throws InterruptedException {
        Thread.sleep(Duration.ofSeconds(1)); // simular chamada de API lenta
        return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
    }
}
  • newVirtualThreadPerTaskExecutor() cria uma virtual thread para cada tarefa. Você não precisa ajustar o tamanho da pool.
  • O bloco try-with-resources garante que o executor aguarde a conclusão de todas as tarefas antes de encerrar.
  • Três chamadas lentas de um segundo são executadas em paralelo, então o tempo total é aproximadamente um segundo:
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
Tempo total: 1015 ms

Em Spring Boot 3.2 ou superior, você ativa virtual threads para request handling com uma linha:

spring.threads.virtual.enabled=true

Nota: Virtual threads não tornam o código CPU-bound mais rápido. Tenha cuidado com blocos synchronized envolvendo operações I/O longas, pois podem prender a virtual thread ao carrier thread. O grau de impacto depende da versão do JDK. Além disso, executar mais tarefas simultaneamente pode esgotar a pool de conexões do banco de dados. Você ainda precisa limitar recursos na camada subjacente.

5. Observabilidade: saber o que o sistema está fazendo

Quando você é responsável pela operação, a pergunta “por que está lento?” deve ser respondida com dados. Métricas, logs e traces são os três pilares. No ecossistema Spring, Micrometer e Actuator são o ponto de partida natural.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Adicione uma métrica de negócio 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("Número de pedidos colocados com sucesso")
            .tag("channel", "web")
            .register(registry);
    }

    public void placeOrder(String orderId) {
        // ... salvar pedido, chamar PaymentService ...
        placedOrders.increment();
    }
}
management.endpoints.web.exposure.include=health,metrics
  • MeterRegistry é auto-configurado pelo Spring Boot e injetado no bean.
  • A tag channel permite separar as métricas por canal de vendas ao visualizar o dashboard.
  • A linha de configuração abre apenas os endpoints health e metrics, não todos.

Após alguns pedidos, chame GET /actuator/metrics/orders.placed:

{
  "name": "orders.placed",
  "description": "Número de pedidos colocados com sucesso",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}

Nota: Não use valores com cardinalidade alta e ilimitada (como orderId ou customerId) como tags. Cada valor de tag cria uma nova série temporal, causando explosão exponencial no sistema de métricas. Também não exponha todos os endpoints do actuator na Internet.

6. Testes com infraestrutura real usando Testcontainers

Quando a IA pode gerar código rapidamente, testes são o que provam que o código está correto. Testes de integração executados em um banco de dados real, em vez de um banco em memória com dialeto diferente, ajudam a capturar erros que mocks não capturam.

As seguintes dependências têm suas versões gerenciadas pelo 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>

Entidade e repositório minimalistas:

@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);
}

Teste de integração:

@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");
    }
}
  • @Testcontainers e @Container iniciam um PostgreSQL real em Docker antes dos testes executarem.
  • @ServiceConnection (disponível desde Spring Boot 3.1) injeta automaticamente a URL, nome de usuário e senha do container no DataSource. Você não precisa de @DynamicPropertySource.
  • A query findByEmail é gerada pelo Spring Data e executada no banco de dados exatamente igual ao de produção.

Nota: Testcontainers requer Docker na máquina de desenvolvimento e no CI. Testes também são mais lentos que unit tests. Mantenha a maior parte da lógica em testes unitários puros, use Testcontainers apenas para a camada de integração como repositórios, mensagens e migrações.

7. Empacotamento e operação de containers

Entender como a aplicação é empacotada e executada é uma habilidade mínima ao trabalhar com Kubernetes ou plataformas cloud. Um Dockerfile multi-stage bem escrito demonstra que você se importa com tamanho de imagem e segurança:

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"]
  • O estágio build tem JDK e Maven wrapper para compilação.
  • O estágio final contém apenas JRE e o arquivo jar, tornando a imagem menor e com menos superfície de ataque.
  • USER 1000 faz o container não rodar como root, um erro muito comum.

Você pode combinar com o endpoint /actuator/health da seção 5 como probe de liveness/readiness ao fazer deploy.

Nota: Se o time já usa Buildpacks (por exemplo via ./mvnw spring-boot:build-image), você não precisa escrever o Dockerfile. O importante é entender o que a imagem contém, qual usuário a executa e como limites de memória afetam a JVM.

8. Quando não seguir as tendências

O investimento em habilidades também precisa de bom senso:

  • Não negligencie os fundamentos. Estruturas de dados, SQL, HTTP, transações e tratamento de erros ainda são questionados em toda entrevista. IA e frameworks mudam muito mais rapidamente do que esses conhecimentos.
  • Não aprenda de forma dispersa. Escolha uma ou duas habilidades relacionadas ao seu trabalho atual, aplique em projetos reais, depois expanda. Um PR adicionando métricas e testes de integração tem mais valor que dez certificados não utilizados.
  • Não adicione tecnologia apenas para ter no currículo. Um sistema pequeno com poucos usuários não precisa de Kubernetes, microserviços ou LLM. Saber dizer “não é necessário” com justificativas também é uma habilidade valorizada.
  • Não delegue completamente para ferramentas de geração de código. Você ainda é responsável. Leia, entenda e escreva testes para todo código gerado.

9. Conclusão

O mercado de TI recompensa desenvolvedores que dominam todo o ciclo de vida da feature: design, implementação, testes, empacotamento e operação. Este artigo cobriu cinco grupos de habilidades que valem a pena investir:

  • Integração de LLM atrás de uma interface, com validação e fallback.
  • Usar virtual threads para workloads I/O-bound.
  • Adicionar observabilidade com Micrometer e Actuator.
  • Escrever testes de integração com infraestrutura real usando Testcontainers.
  • Empacotar containers de forma segura.

Cada habilidade tem seus próprios limites, e conhecer esses limites é tão importante quanto saber como usar. Escolha um tópico e aplique imediatamente ao serviço em que está trabalhando esta semana. Essa é a forma mais rápida de transformar tendências em experiência real.

carreiratech-trendsjavaspring-bootai