Junior dev à l'ère de l'IA : quoi apprendre pour ne pas être remplacé
1. Vue d’ensemble
Les outils de génération de code peuvent maintenant écrire un controller, une entity ou des tests modèles en quelques secondes. C’est pourquoi beaucoup de nouveaux développeurs craignent que les postes junior ne disparaissent. Cet article ne prédit pas l’avenir. Il se concentre sur une question plus pratique : quand taper du code n’est plus la partie difficile, quelles compétences devriez-vous développer ?
Après la lecture, vous aurez une liste de compétences à prioriser. Chaque compétence est accompagnée d’un exemple Java que vous pouvez exécuter immédiatement. L’article précise aussi quand ne pas confier le travail à l’assistant code.
2. Pourquoi cette question retient l’attention
Autrefois, les juniors recevaient des tâches répétitives comme écrire du CRUD, faire du mapping DTO ou corriger des petits bugs. C’était aussi leur façon d’apprendre le métier. Aujourd’hui, la plupart de ces tâches peuvent être générées automatiquement. Le coût de production du code a baissé considérablement.
Le coût de faire une erreur reste inchangé. Le code généré peut compiler, sembler raisonnable et passer un simple test, mais être faux de manière subtile : monnaie, concurrence, transaction, sécurité. La responsabilité quand le code passe en production, c’est encore vous.
Donc la valeur se déplace vers trois activités :
- Évaluer si le code est correct ou non, et pourquoi.
- Vérifier le comportement par des tests.
- Comprendre le runtime, le framework et le système dans lequel le code s’exécute.
Les sections suivantes parcourent chacune de ces compétences.
3. Lire et relire du code est mieux que d’écrire du code rapidement
La première compétence est de regarder un morceau de code « qui semble correct » et détecter qu’il est faux. Supposons que vous demandiez à l’assistant code d’écrire une fonction pour calculer le total d’une commande et que vous receviez ceci :
import java.util.List;
record OrderLine(String product, double price, int quantity) {}
record Order(List<OrderLine> lines) {
double total() {
return lines.stream()
.mapToDouble(l -> l.price() * l.quantity())
.sum();
}
}
public class Main {
public static void main(String[] args) {
var order = new Order(List.of(
new OrderLine("Stylo", 0.1, 1),
new OrderLine("Cahier", 0.2, 1)));
System.out.println(order.total());
}
}
Le code est court et lisible, mais le résultat est :
0.30000000000000004
double représente les nombres en notation binaire en virgule flottante. Par conséquent, 0.1 et 0.2 ne sont pas stockés avec précision. Avec la monnaie, ces imprécisions s’accumulent au fil de milliers de transactions et deviennent une erreur de rapprochement. La version corrigée utilise BigDecimal :
import java.math.BigDecimal;
import java.util.List;
record OrderLine(String product, BigDecimal price, int quantity) {
BigDecimal subtotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
}
record Order(List<OrderLine> lines) {
BigDecimal total() {
return lines.stream()
.map(OrderLine::subtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Il y a trois points à noter :
pricea le typeBigDecimal. Vous devriez créer la valeur avecnew BigDecimal("0.1"), pasnew BigDecimal(0.1).subtotal()multiplie le prix par la quantité sans passer pardouble.reducecommence à partir deBigDecimal.ZERO, donc une commande vide retourne toujours0au lieu de lever une erreur.
C’est précisément la compétence que les outils ne peuvent pas remplacer. Vous devez savoir poser des questions sur les types de données, les valeurs limites et les cas vides. Lors d’une revue, posez-vous : « Quelle entrée rendrait cette fonction incorrecte ? »
4. Écrire des tests comme une spécification
Sans tests, vous ne savez pas si le code que vous venez de recevoir est correct. Les tests sont aussi le moyen le plus clair de décrire ce que vous voulez. Plus la description est claire, plus le code généré ou écrit par vos collègues sera facile à vérifier.
Avec un projet Spring Boot, vous n’avez besoin que d’une dépendance pour avoir JUnit 5 et AssertJ :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Test pour la classe Order ci-dessus :
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class OrderTest {
@Test
@DisplayName("Le total ajoute correctement les décimales")
void totalIsExactForDecimals() {
var order = new Order(List.of(
new OrderLine("Stylo", new BigDecimal("0.1"), 1),
new OrderLine("Cahier", new BigDecimal("0.2"), 1)));
assertThat(order.total()).isEqualByComparingTo("0.3");
}
@Test
@DisplayName("Une commande vide a un total de zéro")
void emptyOrderTotalIsZero() {
var order = new Order(List.of());
assertThat(order.total()).isEqualByComparingTo(BigDecimal.ZERO);
}
@Test
@DisplayName("Le total se multiplie correctement par la quantité")
void totalMultipliesByQuantity() {
var order = new Order(List.of(
new OrderLine("Livre", new BigDecimal("12.50"), 3)));
assertThat(order.total()).isEqualByComparingTo("37.50");
}
}
Quelques détails à apprendre :
@DisplayNamedocumente la règle métier dans un langage lisible par l’homme. Lire le rapport de test signifie comprendre ce que le système doit faire.isEqualByComparingTocompare par valeur.new BigDecimal("0.3").equals(new BigDecimal("0.30"))retournefalsecar l’échelle est différente. C’est une erreur très courante.- Chaque test vérifie exactement un comportement : le cas normal, le cas vide, la multiplication par la quantité.
Remarque : Ne laissez pas l’outil générer à la fois le code et les tests, puis vous contenter de voir du vert. Si les deux comprennent mal le besoin, le test passera quand même. Vous devriez écrire au moins les tests pour les règles métier importantes vous-même.
5. Comprendre le runtime : la concurrence ne peut pas se deviner
La plupart des bugs graves n’apparaissent que quand plusieurs threads s’exécutent simultanément. Dans Spring, chaque bean est par défaut un singleton, ce qui signifie que chaque requête partage la même instance. Le code suivant semble inoffensif :
import java.util.concurrent.Executors;
class ReportService {
private int processedCount = 0;
void process() {
processedCount++;
}
int processedCount() {
return processedCount;
}
}
public class CounterDemo {
public static void main(String[] args) {
var service = new ReportService();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(service::process);
}
}
System.out.println("Traité : " + service.processedCount());
}
}
Executors.newVirtualThreadPerTaskExecutor() en Java 21 crée un virtual thread pour chaque tâche. Le bloc try-with-resources attend que toutes les tâches se terminent avant de fermer. On s’attend à imprimer 10000, mais en réalité, on voit généralement un nombre plus petit, et à chaque exécution c’est différent :
Traité : 9871
La raison est que processedCount++ n’est pas une opération atomique. Elle consiste en trois étapes : lire, incrémenter et écrire. Deux threads peuvent lire la même valeur, ce qui fait qu’un incrément est perdu. La version corrigée :
import java.util.concurrent.atomic.AtomicInteger;
class ReportService {
private final AtomicInteger processedCount = new AtomicInteger();
void process() {
processedCount.incrementAndGet();
}
int processedCount() {
return processedCount.get();
}
}
Cette fois, le résultat est toujours 10000. La leçon plus large est : un bean singleton ne doit pas maintenir un état mutable par requête. Quand vous lisez un @Service, cherchez les champs non final. C’est généralement là que se cachent les bugs.
Vous n’avez pas besoin de mémoriser chaque API de java.util.concurrent. Mais vous devez comprendre des concepts comme race condition, visibility et lock pour reconnaître un bug quand vous le voyez.
6. Comprendre le framework : savoir comment fonctionne la « magie »
Spring vous aide à écrire moins de code grâce aux annotations, en contrepartie beaucoup de choses se font de manière invisible. Si vous ne comprenez pas le mécanisme sous-jacent, vous serez pris de court. L’exemple classique est l’auto-invocation avec @Transactional :
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
public void payAll(List<Long> paymentIds) {
paymentIds.forEach(this::pay);
}
@Transactional
public void pay(Long paymentId) {
var payment = paymentRepository.findById(paymentId).orElseThrow();
payment.markPaid();
paymentRepository.save(payment);
}
}
À première vue, chaque appel à pay devrait s’exécuter dans une transaction. En réalité, ce n’est pas le cas. Spring applique @Transactional via un proxy qui enveloppe le bean de l’extérieur. Quand payAll appelle this.pay(...), l’appel va directement à l’objet original et contourne le proxy. Donc aucune transaction n’est ouverte.
Il y a deux façons communes de corriger cela :
- Mettre
@Transactionaldirectement sur la méthode appelée de l’extérieur, c’est-à-direpayAll, si vous voulez que le lot entier s’exécute dans une transaction. - Extraire
paydans un bean séparé, par exempleSinglePaymentProcessor, puis l’injecter dansPaymentServicepour que l’appel passe par le proxy.
Comprendre le mécanisme proxy vous aide à expliquer de nombreux autres phénomènes : @Async qui ne s’exécute pas de manière asynchrone, @Cacheable qui ne met pas en cache, ou des annotations sur des méthodes private qui sont ignorées. C’est une connaissance fondamentale qui vous aide à déboguer plus rapidement qu’n’importe quel outil de suggestion.
7. Observer le système plutôt que de deviner
Le code qui fonctionne sur votre machine ne fonctionne pas nécessairement en production. Un junior qui sait lire les logs, les métriques et les checks de santé sera beaucoup plus utile que quelqu’un qui sait seulement écrire des fonctionnalités. Avec Spring Boot, vous commencez par Actuator :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Puis configurez les endpoints dont vous avez besoin :
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
logging:
level:
root: INFO
org.springframework.transaction: DEBUG
La signification de chaque partie :
exposure.includeexpose seulement trois endpoints. Tout exposer avec"*"est une erreur de sécurité courante.show-details: when-authorizedaffiche les détails de la santé seulement pour les utilisateurs authentifiés.- Activez
DEBUGpourorg.springframework.transactionpour voir dans les logs quand une transaction s’ouvre et se valide. C’est le moyen de vérifier directement le bug d’auto-invocation de la section 6.
Vérification rapide :
curl http://localhost:8080/actuator/health
{"status":"UP"}
L’habitude à développer ici est : quand il y a un bug, recueillez des preuves à partir des logs, des métriques et de la stack trace avant de corriger. Ne testez pas aléatoirement des bouts de code suggérés jusqu’à ce que le bug disparaisse magiquement.
8. Utiliser l’assistant code correctement et quand ne pas l’utiliser
L’assistant code est un bon outil. L’éviter ne vous aide pas à progresser davantage. Ce qui compte, c’est que vous gardiez le rôle de décideur. Un processus raisonnable comporte quatre étapes :
- Écrivez d’abord vous-même le besoin et les tests clés, comme à la section 4.
- Demandez à l’outil de proposer la mise en œuvre.
- Lisez chaque ligne. Si vous ne pouvez pas expliquer une ligne, ne la fusionnez pas.
- Exécutez les tests, ajoutez des tests pour les cas limites auxquels vous venez de penser en relisant.
Ne pas confier le travail à l’assistant code, ou être très prudent, dans les cas suivants :
- Code de sécurité : authentification, autorisation, chiffrement, traitement des tokens. Une petite erreur ici peut avoir de graves conséquences, et le code faux ressemble souvent au code correct.
- Quand vous apprenez un nouveau concept. Si l’outil écrit votre premier exercice de concurrence pour vous, vous allez manquer la partie la plus importante de l’apprentissage.
- Données sensibles : ne collez pas de secrets, d’informations clients ou de code interne dans un outil qui n’est pas approuvé par votre entreprise.
- Métier spécifique où l’outil n’a pas le contexte, comme les règles fiscales ou les politiques de réduction propriétaires. Demandez d’abord à la personne qui comprend le métier.
Remarque : Un signe dangereux est quand vous fusionnez le code plus vite que vous ne le comprenez. À ce stade, vous n’êtes plus un ingénieur, vous transférez juste du code.
9. Feuille de route suggérée pour un junior
Vous n’avez pas besoin d’apprendre tout à la fois. Un ordre raisonnable serait :
- Fondamentaux du langage : types de données, collections,
equals/hashCode, exceptions, concurrence de base. - Tests : JUnit 5, AssertJ, comment nommer les tests selon le comportement, tester les cas limites.
- Framework de l’intérieur : injection de dépendance, cycle de vie du bean, proxy, transaction.
- Données : SQL réel, index, isolation de transaction, bug N+1 query avec JPA.
- Exploitation : lire les logs, métriques, health check, déboguer sur un environnement proche de la production.
- Communication : transformer des besoins flous en critères d’acceptation clairs, écrire des descriptions de pull request faciles à relire.
La dernière compétence est souvent sous-estimée. Quelqu’un qui sait poser les bonnes questions, comme « et si la commande est vide ? » ou « et si deux requêtes arrivent en même temps ? », sera toujours utile, peu importe qui écrit le code ou quel outil le génère.
10. Conclusion
Dans cet article, nous avons vu que la valeur d’un junior dev se déplace de la vitesse d’écriture de code vers la capacité d’évaluer, de vérifier et de comprendre les systèmes. Les exemples illustrent des bugs que le code « correct » peut toujours contenir : erreur de précision de double avec la monnaie, race condition dans un bean singleton, et @Transactional contourné par auto-invocation. Vous avez aussi vu comment utiliser les tests et Actuator pour vérifier plutôt que de deviner.
Utilisez l’assistant code comme un collègue qui écrit vite mais se trompe souvent. Vous tirez parti de sa rapidité, mais relisez toujours, testez et comprenez chaque ligne avant de fusionner. C’est comme ça que vous ne serez pas remplacé et que vous deviendrez quelqu’un que l’équipe ne peut pas se permettre de perdre.