Marché de l'IT en 2026 : quelles compétences les devs devraient acquérir

1. Vue d’ensemble

Le marché du travail IT change en exigeant davantage de chaque développeur. Savoir coder une fonctionnalité ne suffit plus. Vous devez aussi comprendre comment cette fonctionnalité s’exécute, quels sont les problèmes en production et quel en est le coût.

Cet article n’apporte pas de chiffres ni de prédictions salariales. À la place, il analyse cinq groupes de compétences fondamentales qui intéressent les recruteurs, explique les raisons, et illustre chaque groupe avec du code Java 21 / Spring Boot 3.x. Après la lecture, vous saurez sur quoi vous concentrer, comment l’appliquer à vos projets et quand ne pas suivre les tendances à la mode.

2. Pourquoi les exigences en compétences sont en train de changer

Il y a trois forces principales, que vous pouvez observer dans la plupart des annonces d’emploi :

  • Les outils IA pour l’assistance au code sont maintenant omniprésents. Le code « boilerplate » se génère plus vite. La valeur d’un dev se déplace vers la conception, la revue de code, les tests et la responsabilité des résultats.
  • Les entreprises se concentrent sur les coûts d’exploitation. Un service qui utilise moins de ressources et dont les erreurs sont plus faciles à diagnostiquer économisera directement de l’argent.
  • Des équipes plus petites, une plus grande responsabilité. Le modèle « vous le construisez, vous le maintenez » fait en sorte que les devs doivent maîtriser l’empaquetage, la surveillance et la résolution des problèmes.

De là, les cinq groupes de compétences ci-dessous émergent comme des investissements avec un retour à long terme.

3. Intégrer l’IA dans l’application de manière contrôlée

La compétence la plus citée en ce moment n’est pas « savoir utiliser un chatbot ». C’est la capacité à intégrer un LLM dans un système réel tout en gardant la stabilité, la testabilité et un plan de secours.

Principe fondamental : traiter le LLM comme une dépendance externe non fiable. Sa sortie peut être mal formatée, lente ou avoir des erreurs réseau.

3.1. Isoler le LLM derrière une 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 = """
            Classer la demande de support suivante dans exactement un label : BILLING, TECHNICAL, OTHER.
            Retourner uniquement le label, pas d'explications.
            Demande: %s
            """.formatted(message);
        try {
            String raw = llm.complete(prompt).trim().toUpperCase();
            return TicketCategory.valueOf(raw);
        } catch (RuntimeException e) {
            return TicketCategory.OTHER;
        }
    }
}

Explication :

  • LlmClient est votre propre interface. L’implémentation réelle peut appeler n’importe quel fournisseur via HTTP ou via une bibliothèque comme Spring AI. La logique métier ne dépend pas d’un fournisseur spécifique.
  • Le prompt demande que la sortie soit un seul label. La contrainte de format vous aide à analyser le résultat.
  • TicketCategory.valueOf(raw) fait à la fois l’analyse et la validation. Si le modèle retourne une longue phrase, IllegalArgumentException est levée et le système bascule vers OTHER. Les erreurs réseau (généralement RuntimeException) entrent aussi dans cette branche.

3.2. Tester sans appeler le modèle réel

Puisque LlmClient est une interface fonctionnelle, vous pouvez passer une lambda comme bouchon :

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("On m'a débité deux fois"))
            .isEqualTo(TicketCategory.BILLING);
    }

    @Test
    void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> "Peut-être BILLING");

        assertThat(classifier.classify("On m'a débité deux fois"))
            .isEqualTo(TicketCategory.OTHER);
    }

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

        assertThat(classifier.classify("L'app a planté")).isEqualTo(TicketCategory.OTHER);
    }
}

Le premier test montre que le code tolère les espaces et les minuscules. Les deux tests suivants prouvent que le système ne s’effondre pas quand le modèle « raconte » ou que la connexion échoue. C’est exactement ce que les recruteurs veulent voir : vous savez que le LLM se trompe et vous avez conçu le système pour ce cas.

Note : N’utilisez pas LLM pour les choses qu’une règle, un regex ou une requête SQL peut faire plus précisément et moins cher. Évitez aussi d’utiliser le LLM pour les décisions qui exigent des résultats déterministes et vérifiables, comme la facturation ou l’autorisation.

4. Concurrence moderne avec les virtual threads

La plupart des backends sont I/O-bound : appels de base de données, appels à d’autres APIs. Java 21 a rendu les virtual threads officiels, vous permettant d’écrire du code blocking familier tout en servant un très grand nombre de requêtes concurrentes. Savoir quand les utiliser et quand ne pas les utiliser est un plus clair dans les entretiens.

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

    static String fetchPrice(String supplier) throws InterruptedException {
        Thread.sleep(Duration.ofSeconds(1)); // simule un appel API lent
        return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
    }
}
  • newVirtualThreadPerTaskExecutor() crée un virtual thread pour chaque tâche. Vous n’avez pas besoin d’ajuster la taille du pool.
  • Le bloc try-with-resources garantit que l’executor attend que toutes les tâches se terminent avant de fermer.
  • Les trois appels lents d’une seconde s’exécutent en parallèle, donc le temps total est environ une seconde :
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
Temps total : 1015 ms

Dans Spring Boot 3.2 et versions ultérieures, vous activez les virtual threads pour la gestion des requêtes avec une seule ligne :

spring.threads.virtual.enabled=true

Note : Les virtual threads ne rendent pas le code CPU-bound plus rapide. Soyez prudent avec les blocs synchronized autour des opérations I/O longues, car ils peuvent épingler le virtual thread sur un carrier thread. L’impact dépend encore de la version du JDK. De plus, exécuter plus de tâches concurrentes peut épuiser le pool de connexions de la base de données. Vous devez toujours limiter les ressources au niveau du backend.

5. Observabilité : savoir ce que fait votre système

Quand vous êtes responsable aussi de l’exploitation, la question « pourquoi c’est lent ? » doit trouver réponse dans les données. Les métriques, les logs et les traces sont les trois piliers. Dans l’écosystème Spring, Micrometer et Actuator sont le point de départ naturel.

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

Ajoutez une métrique métier personnalisée :

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("Nombre de commandes placées avec succès")
            .tag("channel", "web")
            .register(registry);
    }

    public void placeOrder(String orderId) {
        // ... sauvegarder la commande, appeler PaymentService ...
        placedOrders.increment();
    }
}
management.endpoints.web.exposure.include=health,metrics
  • MeterRegistry est automatiquement configuré par Spring Boot et injecté dans le bean.
  • Le tag channel vous permet de segmenter les métriques par canal de vente lors de la visualisation du tableau de bord.
  • La ligne de configuration n’expose que les deux endpoints health et metrics, pas tous.

Après quelques commandes, appelez GET /actuator/metrics/orders.placed :

{
  "name": "orders.placed",
  "description": "Nombre de commandes placées avec succès",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}

Note : N’utilisez pas de valeurs nombreuses et illimitées (comme orderId ou customerId) comme tags. Chaque valeur de tag crée une nouvelle série temporelle, et donc le système de métriques gonflera très vite. N’exposez pas non plus tous les endpoints actuator sur Internet.

6. Tester avec l’infrastructure réelle via Testcontainers

Quand l’IA peut générer du code rapidement, les tests sont ce qui prouve que le code est correct. Les tests d’intégration s’exécutent sur une vraie base de données, plutôt que sur une base en mémoire d’un dialecte différent, et aident à attraper les erreurs que les mocks ne peuvent pas attraper.

Les dépendances suivantes sont gérées en version par 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>

Entité et repository minimales :

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

Test d’intégration :

@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 et @Container lancent un vrai PostgreSQL dans Docker avant que le test s’exécute.
  • @ServiceConnection (disponible depuis Spring Boot 3.1) injecte automatiquement l’URL, le nom d’utilisateur et le mot de passe du container dans DataSource. Vous n’avez pas besoin de @DynamicPropertySource.
  • La requête findByEmail est générée par Spring Data et s’exécute sur le même type de base de données que la production.

Note : Testcontainers nécessite Docker sur votre machine de dev et sur CI. Les tests sont aussi plus lents que les tests unitaires. Conservez la plupart de la logique dans les tests unitaires purs, utilisez Testcontainers uniquement pour la couche d’intégration comme les repositories, la messagerie et les migrations.

7. Empaqueter et orchestrer les containers

Comprendre comment l’application est empaquetée et exécutée est une compétence minimale lorsque vous travaillez avec Kubernetes ou les plateformes cloud. Un Dockerfile multi-étapes bien structuré montre que vous vous souciez de la taille de l’image et de la sécurité :

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"]
  • L’étape build contient le JDK et le Maven wrapper pour compiler.
  • La dernière étape ne contient que le JRE et le fichier jar, donc l’image est plus petite et a une surface d’attaque réduite.
  • USER 1000 empêche le container de s’exécuter en tant que root, une erreur très courante.

Vous pouvez combiner avec l’endpoint /actuator/health de la section 5 comme sonde liveness/readiness lors du déploiement.

Note : Si votre équipe utilise déjà Buildpacks (par exemple via ./mvnw spring-boot:build-image), vous n’avez pas besoin d’écrire Dockerfile vous-même. L’important est de comprendre ce que contient l’image, quel utilisateur l’exécute et comment la limite de mémoire affecte la JVM.

8. Quand ne pas suivre les tendances

L’investissement en compétences demande aussi de la prudence :

  • Ne négligez pas les fondations. Les structures de données, SQL, HTTP, les transactions, la gestion des erreurs sont toujours posées dans tous les entretiens. L’IA et les frameworks changent beaucoup plus vite que ces connaissances.
  • Ne vous dispersez pas. Choisissez une ou deux compétences liées à votre travail actuel, appliquez-les sur des vrais projets, puis élargissez. Un PR qui ajoute des métriques et des tests d’intégration a plus de valeur que dix certifications non utilisées.
  • N’introduisez pas de technologies juste pour les avoir dans votre CV. Un petit système avec peu d’utilisateurs n’a pas besoin de Kubernetes, de microservices ou de LLM. Savoir dire « ce n’est pas nécessaire » avec une justification est aussi une compétence très appréciée.
  • Ne déléguez pas entièrement aux outils de génération de code. Vous êtes toujours responsable. Lisez, comprenez et écrivez des tests pour chaque morceau de code généré.

9. Conclusion

Le marché IT récompense les devs qui maîtrisent le cycle de vie complet des fonctionnalités : conception, codage, tests, empaquetage et exploitation. Cet article a couvert cinq groupes de compétences à investir :

  • Intégrer le LLM derrière une interface, avec validation et plan de secours.
  • Utiliser les virtual threads pour les workloads I/O-bound.
  • Ajouter l’observabilité avec Micrometer et Actuator.
  • Écrire des tests d’intégration sur l’infrastructure réelle avec Testcontainers.
  • Empaqueter les containers de manière sécurisée.

Chaque compétence a ses limites, et connaître ces limites est tout aussi important que savoir les utiliser. Choisissez un domaine et appliquez-le immédiatement au service sur lequel vous travaillez cette semaine. C’est le moyen le plus rapide de transformer une tendance en expérience réelle.

careertech-trendsjavaspring-bootai