1Plateforme Java & versions23
JVM, JRE et JDKLa JVM exécute le bytecode ; le JRE = JVM + bibliothèques standard ; le JDK = JRE + outils de développement (javac, jshell, jdb…).
La JVM exécute le bytecode ; le JRE = JVM + bibliothèques standard ; le JDK = JRE + outils de développement (javac, jshell, jdb…).
La JVM (Java Virtual Machine) est un programme qui charge les fichiers .class, vérifie leur bytecode, gère la mémoire (heap, GC) et exécute le code. Le JRE (Java Runtime Environment) ajoute les bibliothèques standard (java.util, java.net…) : c'est ce qu'il faut pour *lancer* une application. Le JDK (Java Development Kit) ajoute le compilateur javac et les outils : c'est ce qu'il faut pour *développer*. Depuis Java 11, on ne distribue plus de JRE séparé : on télécharge un JDK (Temurin, Corretto, Oracle…).
javac Hello.java # le compilateur (JDK) produit Hello.class (bytecode)
java Hello # la JVM charge et exécute le bytecode
java -version # ex. openjdk version "21.0.4" 2024-07-16 LTSBytecode et compilation JITLe bytecode est le code intermédiaire produit par javac ; le JIT le compile en code machine pendant l'exécution pour les parties les plus utilisées.
Le bytecode est le code intermédiaire produit par javac ; le JIT le compile en code machine pendant l'exécution pour les parties les plus utilisées.
javac ne produit pas de code machine mais du bytecode, un jeu d'instructions indépendant du processeur. Au démarrage, la JVM l'interprète ; quand une méthode devient « chaude » (appelée très souvent), le compilateur JIT (Just-In-Time, C1 puis C2) la compile en code machine optimisé. C'est pour cela qu'une application Java est plus lente les premières secondes (« warm-up ») puis très rapide.
javap -c Hello.class # affiche le bytecode d'une classe
# 0: getstatic #7 // Field java/lang/System.out
# 3: ldc #13 // String Bonjour
# 5: invokevirtual // Method printlnLTS (Long-Term Support)Version Java maintenue plusieurs années (correctifs de sécurité) ; aujourd'hui 8, 11, 17, 21 et 25. À privilégier en production.
Version Java maintenue plusieurs années (correctifs de sécurité) ; aujourd'hui 8, 11, 17, 21 et 25. À privilégier en production.
Une nouvelle version de Java sort tous les 6 mois (mars et septembre). Les versions intermédiaires ne sont supportées que 6 mois ; une LTS sort tous les 2 ans et reçoit des correctifs pendant des années. En entreprise (banque, assurance), on reste sur les LTS : 17 et 21 sont les plus déployées, 25 (septembre 2025) est la plus récente.
| Version | Année | Apport phare |
|---|---|---|
| 8 | 2014 | Lambdas, Streams, Optional |
| 11 | 2018 | var en lambda, HttpClient |
| 17 | 2021 | Records, Sealed classes |
| 21 | 2023 | Virtual Threads, pattern matching switch |
| 25 | 2025 | Scoped Values, constructeurs flexibles |
var (inférence de type locale)Mot-clé (Java 10) qui laisse le compilateur déduire le type d'une variable locale à partir de sa valeur. Le typage reste statique.
Mot-clé (Java 10) qui laisse le compilateur déduire le type d'une variable locale à partir de sa valeur. Le typage reste statique.
var ne rend pas Java dynamique : le type est fixé à la compilation, il est juste écrit par le compilateur à ta place. Utilisable uniquement pour les variables locales initialisées (pas pour les champs, paramètres ou retours).
var clients = new ArrayList<Client>(); // type = ArrayList<Client>
var total = BigDecimal.ZERO; // type = BigDecimal
// clients = "texte"; ❌ erreur de compilation : le type reste ArrayList<Client>
for (var c : clients) { total = total.add(c.solde()); }var x = service.process(); nuit à la lisibilité.HttpClient (Java 11)Client HTTP standard du JDK (java.net.http), synchrone ou asynchrone, compatible HTTP/2.
Client HTTP standard du JDK (java.net.http), synchrone ou asynchrone, compatible HTTP/2.
Avant Java 11, il fallait HttpURLConnection (verbeux) ou une bibliothèque externe. HttpClient est immuable, réutilisable et thread-safe ; il supporte HTTP/2 et les appels asynchrones via CompletableFuture. Dans une application Spring, on utilise plutôt RestClient, qui peut d'ailleurs s'appuyer dessus.
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.exemple.sn/taux"))
.header("Accept", "application/json").GET().build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());Lambda et interface fonctionnelleUne lambda est une fonction anonyme (params) -> corps ; elle implémente une interface fonctionnelle, c'est-à-dire une interface à une seule méthode abstraite.
Une lambda est une fonction anonyme (params) -> corps ; elle implémente une interface fonctionnelle, c'est-à-dire une interface à une seule méthode abstraite.
Depuis Java 8, on peut passer du comportement comme une valeur. Une interface fonctionnelle possède exactement une méthode abstraite (ex. Runnable, Comparator<T>, Function<T,R>, Predicate<T>, Supplier<T>, Consumer<T>). L'annotation @FunctionalInterface fait vérifier cette règle par le compilateur.
@FunctionalInterface
interface RegleFrais { BigDecimal calculer(BigDecimal montant); }
RegleFrais fraisFixes = m -> new BigDecimal("500");
RegleFrais pourcent = m -> m.multiply(new BigDecimal("0.01"));
Predicate<Client> majeur = c -> c.age() >= 18;
Function<Client, String> nom = c -> c.nom().toUpperCase();Référence de méthodeRaccourci Classe::methode pour une lambda qui ne fait qu'appeler une méthode existante.
Raccourci Classe::methode pour une lambda qui ne fait qu'appeler une méthode existante.
Quatre formes : méthode statique (Integer::parseInt), méthode d'instance d'un objet donné (System.out::println), méthode d'instance du paramètre (Order::getCustomerName), constructeur (ArrayList::new).
orders.stream().map(Order::getCustomerName) // = o -> o.getCustomerName()
.forEach(System.out::println); // = s -> System.out.println(s)
Supplier<List<String>> fabrique = ArrayList::new; // = () -> new ArrayList<>()OptionalConteneur (Java 8) qui contient une valeur ou rien ; il rend explicite qu'un résultat peut être absent et évite les NullPointerException.
Conteneur (Java 8) qui contient une valeur ou rien ; il rend explicite qu'un résultat peut être absent et évite les NullPointerException.
Optional<T> sert surtout comme type de retour (ex. findById d'un repository). On le consomme avec map, orElse, orElseThrow, ifPresent plutôt qu'avec get().
Optional<Client> c = repo.findById(id);
String email = c.map(Client::email).orElse("inconnu");
Client client = repo.findById(id)
.orElseThrow(() -> new NotFoundException("Client " + id + " introuvable"));get() sans vérifier : on retombe sur le problème du null.Méthode default (interface)Méthode avec un corps dans une interface (Java 8) : elle permet d'ajouter un comportement sans casser les classes qui implémentent déjà l'interface.
Méthode avec un corps dans une interface (Java 8) : elle permet d'ajouter un comportement sans casser les classes qui implémentent déjà l'interface.
Avant Java 8, ajouter une méthode à une interface obligeait à modifier toutes ses implémentations. Une méthode default fournit une implémentation par défaut, que les classes peuvent redéfinir. Les interfaces peuvent aussi avoir des méthodes static et (Java 9) private.
interface Notifiable {
void envoyer(String message);
default void envoyerUrgent(String message) { // implémentation fournie
envoyer("[URGENT] " + message);
}
}RecordClasse de données immuable (Java 16, LTS 17) : le compilateur génère constructeur, accesseurs, equals, hashCode et toString.
Classe de données immuable (Java 16, LTS 17) : le compilateur génère constructeur, accesseurs, equals, hashCode et toString.
Un record déclare ses composants dans l'en-tête ; tous les champs sont private final. Idéal pour les DTO, les messages Kafka, les clés composées, les value objects. On peut ajouter un constructeur compact pour valider les données.
public record VirementDto(String ibanSource, String ibanDest, BigDecimal montant) {
public VirementDto { // constructeur compact
if (montant.signum() <= 0) throw new IllegalArgumentException("montant > 0 requis");
}
}
var v = new VirementDto("SN01…", "SN02…", new BigDecimal("15000"));
v.montant(); // accesseur généré (pas getMontant())Sealed class / interfaceHiérarchie fermée (Java 17) : sealed ... permits A, B liste exhaustivement les seuls sous-types autorisés.
Hiérarchie fermée (Java 17) : sealed ... permits A, B liste exhaustivement les seuls sous-types autorisés.
Une classe ou interface sealed contrôle qui peut l'étendre. Chaque sous-type doit être final, sealed ou non-sealed. Avantage majeur : le compilateur connaît tous les cas, donc un switch sur ce type peut être exhaustif sans default.
public sealed interface ResultatPaiement permits Accepte, Refuse, EnAttente {}
public record Accepte(String reference) implements ResultatPaiement {}
public record Refuse(String motif) implements ResultatPaiement {}
public record EnAttente() implements ResultatPaiement {}Pattern matching pour instanceofJava 16 : if (obj instanceof Client c) teste le type et déclare directement la variable typée, sans cast manuel.
Java 16 : if (obj instanceof Client c) teste le type et déclare directement la variable typée, sans cast manuel.
// Avant
if (obj instanceof Client) { Client c = (Client) obj; System.out.println(c.nom()); }
// Depuis Java 16
if (obj instanceof Client c && c.estActif()) { System.out.println(c.nom()); }Pattern matching pour switch et Record PatternsJava 21 : un switch peut tester le type d'un objet et déstructurer un record (case Cercle(var r) ->), avec contrôle d'exhaustivité.
Java 21 : un switch peut tester le type d'un objet et déstructurer un record (case Cercle(var r) ->), avec contrôle d'exhaustivité.
Combiné aux sealed interfaces, c'est le remplaçant moderne des chaînes de if/else instanceof et du pattern Visitor. Les record patterns extraient directement les composants d'un record. Les gardes when ajoutent une condition.
String message = switch (resultat) {
case Accepte(var ref) -> "Paiement OK, réf. " + ref;
case Refuse(var motif) when motif.isBlank() -> "Refusé";
case Refuse(var motif) -> "Refusé : " + motif;
case EnAttente e -> "En attente de validation";
}; // pas de default : le compilateur sait que tous les cas sont couvertsSequenced Collections (Java 21)Nouvelles interfaces qui donnent à toutes les collections ordonnées des méthodes communes : getFirst(), getLast(), addFirst(), reversed().
Nouvelles interfaces qui donnent à toutes les collections ordonnées des méthodes communes : getFirst(), getLast(), addFirst(), reversed().
Avant Java 21, récupérer le dernier élément dépendait du type (list.get(list.size()-1), deque.getLast(), sortedSet.last()…). SequencedCollection, SequencedSet et SequencedMap uniformisent cela.
List<String> etapes = new ArrayList<>(List.of("créé", "validé", "payé"));
etapes.getFirst(); // "créé"
etapes.getLast(); // "payé"
etapes.reversed(); // vue inversée, sans copie
LinkedHashMap<String, Integer> m = new LinkedHashMap<>();
m.putFirst("prioritaire", 1);Virtual Threads (Java 21)Threads ultra-légers gérés par la JVM (et non par l'OS) : on peut en créer des millions ; parfaits pour les traitements qui attendent des I/O.
Threads ultra-légers gérés par la JVM (et non par l'OS) : on peut en créer des millions ; parfaits pour les traitements qui attendent des I/O.
Un thread classique (« platform thread ») correspond à un thread de l'OS : il coûte ~1 Mo de pile et on n'en crée que quelques milliers. Un virtual thread est monté sur un thread porteur uniquement quand il calcule ; dès qu'il attend (appel HTTP, requête SQL), la JVM le démonte et libère le porteur. On garde un code bloquant simple tout en ayant la scalabilité du code réactif.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> appelerApiExterne(id)); // 10 000 appels concurrents
}
} // close() attend la fin de toutes les tâches
Thread.ofVirtual().start(() -> System.out.println("léger !"));# Spring Boot 3.2+ : Tomcat et @Async utilisent des virtual threads
spring.threads.virtual.enabled=truesynchronized qui « épinglent » le thread porteur (corrigé en grande partie depuis Java 24).Flexible Constructor Bodies (JEP 513, Java 25)Java 25 autorise des instructions (validation, calculs) avant l'appel à super(...) ou this(...) dans un constructeur.
Java 25 autorise des instructions (validation, calculs) avant l'appel à super(...) ou this(...) dans un constructeur.
Avant, super(...) devait être la toute première instruction, ce qui obligeait à valider les arguments *après* la construction du parent, ou via des méthodes statiques. Désormais on peut valider d'abord, à condition de ne pas utiliser this avant super.
public class CompteEpargne extends Compte {
public CompteEpargne(BigDecimal soldeInitial) {
if (soldeInitial.signum() < 0)
throw new IllegalArgumentException("Solde négatif"); // ✅ avant super() en Java 25
super(soldeInitial);
}
}Scoped Values (JEP 506, Java 25)Valeur immuable partagée avec tout le code appelé dans une portée (et ses threads enfants) ; alternative moderne et sûre à ThreadLocal, adaptée aux virtual threads.
Valeur immuable partagée avec tout le code appelé dans une portée (et ses threads enfants) ; alternative moderne et sûre à ThreadLocal, adaptée aux virtual threads.
ThreadLocal est mutable, doit être nettoyé à la main et coûte cher avec des millions de virtual threads. Une ScopedValue est liée pour la durée d'un appel puis disparaît automatiquement. Usage typique : propager l'utilisateur courant ou un identifiant de corrélation.
static final ScopedValue<String> UTILISATEUR = ScopedValue.newInstance();
ScopedValue.where(UTILISATEUR, "abdoulaye").run(() -> traiterRequete());
void traiterRequete() {
log.info("Action de {}", UTILISATEUR.get()); // lisible partout dans la portée
}Compact source files et main d'instance (JEP 512, Java 25)Java 25 permet d'écrire un programme sans déclarer de classe et avec un simple void main(), idéal pour les scripts et l'apprentissage.
Java 25 permet d'écrire un programme sans déclarer de classe et avec un simple void main(), idéal pour les scripts et l'apprentissage.
// Fichier Hello.java — lancé avec : java Hello.java
void main() {
IO.println("Bonjour Dakar !"); // classe utilitaire java.lang.IO (Java 25)
}Module Import Declarations (JEP 511, Java 25)import module java.base; importe d'un coup tous les packages exportés par un module.
import module java.base; importe d'un coup tous les packages exportés par un module.
Pratique pour les scripts et prototypes : une seule ligne remplace des dizaines d'imports (java.util.*, java.io.*…). En code de production, les imports explicites restent plus lisibles.
import module java.base; // List, Map, Path, Files, Stream… tous disponiblesCompact Object Headers (JEP 519, Java 25)Option de la JVM (finale en Java 25) qui réduit l'en-tête de chaque objet de 12 à 8 octets : moins de mémoire et meilleure utilisation du cache CPU pour les applications qui créent beaucoup de petits objets.
Option de la JVM (finale en Java 25) qui réduit l'en-tête de chaque objet de 12 à 8 octets : moins de mémoire et meilleure utilisation du cache CPU pour les applications qui créent beaucoup de petits objets.
Chaque objet Java porte un en-tête (verrou, hash, classe). En le compactant, le heap occupé baisse souvent de 10 à 20 %. Ce n'est pas activé par défaut en Java 25.
java -XX:+UseCompactObjectHeaders -jar app.jarPrimitive Patterns (JEP 507, preview en Java 25)Fonctionnalité en preview : autorise les types primitifs (int, double…) dans les patterns de instanceof et switch.
Fonctionnalité en preview : autorise les types primitifs (int, double…) dans les patterns de instanceof et switch.
« Preview » signifie que la fonctionnalité est livrée pour retour d'expérience : il faut --enable-preview et l'API peut encore changer. À citer comme tendance, pas à utiliser en production.
// javac --release 25 --enable-preview
String categorie = switch (codeHttp) {
case int c when c >= 500 -> "Erreur serveur";
case int c when c >= 400 -> "Erreur client";
case int c -> "OK";
};Structured Concurrency (preview)API en preview (JEP 505 en Java 25) qui traite un groupe de tâches concurrentes comme une seule unité : si une échoue, les autres sont annulées.
API en preview (JEP 505 en Java 25) qui traite un groupe de tâches concurrentes comme une seule unité : si une échoue, les autres sont annulées.
Avec un ExecutorService classique, si une sous-tâche échoue, les autres continuent et peuvent fuir. StructuredTaskScope garantit que les sous-tâches ne survivent pas à leur portée. Encore en preview en Java 25 : bon sujet de culture, pas pour la production.
try (var scope = StructuredTaskScope.open()) { // API preview Java 25
var client = scope.fork(() -> clientService.charger(id));
var comptes = scope.fork(() -> compteService.lister(id));
scope.join(); // attend les deux ou échoue
return new Synthese(client.get(), comptes.get());
}Text blocksChaînes multilignes délimitées par """ (Java 15) : idéales pour du JSON, du SQL ou du HTML dans le code.
Chaînes multilignes délimitées par """ (Java 15) : idéales pour du JSON, du SQL ou du HTML dans le code.
String sql = """
SELECT c.id, c.nom
FROM client c
WHERE c.statut = 'ACTIF'
""";2Programmation orientée objet16
Classe et objetUne classe est un modèle (attributs + méthodes) ; un objet est une instance concrète de ce modèle, créée avec new et stockée dans le heap.
Une classe est un modèle (attributs + méthodes) ; un objet est une instance concrète de ce modèle, créée avec new et stockée dans le heap.
public class Compte { // la classe : le plan
private BigDecimal solde = BigDecimal.ZERO;
public void crediter(BigDecimal m) { solde = solde.add(m); }
}
Compte c1 = new Compte(); // un objet (instance)
Compte c2 = new Compte(); // un autre objet, état indépendantEncapsulationCacher l'état interne d'un objet (champs private) et n'exposer que des méthodes qui garantissent ses règles (invariants).
Cacher l'état interne d'un objet (champs private) et n'exposer que des méthodes qui garantissent ses règles (invariants).
L'encapsulation protège la cohérence : personne ne peut mettre un solde négatif « à la main » si la seule porte d'entrée est une méthode qui vérifie la règle. Ce n'est pas « des getters/setters partout » : un setter public sur chaque champ casse l'encapsulation.
public class Compte {
private BigDecimal solde = BigDecimal.ZERO; // état caché
public void debiter(BigDecimal montant) {
if (montant.compareTo(solde) > 0) throw new SoldeInsuffisantException();
solde = solde.subtract(montant); // l'invariant solde >= 0 est garanti
}
public BigDecimal solde() { return solde; } // lecture seule
}HéritageUne classe fille (extends) réutilise et spécialise le code de sa classe mère ; super appelle le constructeur ou les méthodes du parent.
Une classe fille (extends) réutilise et spécialise le code de sa classe mère ; super appelle le constructeur ou les méthodes du parent.
L'héritage exprime une relation « est un » (un CompteEpargne *est un* Compte). Java n'autorise qu'un seul parent de classe. Utilisé avec excès, il crée des hiérarchies fragiles : une modification du parent casse les enfants.
public class Compte {
protected BigDecimal solde;
public Compte(BigDecimal solde) { this.solde = solde; }
public BigDecimal frais() { return new BigDecimal("1000"); }
}
public class CompteEpargne extends Compte {
public CompteEpargne(BigDecimal s) { super(s); }
@Override public BigDecimal frais() { return BigDecimal.ZERO; } // spécialisation
}CompositionConstruire un objet en y plaçant d'autres objets (relation « a un ») au lieu d'hériter ; plus souple et plus testable que l'héritage.
Construire un objet en y plaçant d'autres objets (relation « a un ») au lieu d'hériter ; plus souple et plus testable que l'héritage.
Avec la composition, on assemble des comportements via des champs (souvent des interfaces injectées). On peut changer une partie sans toucher aux autres et la remplacer par un mock en test. C'est le principe « favor composition over inheritance » du livre Design Patterns.
public class ServiceVirement {
private final CalculFrais calculFrais; // "a un" calculateur de frais
private final Notificateur notificateur; // "a un" notificateur
public ServiceVirement(CalculFrais f, Notificateur n) { this.calculFrais = f; this.notificateur = n; }
}PolymorphismeCapacité d'un même appel à produire un comportement différent selon le type réel de l'objet (résolu à l'exécution).
Capacité d'un même appel à produire un comportement différent selon le type réel de l'objet (résolu à l'exécution).
On manipule des objets via un type commun (interface ou classe mère), et c'est l'implémentation concrète qui s'exécute. C'est ce qui permet d'ajouter un nouveau cas sans modifier le code appelant (principe Open/Closed).
List<MoyenPaiement> moyens = List.of(new OrangeMoney(), new Wave(), new CarteBancaire());
for (MoyenPaiement m : moyens) {
m.payer(montant); // chaque classe exécute SA version de payer()
}Override vs OverloadOverride (redéfinition) = même signature dans une sous-classe, choisie à l'exécution. Overload (surcharge) = même nom, paramètres différents, choisie à la compilation.
Override (redéfinition) = même signature dans une sous-classe, choisie à l'exécution. Overload (surcharge) = même nom, paramètres différents, choisie à la compilation.
| Override | Overload | |
|---|---|---|
| Où | Classe fille | Même classe |
| Signature | Identique | Paramètres différents |
| Résolution | Exécution (dynamique) | Compilation (statique) |
class Compte { BigDecimal frais() { return BigDecimal.TEN; } }
class Premium extends Compte {
@Override BigDecimal frais() { return BigDecimal.ZERO; } // override
}
class Calcul {
int somme(int a, int b) { return a + b; } // overload
BigDecimal somme(BigDecimal a, BigDecimal b) { return a.add(b); }
}@Override : le compilateur signale une erreur si la signature ne correspond pas (faute de frappe, mauvais type).AbstractionNe montrer que l'essentiel (le « quoi ») et cacher les détails d'implémentation (le « comment »), via interfaces et classes abstraites.
Ne montrer que l'essentiel (le « quoi ») et cacher les détails d'implémentation (le « comment »), via interfaces et classes abstraites.
Le code appelant dépend d'un contrat (PaymentGateway.charger()) sans savoir s'il parle à Wave, Orange Money ou à un mock. Cela réduit le couplage et permet de changer d'implémentation sans impact.
public interface PaymentGateway { // le "quoi"
Confirmation charger(String telephone, BigDecimal montant);
}
@Service class WaveGateway implements PaymentGateway { /* le "comment" : appels HTTP Wave */ }InterfaceContrat qui déclare des méthodes à implémenter ; une classe peut implémenter plusieurs interfaces. Peut contenir des méthodes default, static et private.
Contrat qui déclare des méthodes à implémenter ; une classe peut implémenter plusieurs interfaces. Peut contenir des méthodes default, static et private.
Une interface n'a pas d'état d'instance (seulement des constantes public static final). Elle définit un comportement attendu, ce qui permet le polymorphisme et l'injection de dépendances (Spring injecte l'implémentation).
public interface Auditable { String auteur(); Instant dateModification(); }
public interface Exportable { byte[] versPdf(); }
public class Contrat implements Auditable, Exportable { /* ... */ } // multi-implémentationClasse abstraiteClasse non instanciable (abstract) qui partage état et code entre sous-classes et impose certaines méthodes à implémenter.
Classe non instanciable (abstract) qui partage état et code entre sous-classes et impose certaines méthodes à implémenter.
On la choisit quand plusieurs classes ont un vrai socle commun (champs, constructeur, logique partielle). Pattern typique : Template Method, où la classe abstraite fixe l'algorithme et délègue certaines étapes.
public abstract class Rapport {
protected final LocalDate date = LocalDate.now(); // état partagé
public final String generer() { // algorithme fixé
return entete() + corps() + "\nGénéré le " + date;
}
protected String entete() { return "== RAPPORT ==\n"; }
protected abstract String corps(); // étape imposée
}equals() et hashCode()equals() définit l'égalité logique de deux objets ; hashCode() donne un entier utilisé par les HashMap/HashSet. Contrat : deux objets égaux doivent avoir le même hashCode.
equals() définit l'égalité logique de deux objets ; hashCode() donne un entier utilisé par les HashMap/HashSet. Contrat : deux objets égaux doivent avoir le même hashCode.
Si on redéfinit equals() sans hashCode(), un objet ajouté dans un HashSet peut devenir « introuvable » : la recherche va dans le mauvais bucket. Les records génèrent les deux automatiquement.
public final class Iban {
private final String valeur;
public Iban(String v) { this.valeur = v.replace(" ", ""); }
@Override public boolean equals(Object o) {
return o instanceof Iban other && valeur.equals(other.valeur);
}
@Override public int hashCode() { return valeur.hashCode(); }
}== vs equals()== compare les références (même objet en mémoire ?) ; equals() compare le contenu (même valeur logique ?).
== compare les références (même objet en mémoire ?) ; equals() compare le contenu (même valeur logique ?).
String a = "OK";
String b = "OK";
String c = new String("OK");
a == b; // true : même littéral, partagé dans le String Pool
a == c; // false : c est un autre objet
a.equals(c); // true : même contenu
Integer x = 127, y = 127; x == y; // true (cache -128..127)
Integer p = 128, q = 128; p == q; // false (deux objets) → toujours equals()Integer entre -128 et 127 fait passer certains tests avec == puis échouer en production : toujours equals() pour les objets.ImmuabilitéUn objet immuable ne change plus après sa création ; toute « modification » crée un nouvel objet. Il est thread-safe par nature.
Un objet immuable ne change plus après sa création ; toute « modification » crée un nouvel objet. Il est thread-safe par nature.
Recette : classe final, champs private final, pas de setter, copies défensives des collections reçues. Exemples du JDK : String, BigDecimal, LocalDate, List.of(...). Les records sont immuables (en surface).
public final class Montant {
private final BigDecimal valeur;
private final String devise;
public Montant(BigDecimal v, String d) { this.valeur = v; this.devise = d; }
public Montant ajouter(Montant autre) { // retourne un NOUVEL objet
return new Montant(valeur.add(autre.valeur), devise);
}
}String PoolZone de la JVM où les littéraux String sont stockés une seule fois et partagés, ce qui économise la mémoire. Possible uniquement parce que String est immuable.
Zone de la JVM où les littéraux String sont stockés une seule fois et partagés, ce qui économise la mémoire. Possible uniquement parce que String est immuable.
Quand on écrit "XOF" à plusieurs endroits, tous pointent vers le même objet. new String("XOF") force la création d'un nouvel objet hors du pool. intern() permet d'y placer une chaîne manuellement.
String d1 = "XOF";
String d2 = "XOF";
System.out.println(d1 == d2); // true : même objet du pool
System.out.println(new String("XOF") == d1); // falseStringBuilder vs StringBufferDeux classes de chaîne mutable : StringBuilder (rapide, non thread-safe) est le choix par défaut ; StringBuffer (synchronized) est historique.
Deux classes de chaîne mutable : StringBuilder (rapide, non thread-safe) est le choix par défaut ; StringBuffer (synchronized) est historique.
Comme String est immuable, s += x dans une boucle crée un nouvel objet à chaque tour (coût O(n²)). StringBuilder modifie un tampon interne (O(n)).
var sb = new StringBuilder();
for (Transaction t : transactions) {
sb.append(t.reference()).append(';').append(t.montant()).append('\n');
}
String csv = sb.toString();a + b + c sur une seule ligne ; le problème n'existe que dans les boucles.finalMot-clé : variable non réassignable, méthode non redéfinissable, classe non héritable.
Mot-clé : variable non réassignable, méthode non redéfinissable, classe non héritable.
final sur un champ ne rend pas l'objet référencé immuable : une final List peut toujours recevoir des éléments. En Spring, les dépendances injectées par constructeur sont déclarées final.
private final OrderRepo repo; // assignée une seule fois (constructeur)
public final String generer() {...} // ne peut pas être @Override
public final class Iban {...} // ne peut pas être étendue
final List<String> l = new ArrayList<>(); l.add("ok"); // ✅ autorisé : la référence est finale, pas le contenustaticMembre qui appartient à la classe et non à une instance : partagé par tous les objets, accessible sans new.
Membre qui appartient à la classe et non à une instance : partagé par tous les objets, accessible sans new.
Utilisé pour les constantes (static final), les méthodes utilitaires et les fabriques. Un champ static mutable est un état global partagé entre threads : source fréquente de bugs de concurrence.
public final class Devises {
public static final String XOF = "XOF"; // constante
public static BigDecimal arrondir(BigDecimal m) { // utilitaire
return m.setScale(0, RoundingMode.HALF_UP);
}
}
Devises.arrondir(new BigDecimal("1250.6")); // 12513Collections10
Complexité algorithmique (notation Big O)Mesure de l'évolution du temps d'un algorithme quand la taille n des données augmente : O(1) constant, O(log n) logarithmique, O(n) linéaire, O(n²) quadratique.
Mesure de l'évolution du temps d'un algorithme quand la taille n des données augmente : O(1) constant, O(log n) logarithmique, O(n) linéaire, O(n²) quadratique.
On s'intéresse au pire cas ou au cas moyen pour de grandes valeurs de n. Amorti signifie « en moyenne sur une série d'opérations » : un ArrayList.add est O(1) amorti car le redimensionnement (O(n)) est rare.
| Notation | Exemple | 1 000 éléments | 1 000 000 éléments |
|---|---|---|---|
| O(1) | HashMap.get | 1 étape | 1 étape |
| O(log n) | TreeMap.get | ~10 | ~20 |
| O(n) | parcours de liste | 1 000 | 1 000 000 |
| O(n²) | double boucle | 1 000 000 | 10¹² |
ArrayListListe basée sur un tableau redimensionnable : accès par index en O(1), ajout en fin en O(1) amorti, insertion/suppression au début en O(n).
Liste basée sur un tableau redimensionnable : accès par index en O(1), ajout en fin en O(1) amorti, insertion/suppression au début en O(n).
C'est la liste par défaut dans 95 % des cas. Quand le tableau interne est plein, il est recopié dans un tableau 1,5 fois plus grand.
List<String> statuts = new ArrayList<>(100); // capacité initiale si la taille est connue
statuts.add("CREE");
statuts.get(0); // O(1)
statuts.add(0, "BROUILLON"); // O(n) : décale tous les élémentsLinkedListListe doublement chaînée : insertion/suppression O(1) aux extrémités, mais accès par index en O(n). Rarement le bon choix en pratique.
Liste doublement chaînée : insertion/suppression O(1) aux extrémités, mais accès par index en O(n). Rarement le bon choix en pratique.
Chaque élément est un nœud avec des pointeurs vers le précédent et le suivant : beaucoup d'objets, mauvaise localité mémoire. Pour une file ou une pile, ArrayDeque est presque toujours plus rapide.
LinkedList<String> l = new LinkedList<>();
l.addFirst("a"); l.addLast("b"); // O(1)
l.get(5000); // O(n) : parcourt les nœuds un par unHashMapTable de hachage clé → valeur : get/put en O(1) amorti grâce à hashCode() qui désigne directement le « bucket ». Non ordonnée, non thread-safe.
Table de hachage clé → valeur : get/put en O(1) amorti grâce à hashCode() qui désigne directement le « bucket ». Non ordonnée, non thread-safe.
La clé passe par hashCode() pour choisir un bucket, puis equals() départage les collisions. Depuis Java 8, un bucket qui contient plus de 8 éléments devient un arbre rouge-noir (pire cas O(log n) au lieu de O(n)). Capacité doublée quand le facteur de charge 0,75 est dépassé.
Map<String, BigDecimal> soldes = new HashMap<>();
soldes.put("SN-001", new BigDecimal("250000"));
soldes.getOrDefault("SN-999", BigDecimal.ZERO);
soldes.merge("SN-001", new BigDecimal("5000"), BigDecimal::add); // incrément atomique logiqueTreeMapMap triée par clé (arbre rouge-noir) : opérations en O(log n), parcours dans l'ordre et requêtes par plage (avant/après une clé).
Map triée par clé (arbre rouge-noir) : opérations en O(log n), parcours dans l'ordre et requêtes par plage (avant/après une clé).
TreeMap<LocalDate, BigDecimal> historique = new TreeMap<>();
historique.put(LocalDate.of(2026, 1, 1), new BigDecimal("1000"));
historique.put(LocalDate.of(2026, 3, 1), new BigDecimal("1500"));
historique.floorEntry(LocalDate.of(2026, 2, 15)); // dernière valeur connue au 15/02
historique.subMap(debut, true, fin, true); // plage de datesHashSetEnsemble sans doublons basé sur une HashMap : add/contains en O(1) amorti, sans ordre garanti.
Ensemble sans doublons basé sur une HashMap : add/contains en O(1) amorti, sans ordre garanti.
Set<String> ibansVus = new HashSet<>();
if (!ibansVus.add(iban)) { // add() renvoie false si déjà présent
log.warn("IBAN en double : {}", iban);
}LinkedHashSet ; besoin d'un tri → TreeSet.PriorityQueueFile à priorité (tas binaire) : l'élément le plus prioritaire est toujours en tête ; lecture O(1), ajout/retrait O(log n).
File à priorité (tas binaire) : l'élément le plus prioritaire est toujours en tête ; lecture O(1), ajout/retrait O(log n).
PriorityQueue<Tache> taches = new PriorityQueue<>(Comparator.comparing(Tache::echeance));
taches.add(new Tache("relance", LocalDate.now().plusDays(3)));
taches.add(new Tache("clôture", LocalDate.now()));
taches.poll(); // "clôture" : l'échéance la plus proche sort en premierArrayDequeFile à double extrémité basée sur un tableau circulaire : ajout/retrait O(1) aux deux bouts. Le meilleur choix pour une pile ou une file.
File à double extrémité basée sur un tableau circulaire : ajout/retrait O(1) aux deux bouts. Le meilleur choix pour une pile ou une file.
Deque<String> pile = new ArrayDeque<>();
pile.push("A"); pile.push("B");
pile.pop(); // "B" (LIFO) — remplace la vieille classe Stack
Deque<String> file = new ArrayDeque<>();
file.offer("1"); file.offer("2");
file.poll(); // "1" (FIFO)Collections immuables (List.of, Map.of)List.of(), Set.of(), Map.of() (Java 9) créent des collections non modifiables : toute tentative d'ajout lève UnsupportedOperationException.
List.of(), Set.of(), Map.of() (Java 9) créent des collections non modifiables : toute tentative d'ajout lève UnsupportedOperationException.
List<String> devises = List.of("XOF", "EUR", "USD");
devises.add("GBP"); // ❌ UnsupportedOperationException
List<String> copie = List.copyOf(source); // copie défensive immuablenull : List.of("a", null) lève une NullPointerException.Comparable vs ComparatorComparable définit l'ordre « naturel » dans la classe elle-même (compareTo) ; Comparator définit un ordre externe, à la demande.
Comparable définit l'ordre « naturel » dans la classe elle-même (compareTo) ; Comparator définit un ordre externe, à la demande.
record Client(String nom, int age) implements Comparable<Client> {
public int compareTo(Client o) { return nom.compareTo(o.nom); } // ordre naturel : par nom
}
clients.sort(Comparator.comparing(Client::age).reversed()
.thenComparing(Client::nom)); // ordre ad hoc4Streams6
Stream APIPipeline déclaratif (Java 8) pour transformer des collections : source → opérations intermédiaires → opération terminale. Ne modifie pas la source.
Pipeline déclaratif (Java 8) pour transformer des collections : source → opérations intermédiaires → opération terminale. Ne modifie pas la source.
Un stream décrit *ce qu'on veut* (filtrer, transformer, regrouper) plutôt que *comment* le faire avec des boucles. Un stream ne se consomme qu'une seule fois.
BigDecimal totalActifs = comptes.stream()
.filter(Compte::estActif) // intermédiaire
.map(Compte::solde) // intermédiaire
.reduce(BigDecimal.ZERO, BigDecimal::add); // terminaleOpérations intermédiaires vs terminales (évaluation paresseuse)Les opérations intermédiaires (filter, map, sorted…) ne font rien tant qu'une opération terminale (collect, forEach, count…) n'est pas appelée.
Les opérations intermédiaires (filter, map, sorted…) ne font rien tant qu'une opération terminale (collect, forEach, count…) n'est pas appelée.
Le stream est paresseux : il traite les éléments un par un à travers toute la chaîne et s'arrête dès que possible (limit, findFirst, anyMatch court-circuitent).
Stream<String> s = noms.stream().filter(n -> { System.out.println("test " + n); return n.length() > 3; });
// rien n'est affiché ici : aucune opération terminale
s.findFirst(); // maintenant seulement, et on s'arrête au premier trouvé| Intermédiaires | Terminales |
|---|---|
| filter, map, flatMap, distinct, sorted, limit, skip, peek | collect, toList, forEach, reduce, count, findFirst, anyMatch, min, max |
filter, map et flatMapfilter garde les éléments qui satisfont une condition ; map transforme chaque élément ; flatMap transforme puis « aplatit » des sous-listes en un seul flux.
filter garde les éléments qui satisfont une condition ; map transforme chaque élément ; flatMap transforme puis « aplatit » des sous-listes en un seul flux.
List<String> emails = clients.stream()
.filter(c -> c.ville().equals("Dakar")) // garde
.map(Client::email) // transforme Client → String
.toList();
List<Transaction> toutes = comptes.stream()
.flatMap(c -> c.transactions().stream()) // List<List<T>> → Stream<T>
.toList();collect et Collectorscollect() termine un stream en construisant un résultat ; la classe Collectors fournit les recettes : toList, toMap, groupingBy, counting, joining…
collect() termine un stream en construisant un résultat ; la classe Collectors fournit les recettes : toList, toMap, groupingBy, counting, joining…
Map<Statut, List<Order>> parStatut = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus));
Map<Statut, Long> compte = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
Map<String, Client> parId = clients.stream()
.collect(Collectors.toMap(Client::id, c -> c)); // ⚠️ IllegalStateException si id en double
String liste = noms.stream().collect(Collectors.joining(", ", "[", "]"));toList() vs Collectors.toList()stream.toList() (Java 16) renvoie une liste non modifiable ; collect(Collectors.toList()) renvoie en pratique une ArrayList modifiable.
stream.toList() (Java 16) renvoie une liste non modifiable ; collect(Collectors.toList()) renvoie en pratique une ArrayList modifiable.
List<String> a = noms.stream().toList();
a.add("x"); // ❌ UnsupportedOperationException
List<String> b = noms.stream().collect(Collectors.toList());
b.add("x"); // ✅ fonctionne (ArrayList)toList() : plus court et l'immutabilité évite des effets de bord.Parallel streamsparallelStream() découpe le travail sur le ForkJoinPool commun ; utile seulement pour de gros calculs CPU, sans I/O ni état partagé.
parallelStream() découpe le travail sur le ForkJoinPool commun ; utile seulement pour de gros calculs CPU, sans I/O ni état partagé.
long nb = grandeListe.parallelStream().filter(this::calculLourd).count();5Exceptions6
Hiérarchie Throwable : Error vs ExceptionTout ce qui peut être levé hérite de Throwable : Error = problème grave de la JVM (à ne pas attraper) ; Exception = problème applicatif (checked ou unchecked).
Tout ce qui peut être levé hérite de Throwable : Error = problème grave de la JVM (à ne pas attraper) ; Exception = problème applicatif (checked ou unchecked).
Throwable
├── Error (OutOfMemoryError, StackOverflowError) → ne pas catcher
└── Exception (checked : IOException, SQLException…)
└── RuntimeException (unchecked : NullPointerException, IllegalArgumentException…)Exception checkedException que le compilateur oblige à traiter (try/catch) ou à déclarer (throws). Hérite d'Exception mais pas de RuntimeException.
Exception que le compilateur oblige à traiter (try/catch) ou à déclarer (throws). Hérite d'Exception mais pas de RuntimeException.
Elle représente une situation externe prévisible dont l'appelant peut se remettre (fichier absent, réseau coupé). Exemples : IOException, SQLException.
public String lire(Path p) throws IOException { // déclarée dans la signature
return Files.readString(p);
}
try { lire(Path.of("config.json")); }
catch (IOException e) { log.error("Lecture impossible", e); }Exception uncheckedException héritant de RuntimeException, que le compilateur n'oblige pas à traiter ; elle signale en général une erreur de programmation ou une règle métier violée.
Exception héritant de RuntimeException, que le compilateur n'oblige pas à traiter ; elle signale en général une erreur de programmation ou une règle métier violée.
Spring privilégie les unchecked (ex. DataAccessException) : elles traversent les couches sans polluer les signatures et déclenchent le rollback par défaut de @Transactional.
public class SoldeInsuffisantException extends RuntimeException {
public SoldeInsuffisantException(String compte) {
super("Solde insuffisant sur le compte " + compte);
}
}@Transactional ne fait rollback que sur les unchecked par défaut ; pour une checked, il faut rollbackFor = Exception.class.try-with-resourcesSyntaxe try (ressource) {...} (Java 7) qui ferme automatiquement les ressources (AutoCloseable), même en cas d'exception.
Syntaxe try (ressource) {...} (Java 7) qui ferme automatiquement les ressources (AutoCloseable), même en cas d'exception.
try (var conn = dataSource.getConnection();
var ps = conn.prepareStatement("SELECT solde FROM compte WHERE id = ?")) {
ps.setLong(1, id);
try (var rs = ps.executeQuery()) { /* ... */ }
} // ps puis conn fermés automatiquement, dans l'ordre inverseAvaler une exception (anti-pattern) et chaînage« Avaler » = attraper une exception sans rien faire : le bug devient invisible. Il faut au minimum logger, ou relancer en gardant la cause.
« Avaler » = attraper une exception sans rien faire : le bug devient invisible. Il faut au minimum logger, ou relancer en gardant la cause.
// ❌ l'erreur disparaît
try { paiement.executer(); } catch (Exception e) { }
// ✅ relancer avec contexte métier ET la cause d'origine
try { paiement.executer(); }
catch (IOException e) {
throw new PaiementException("Échec appel opérateur pour " + ref, e); // e = cause
}Exception métier personnaliséeException dédiée à une règle du domaine (CompteBloqueException, NotFoundException) ; elle rend le code lisible et se traduit en code HTTP via un handler global.
Exception dédiée à une règle du domaine (CompteBloqueException, NotFoundException) ; elle rend le code lisible et se traduit en code HTTP via un handler global.
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) { super(message); }
}
// traduite en 404 par @RestControllerAdvice (voir Spring)6Mémoire JVM & Garbage Collector7
Heap (tas)Zone mémoire partagée par tous les threads où vivent tous les objets créés avec new ; gérée par le Garbage Collector.
Zone mémoire partagée par tous les threads où vivent tous les objets créés avec new ; gérée par le Garbage Collector.
Sa taille se règle avec -Xms (initiale) et -Xmx (maximale). En conteneur, on préfère -XX:MaxRAMPercentage=75 pour s'adapter à la limite mémoire du pod.
java -Xms512m -Xmx2g -jar app.jar
java -XX:MaxRAMPercentage=75 -jar app.jar # recommandé dans Docker/KubernetesYoung Generation et Old GenerationLe heap est découpé par âge : les objets neufs naissent dans la Young Gen (nettoyée souvent et vite) ; ceux qui survivent sont promus dans l'Old Gen (nettoyée plus rarement).
Le heap est découpé par âge : les objets neufs naissent dans la Young Gen (nettoyée souvent et vite) ; ceux qui survivent sont promus dans l'Old Gen (nettoyée plus rarement).
Hypothèse générationnelle : la plupart des objets meurent jeunes (variables temporaires d'une requête). Nettoyer souvent une petite zone est très efficace (« minor GC »). L'Old Gen contient les caches, singletons, sessions ; sa collecte (« major/full GC ») est plus coûteuse.
Stack (pile d'appels)Mémoire propre à chaque thread qui contient les frames d'appel : variables locales, paramètres, adresse de retour. Libérée automatiquement à la sortie de la méthode.
Mémoire propre à chaque thread qui contient les frames d'appel : variables locales, paramètres, adresse de retour. Libérée automatiquement à la sortie de la méthode.
Les types primitifs locaux et les références sont sur la pile ; les objets eux-mêmes sont dans le heap. Une récursion infinie remplit la pile → StackOverflowError.
void traiter() {
int compteur = 0; // sur la stack du thread
Client c = new Client("Awa"); // la référence c est sur la stack, l'objet dans le heap
} // frame dépilée : compteur et c disparaissentMetaspaceZone hors heap (Java 8+) qui stocke les métadonnées des classes chargées ; elle a remplacé la PermGen.
Zone hors heap (Java 8+) qui stocke les métadonnées des classes chargées ; elle a remplacé la PermGen.
Elle grandit avec le nombre de classes (frameworks, proxies générés). Une fuite de classloaders (redéploiements à chaud) provoque OutOfMemoryError: Metaspace. Limite réglable avec -XX:MaxMetaspaceSize.
Garbage Collector (GC)Mécanisme automatique qui libère la mémoire des objets devenus inaccessibles (plus aucune référence vers eux).
Mécanisme automatique qui libère la mémoire des objets devenus inaccessibles (plus aucune référence vers eux).
Le GC part des « racines » (variables de pile, champs statiques) et marque tout ce qui est atteignable ; le reste est récupéré. G1 est le GC par défaut (bon compromis débit/pauses) ; ZGC (générationnel depuis Java 21) vise des pauses de l'ordre de la milliseconde même sur de très gros heaps.
java -XX:+UseZGC -Xmx8g -jar app.jar # pauses très courtes
java -Xlog:gc -jar app.jar # journaliser l'activité du GCFuite mémoireDes objets inutiles restent référencés (cache sans limite, listener jamais retiré, ThreadLocal non nettoyé) : le GC ne peut pas les libérer et la mémoire monte jusqu'à l'OutOfMemoryError.
Des objets inutiles restent référencés (cache sans limite, listener jamais retiré, ThreadLocal non nettoyé) : le GC ne peut pas les libérer et la mémoire monte jusqu'à l'OutOfMemoryError.
// ❌ grossit à chaque requête, jamais vidé
private static final Map<String, Rapport> CACHE = new HashMap<>();
// ✅ cache borné avec expiration (Caffeine)
Cache<String, Rapport> cache = Caffeine.newBuilder().maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10)).build();jcmd <pid> GC.heap_dump) analysé avec Eclipse MAT ; chercher le « dominator » qui retient la mémoire.OutOfMemoryError et StackOverflowErrorOutOfMemoryError : le heap (ou le Metaspace) est plein malgré le GC. StackOverflowError : la pile d'un thread est pleine, souvent à cause d'une récursion infinie.
OutOfMemoryError : le heap (ou le Metaspace) est plein malgré le GC. StackOverflowError : la pile d'un thread est pleine, souvent à cause d'une récursion infinie.
int factorielle(int n) { return n * factorielle(n - 1); } // pas de cas d'arrêt → StackOverflowErrorjava -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps -jar app.jar7Concurrence13
Thread et RunnableUn thread est un fil d'exécution indépendant ; Runnable est la tâche (sans résultat) qu'il exécute. Callable est la variante qui renvoie un résultat.
Un thread est un fil d'exécution indépendant ; Runnable est la tâche (sans résultat) qu'il exécute. Callable est la variante qui renvoie un résultat.
On crée rarement des threads à la main : on soumet des tâches à un ExecutorService qui gère un pool.
Runnable tache = () -> System.out.println("Exécuté par " + Thread.currentThread().getName());
Thread t = new Thread(tache);
t.start(); // ⚠️ start(), pas run() : run() s'exécuterait dans le thread courant
t.join(); // attendre la finRace condition (condition de course)Bug où le résultat dépend de l'ordre d'exécution de plusieurs threads qui accèdent à une donnée partagée sans synchronisation.
Bug où le résultat dépend de l'ordre d'exécution de plusieurs threads qui accèdent à une donnée partagée sans synchronisation.
class Compteur { int valeur = 0; void incr() { valeur++; } } // lire → +1 → écrire
// 2 threads × 1000 appels → résultat souvent < 2000 : des incréments sont perdussynchronized, locks), ou utiliser des classes atomiques/concurrentes.volatileGarantit que l'écriture d'une variable par un thread est immédiatement visible par les autres (pas de cache CPU) et interdit certains réordonnancements. Ne garantit PAS l'atomicité.
Garantit que l'écriture d'une variable par un thread est immédiatement visible par les autres (pas de cache CPU) et interdit certains réordonnancements. Ne garantit PAS l'atomicité.
Parfait pour un drapeau d'arrêt lu par un thread et écrit par un autre. Inutile pour un compteur : compteur++ reste 3 opérations (lire, incrémenter, écrire).
private volatile boolean actif = true;
public void arreter() { actif = false; } // thread A
public void boucle() { while (actif) { traiter(); } } // thread B voit le changementsynchronizedVerrou intrinsèque : un seul thread à la fois exécute le bloc ou la méthode sur un même objet ; garantit exclusion mutuelle ET visibilité.
Verrou intrinsèque : un seul thread à la fois exécute le bloc ou la méthode sur un même objet ; garantit exclusion mutuelle ET visibilité.
public class Compte {
private BigDecimal solde = BigDecimal.ZERO;
public synchronized void crediter(BigDecimal m) { solde = solde.add(m); }
public void debiter(BigDecimal m) {
synchronized (this) { // bloc synchronisé
if (solde.compareTo(m) < 0) throw new SoldeInsuffisantException("…");
solde = solde.subtract(m);
}
}
}SELECT … FOR UPDATE, @Version) ou distribué.AtomicInteger et CASClasses (java.util.concurrent.atomic) qui offrent des opérations atomiques sans verrou grâce à l'instruction processeur Compare-And-Swap.
Classes (java.util.concurrent.atomic) qui offrent des opérations atomiques sans verrou grâce à l'instruction processeur Compare-And-Swap.
private final AtomicLong compteurRequetes = new AtomicLong();
compteurRequetes.incrementAndGet(); // atomique, thread-safe
compteurRequetes.updateAndGet(v -> Math.max(v, 10)); // mise à jour atomique par fonctionConcurrentHashMapMap thread-safe et performante : verrouillage fin par bucket, lectures sans verrou, opérations atomiques compute/merge.
Map thread-safe et performante : verrouillage fin par bucket, lectures sans verrou, opérations atomiques compute/merge.
ConcurrentHashMap<String, LongAdder> appelsParClient = new ConcurrentHashMap<>();
appelsParClient.computeIfAbsent(clientId, k -> new LongAdder()).increment();if (!map.containsKey(k)) map.put(k, v) n'est pas atomique même sur une ConcurrentHashMap : utiliser putIfAbsent ou computeIfAbsent.java.util.concurrent et ExecutorServicejava.util.concurrent regroupe les outils de concurrence haut niveau ; ExecutorService gère un pool de threads auquel on soumet des tâches.
java.util.concurrent regroupe les outils de concurrence haut niveau ; ExecutorService gère un pool de threads auquel on soumet des tâches.
Réutiliser des threads évite le coût de création et limite la concurrence. Toujours fermer l'executor (ou utiliser try-with-resources depuis Java 19).
ExecutorService pool = Executors.newFixedThreadPool(8);
Future<Rapport> f = pool.submit(() -> genererRapport(mois));
Rapport r = f.get(30, TimeUnit.SECONDS); // attente avec timeout
pool.shutdown();CompletableFutureRésultat asynchrone composable (Java 8) : on enchaîne des traitements (thenApply, thenCombine) sans bloquer, avec gestion d'erreur (exceptionally).
Résultat asynchrone composable (Java 8) : on enchaîne des traitements (thenApply, thenCombine) sans bloquer, avec gestion d'erreur (exceptionally).
CompletableFuture<Client> client = CompletableFuture.supplyAsync(() -> clientApi.get(id), pool);
CompletableFuture<List<Compte>> comptes = CompletableFuture.supplyAsync(() -> compteApi.list(id), pool);
Synthese s = client.thenCombine(comptes, Synthese::new) // les deux appels en parallèle
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> Synthese.degradee())
.join();ForkJoinPoolPool spécialisé « diviser pour régner » avec vol de tâches (work stealing) ; utilisé par parallelStream et par défaut par CompletableFuture.
Pool spécialisé « diviser pour régner » avec vol de tâches (work stealing) ; utilisé par parallelStream et par défaut par CompletableFuture.
Chaque thread a sa propre file ; un thread inoccupé « vole » le travail des autres. Idéal pour des calculs récursifs CPU-bound, pas pour des appels bloquants.
Deadlock (interblocage)Deux threads (ou transactions) s'attendent mutuellement : chacun tient un verrou dont l'autre a besoin, et aucun n'avance.
Deux threads (ou transactions) s'attendent mutuellement : chacun tient un verrou dont l'autre a besoin, et aucun n'avance.
// Thread 1 : lock(compteA) puis lock(compteB)
// Thread 2 : lock(compteB) puis lock(compteA) → blocage mutuel
// ✅ Remède : toujours prendre les verrous dans le même ordre (ex. par id croissant)
Compte premier = a.id() < b.id() ? a : b, second = premier == a ? b : a;
synchronized (premier) { synchronized (second) { transferer(a, b, montant); } }jstack <pid> affiche « Found one Java-level deadlock » avec les threads concernés.Thread-safeSe dit d'un code ou d'un objet qui reste correct quand plusieurs threads l'utilisent en même temps.
Se dit d'un code ou d'un objet qui reste correct quand plusieurs threads l'utilisent en même temps.
Les beans Spring sont des singletons partagés par toutes les requêtes : ils doivent être sans état mutable (stateless) ou thread-safe. Un champ private int compteur dans un @Service est un bug de concurrence.
I/O-bound vs CPU-boundI/O-bound : le temps est passé à attendre (base, réseau, fichiers). CPU-bound : le temps est passé à calculer. Le bon outil de concurrence dépend de ce profil.
I/O-bound : le temps est passé à attendre (base, réseau, fichiers). CPU-bound : le temps est passé à calculer. Le bon outil de concurrence dépend de ce profil.
| Profil | Exemple | Outil adapté |
|---|---|---|
| I/O-bound | API qui appelle 3 services et une base | Virtual threads, async |
| CPU-bound | Calcul de scoring sur 1 M de lignes | Pool = nb de cœurs, ForkJoin |
Reactive StreamsStandard d'échange asynchrone de flux avec contre-pression (backpressure) : le consommateur indique combien d'éléments il peut traiter. Implémenté par Reactor (Spring WebFlux) et RxJava.
Standard d'échange asynchrone de flux avec contre-pression (backpressure) : le consommateur indique combien d'éléments il peut traiter. Implémenté par Reactor (Spring WebFlux) et RxJava.
Le modèle réactif (Mono/Flux) permet de servir beaucoup de connexions avec peu de threads, au prix d'un code plus complexe à lire et déboguer. Depuis Java 21, les virtual threads offrent souvent la même scalabilité avec du code impératif classique.
Flux<Transaction> flux = webClient.get().uri("/transactions").retrieve()
.bodyToFlux(Transaction.class)
.filter(t -> t.montant().compareTo(SEUIL) > 0)
.limitRate(100); // backpressure : demande 100 éléments à la fois📋 Synthèse express — Java
🗓 Versions Java LTS
- Lambdas & Streams
- Optional
- Default methods
- var (local type inference)
- HttpClient
- String methods
- Records
- Sealed Classes
- Pattern Matching instanceof
- Virtual Threads (JEP 444)
- Pattern Matching switch
- Sequenced Collections
- Record Patterns
- Flexible Constructors (JEP 513)
- Scoped Values (JEP 506)
- Compact source files (JEP 512)
- Compact Object Headers (JEP 519)
- En preview : Primitive Patterns (JEP 507), Structured Concurrency
Thread.ofVirtual().start(runnable)
🧱 Piliers OOP
Private fields + getters/setters. Invariants protégés. Records Java 17+ = encapsulation immutable.
extends + super. Préférer composition. Max 2-3 niveaux d'héritage.
Override (runtime) vs Overload (compile-time). Sealed classes pour hiérarchies exhaustives.
Interfaces > classes abstraites. Interfaces fonctionnelles + lambdas.
📚 Collections — Complexités
| Structure | Accès | Insertion | Recherche | Cas d'usage |
|---|---|---|---|---|
| ArrayList | O(1) | O(n) tête | O(n) | Lecture fréquente |
| LinkedList | O(n) | O(1) | O(n) | Files, Deque |
| HashMap | — | O(1) amort. | O(1) amort. | Cache key-value |
| TreeMap | — | O(log n) | O(log n) | Trié, ranges |
| HashSet | — | O(1) amort. | O(1) amort. | Unicité |
| PriorityQueue | O(1) head | O(log n) | O(n) | Scheduling |
| ArrayDeque | O(1) | O(1) | O(n) | Pile (LIFO) et file (FIFO) efficaces |
🌊 Streams — Opérateurs clés
💡 Records & Sealed Classes (Java 17+)
🔀 Interface vs Classe Abstraite
| Critère | Interface | Classe Abstraite |
|---|---|---|
| Héritage multiple | ✅ implements plusieurs | ❌ extends une seule |
| État partagé | ❌ (constantes seulement) | ✅ champs d'instance |
| Constructeur | ❌ | ✅ |
| Méthodes | abstract, default, static | abstract + concrètes |
| Quand choisir | Contrat / comportement pur | Socle commun réel avec état |
🔒 Immuabilité & String
- Sécurité — clés de Map, paramètres réseau non altérables
- String Pool — partage de littéraux sans copie
- Cache hashCode — calculé une fois, réutilisé
- Thread-safe — pas besoin de synchronisation
StringBuilder : mutable, non thread-safe, O(n). StringBuffer : synchronized, plus lent. Concaténation en boucle = toujours StringBuilder.
⚠️ Exceptions — Checked vs Unchecked
IOException, SQLException) : déclarées dans la signature, le compilateur les impose. Utiles quand le caller peut raisonnablement récupérer.Unchecked (
RuntimeException, NullPointerException, IllegalArgumentException) : erreurs de programmation, invariants violés. Spring préfère les unchecked pour ne pas polluer les couches.⛔ Ne jamais avaler :
catch(Exception e) {} — logger au moins ou relancer avec contexte.
🧵 JVM & Concurrence
- Heap — objets, Young Gen + Old Gen
- Stack — frames d'appel, variables locales (1 par thread)
- Metaspace — métadonnées des classes (Java 8+ : remplace PermGen)
- GC — libère objets inaccessibles, ne corrige pas les fuites de référence
volatile : garantit la visibilité inter-threads ET ordonnancement mémoire, mais pas l'atomicité. compteur++ n'est pas atomique même avec volatile.
synchronized : mutual exclusion + visibilité. Préférer AtomicInteger, ConcurrentHashMap ou java.util.concurrent.
Thread.ofVirtual().start(task)sealed sur une interface Java 17 ?sealed interface Shape permits Circle, Rectangle : seules ces classes peuvent implémenter Shape. Permet un exhaustive pattern matching dans switch.== compare l'adresse mémoire (référence). equals() compare le contenu. "a" == "a" peut être true via le String Pool, mais new String("a") == new String("a") est false.volatile garantit-il l'atomicité de compteur++ ?volatile garantit la visibilité inter-threads et empêche la réorganisation mémoire, mais compteur++ est en réalité 3 opérations : lire, incrémenter, écrire. Deux threads peuvent lire la même valeur simultanément. Utiliser AtomicInteger.incrementAndGet() à la place.1Fondations de Spring15
Spring Framework vs Spring BootSpring Framework fournit le conteneur IoC, l'AOP, le web MVC, les transactions ; Spring Boot ajoute l'auto-configuration, les starters et un serveur embarqué pour démarrer sans configuration manuelle.
Spring Framework fournit le conteneur IoC, l'AOP, le web MVC, les transactions ; Spring Boot ajoute l'auto-configuration, les starters et un serveur embarqué pour démarrer sans configuration manuelle.
Avec Spring « nu », il fallait déclarer soi-même la DataSource, le TransactionManager, le DispatcherServlet… Spring Boot détecte les bibliothèques présentes et configure tout avec des valeurs par défaut raisonnables, que l'on surcharge dans application.yml. L'application se lance comme un simple java -jar (Tomcat embarqué).
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class BanqueApplication {
public static void main(String[] args) {
SpringApplication.run(BanqueApplication.class, args);
}
}Auto-configurationMécanisme de Spring Boot qui crée automatiquement des beans selon le classpath et les propriétés (ex. une DataSource si un driver JDBC est présent).
Mécanisme de Spring Boot qui crée automatiquement des beans selon le classpath et les propriétés (ex. une DataSource si un driver JDBC est présent).
Chaque auto-configuration est conditionnelle (@ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty). Si tu déclares ton propre bean, celui de Boot s'efface. Pour voir ce qui a été activé ou non : --debug au démarrage.
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
public class MaDataSourceAutoConfig {
@Bean
@ConditionalOnMissingBean // seulement si l'application n'en définit pas
DataSource dataSource(DataSourceProperties p) { return p.initializeDataSourceBuilder().build(); }
}java -jar app.jar --debug # rapport "CONDITIONS EVALUATION REPORT"StarterDépendance Maven/Gradle « tout-en-un » qui apporte un ensemble cohérent de bibliothèques pour un besoin (web, JPA, sécurité…) avec des versions compatibles.
Dépendance Maven/Gradle « tout-en-un » qui apporte un ensemble cohérent de bibliothèques pour un besoin (web, JPA, sécurité…) avec des versions compatibles.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <!-- Spring MVC + Jackson + Tomcat -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>IoC (Inversion of Control)Principe où ce n'est plus ton code qui crée ses dépendances : le conteneur Spring les crée, les relie et gère leur cycle de vie.
Principe où ce n'est plus ton code qui crée ses dépendances : le conteneur Spring les crée, les relie et gère leur cycle de vie.
Sans IoC, OrderService ferait new JpaOrderRepo() : couplage fort, impossible à remplacer en test. Avec IoC, la classe déclare ce dont elle a besoin et le conteneur le fournit. L'Injection de dépendances est la technique qui réalise l'IoC.
// ❌ sans IoC : la classe choisit et construit sa dépendance
private final OrderRepo repo = new JpaOrderRepo();
// ✅ avec IoC : elle la reçoit
public OrderService(OrderRepo repo) { this.repo = repo; }Injection de dépendancesTechnique où les dépendances d'un objet lui sont fournies de l'extérieur, de préférence par le constructeur.
Technique où les dépendances d'un objet lui sont fournies de l'extérieur, de préférence par le constructeur.
Trois formes : par constructeur (recommandée), par setter, par champ (@Autowired sur un attribut, à éviter). Le constructeur rend les dépendances obligatoires et final, rend la classe testable sans Spring (new OrderService(mockRepo)) et révèle les classes qui ont trop de dépendances.
@Service
public class OrderService {
private final OrderRepo repo;
private final PaymentGateway gateway;
// un seul constructeur → @Autowired implicite depuis Spring 4.3
public OrderService(OrderRepo repo, PaymentGateway gateway) {
this.repo = repo;
this.gateway = gateway;
}
}@Autowired private Repo repo;) cache les dépendances et oblige à démarrer Spring ou utiliser la réflexion pour tester.BeanObjet créé, configuré et géré par le conteneur Spring (ApplicationContext). Par défaut, un seul exemplaire (singleton) par contexte.
Objet créé, configuré et géré par le conteneur Spring (ApplicationContext). Par défaut, un seul exemplaire (singleton) par contexte.
Tout ce que Spring injecte est un bean : services, repositories, contrôleurs, mais aussi objets techniques (ObjectMapper, DataSource). Un bean est déclaré par un stéréotype (@Service…) ou une méthode @Bean.
@Component
public class GenerateurReference {
public String nouvelle() { return "TX-" + UUID.randomUUID(); }
}ApplicationContextLe conteneur IoC de Spring : il scanne les classes, crée les beans, résout les dépendances, applique les proxies (transactions, sécurité) et publie des événements.
Le conteneur IoC de Spring : il scanne les classes, crée les beans, résout les dépendances, applique les proxies (transactions, sécurité) et publie des événements.
@Component
public class Demarrage implements ApplicationRunner {
private final ApplicationContext ctx;
public Demarrage(ApplicationContext ctx) { this.ctx = ctx; }
public void run(ApplicationArguments args) {
System.out.println(ctx.getBeanDefinitionCount() + " beans chargés");
}
}Stéréotypes : @Component, @Service, @Repository, @ControllerAnnotations qui déclarent une classe comme bean lors du scan ; elles indiquent aussi son rôle dans l'architecture.
Annotations qui déclarent une classe comme bean lors du scan ; elles indiquent aussi son rôle dans l'architecture.
| Annotation | Rôle | Particularité |
|---|---|---|
@Component | Bean générique | Base des autres |
@Service | Logique métier | Purement sémantique |
@Repository | Accès aux données | Traduit les exceptions SQL en DataAccessException |
@Controller / @RestController | Couche web | Mappe les requêtes HTTP |
@Configuration et @Bean@Configuration marque une classe de configuration ; chaque méthode @Bean qu'elle contient déclare un bean, utile pour des classes que tu ne peux pas annoter (bibliothèques tierces).
@Configuration marque une classe de configuration ; chaque méthode @Bean qu'elle contient déclare un bean, utile pour des classes que tu ne peux pas annoter (bibliothèques tierces).
@Configuration
public class ClientsConfig {
@Bean
RestClient waveClient(RestClient.Builder builder,
@Value("${wave.base-url}") String baseUrl) {
return builder.baseUrl(baseUrl).build();
}
}Scopes de beanDurée de vie d'un bean : singleton (un par contexte, défaut), prototype (nouveau à chaque injection), request (un par requête HTTP), session (un par session HTTP).
Durée de vie d'un bean : singleton (un par contexte, défaut), prototype (nouveau à chaque injection), request (un par requête HTTP), session (un par session HTTP).
@Component
@Scope("prototype")
public class PanierCalcul { /* état propre à chaque utilisation */ }
@Component
@RequestScope // un exemplaire par requête HTTP
public class ContexteRequete { private String correlationId; }@AutowiredAnnotation qui demande à Spring d'injecter une dépendance ; facultative sur un constructeur unique depuis Spring 4.3.
Annotation qui demande à Spring d'injecter une dépendance ; facultative sur un constructeur unique depuis Spring 4.3.
Si plusieurs beans du même type existent, Spring ne sait pas lequel choisir : on précise avec @Qualifier("nom") ou on marque un bean @Primary.
public NotificationService(@Qualifier("sms") Notificateur notificateur) { … }Configuration externalisée : application.yml, @Value, @ConfigurationPropertiesLes paramètres (URL, timeouts, secrets) vivent hors du code, dans application.yml ou des variables d'environnement ; on les lit avec @Value ou, mieux, une classe @ConfigurationProperties typée.
Les paramètres (URL, timeouts, secrets) vivent hors du code, dans application.yml ou des variables d'environnement ; on les lit avec @Value ou, mieux, une classe @ConfigurationProperties typée.
banque:
virement:
plafond-journalier: 5000000
timeout: 3s@ConfigurationProperties(prefix = "banque.virement")
public record VirementProperties(BigDecimal plafondJournalier, Duration timeout) {}
@SpringBootApplication
@ConfigurationPropertiesScan
public class App {}Profils (@Profile)Un profil active une configuration ou des beans selon l'environnement (dev, test, prod) : fichier application-prod.yml, beans annotés @Profile("prod").
Un profil active une configuration ou des beans selon l'environnement (dev, test, prod) : fichier application-prod.yml, beans annotés @Profile("prod").
@Bean @Profile("dev")
PaymentGateway fauxGateway() { return new PaymentGatewaySimule(); }java -jar app.jar --spring.profiles.active=prod # charge application-prod.ymlSpring Boot ActuatorModule qui expose des endpoints de supervision : santé (/actuator/health), métriques, info, et format Prometheus.
Module qui expose des endpoints de supervision : santé (/actuator/health), métriques, info, et format Prometheus.
Indispensable en production et sur Kubernetes : les probes liveness/readiness interrogent /actuator/health/liveness et /actuator/health/readiness.
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
probes:
enabled: trueinclude: "*") publiquement : /env ou /heapdump révèlent des secrets.Spring CloudEnsemble de projets pour les systèmes distribués : Gateway (API Gateway), Config Server, OpenFeign, Circuit Breaker, Stream (Kafka/RabbitMQ).
Ensemble de projets pour les systèmes distribués : Gateway (API Gateway), Config Server, OpenFeign, Circuit Breaker, Stream (Kafka/RabbitMQ).
Sur Kubernetes, une partie de ces besoins (découverte de services, configuration) est assurée par la plateforme ; Spring Cloud Gateway et Spring Cloud Stream restent très utilisés.
spring:
cloud:
gateway:
server:
webmvc:
routes:
- id: comptes
uri: http://comptes-service:8080
predicates:
- Path=/api/comptes/**2Versions & nouveautés (Boot 3 → 4)13
Jakarta EE (javax → jakarta)Depuis Spring Boot 3, les API Java EE ont changé de package : javax.persistence → jakarta.persistence, javax.servlet → jakarta.servlet, etc. Boot 4 passe à Jakarta EE 11.
Depuis Spring Boot 3, les API Java EE ont changé de package : javax.persistence → jakarta.persistence, javax.servlet → jakarta.servlet, etc. Boot 4 passe à Jakarta EE 11.
Oracle a cédé Java EE à la fondation Eclipse, qui l'a renommé Jakarta EE sans pouvoir garder le nom javax. Migrer de Boot 2 à Boot 3 impose donc de réécrire les imports (outil OpenRewrite) et de mettre à jour les bibliothèques. De Boot 3 à Boot 4, pas de nouveau renommage.
// Spring Boot 2.x
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
// Spring Boot 3.x / 4.x
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotBlank;AOT (Ahead-Of-Time) dans SpringTraitement effectué au build (et non au démarrage) : Spring pré-calcule la configuration des beans pour démarrer plus vite et permettre la compilation native.
Traitement effectué au build (et non au démarrage) : Spring pré-calcule la configuration des beans pour démarrer plus vite et permettre la compilation native.
Normalement Spring analyse les annotations par réflexion au démarrage. En mode AOT, le plugin Maven/Gradle génère du code Java qui enregistre directement les beans. Spring Boot 4 étend l'AOT à Spring Data (repositories pré-générés).
./mvnw spring-boot:process-aot packageGraalVM Native ImageCompile l'application Java en exécutable natif : démarrage en dizaines de millisecondes et mémoire réduite, au prix d'un build long et de restrictions sur la réflexion.
Compile l'application Java en exécutable natif : démarrage en dizaines de millisecondes et mémoire réduite, au prix d'un build long et de restrictions sur la réflexion.
Intéressant pour les fonctions serverless et les microservices qui démarrent souvent. Moins pertinent pour une grosse application longue durée, où la JVM et son JIT restent plus performants une fois chauds.
./mvnw -Pnative native:compile # produit target/mon-app (binaire natif)
./mvnw spring-boot:build-image -Pnative # image Docker native via buildpacksSpring Boot 4 / Spring Framework 7 (vue d'ensemble)Génération sortie en novembre 2025 : Java 17 minimum (Java 25 recommandé), Jakarta EE 11, Jackson 3, versioning d'API natif, clients HTTP déclaratifs, null-safety JSpecify, résilience intégrée.
Génération sortie en novembre 2025 : Java 17 minimum (Java 25 recommandé), Jakarta EE 11, Jackson 3, versioning d'API natif, clients HTTP déclaratifs, null-safety JSpecify, résilience intégrée.
Points à retenir pour un entretien :
- Baseline Java 17 (comme Boot 3), support de premier niveau de Java 25.
- Jackson 3 par défaut (Jackson 2 encore disponible en mode déprécié).
- Versioning d'API et HTTP Service Clients intégrés.
- JSpecify pour la null-safety, @Retryable / @ConcurrencyLimit dans le cœur.
- JUnit 6,
@MockitoBeanremplace@MockBean,RestTestClientremplaceTestRestTemplate. - Les virtual threads restent opt-in (
spring.threads.virtual.enabled=true).
JSpecify et null-safetyStandard d'annotations de nullité adopté par Spring 7 : dans un package @NullMarked, tout est non-null par défaut et @Nullable signale explicitement ce qui peut l'être.
Standard d'annotations de nullité adopté par Spring 7 : dans un package @NullMarked, tout est non-null par défaut et @Nullable signale explicitement ce qui peut l'être.
Les IDE et des outils comme NullAway détectent alors à la compilation un risque de NullPointerException. Spring annote toute son API ainsi.
// package-info.java
@NullMarked
package sn.banque.virement;
import org.jspecify.annotations.NullMarked;
// dans le package
public @Nullable Client chercher(String telephone) { … } // peut renvoyer null : l'appelant est avertiJackson 3Bibliothèque JSON par défaut de Spring Boot 4 : packages tools.jackson, configuration immuable (JsonMapper.builder()), dates ISO-8601 par défaut, exceptions non vérifiées.
Bibliothèque JSON par défaut de Spring Boot 4 : packages tools.jackson, configuration immuable (JsonMapper.builder()), dates ISO-8601 par défaut, exceptions non vérifiées.
Jackson convertit les objets Java en JSON et inversement (sérialisation/désérialisation) pour les contrôleurs REST. Les annotations (@JsonProperty, @JsonIgnore) restent dans com.fasterxml.jackson.annotation.
JsonMapper mapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build(); // immuable une fois construit
String json = mapper.writeValueAsString(new VirementDto("SN01", "SN02", BigDecimal.TEN));Versioning d'API (Spring 7)Spring Framework 7 gère nativement plusieurs versions d'un endpoint : la version est lue dans un header, un paramètre, le chemin ou le media type, puis routée via l'attribut version.
Spring Framework 7 gère nativement plusieurs versions d'un endpoint : la version est lue dans un header, un paramètre, le chemin ou le media type, puis routée via l'attribut version.
spring.mvc.apiversion.use.header=X-API-Version
spring.mvc.apiversion.supported=1.0,2.0
spring.mvc.apiversion.default=1.0@GetMapping(path = "/comptes/{id}", version = "1.0")
CompteV1 compteV1(@PathVariable Long id) { … }
@GetMapping(path = "/comptes/{id}", version = "2.0")
CompteV2 compteV2(@PathVariable Long id) { … } // X-API-Version: 2.0/api/v1/...).HTTP Service Clients (@HttpExchange)Client HTTP déclaratif : on écrit une interface annotée et Spring génère l'implémentation (comme OpenFeign, mais intégré au framework). Boot 4 les enregistre avec @ImportHttpServices.
Client HTTP déclaratif : on écrit une interface annotée et Spring génère l'implémentation (comme OpenFeign, mais intégré au framework). Boot 4 les enregistre avec @ImportHttpServices.
@HttpExchange("/v1/paiements")
public interface OperateurClient {
@GetExchange("/{id}")
PaiementDto statut(@PathVariable String id);
@PostExchange
PaiementDto initier(@RequestBody DemandePaiement demande);
}
@Configuration
@ImportHttpServices(group = "operateur", types = OperateurClient.class)
class HttpClientsConfig {} // l'URL de base se configure par groupeRestClientClient HTTP synchrone moderne (Spring 6.1 / Boot 3.2) à API fluide ; c'est le remplaçant officiel de RestTemplate.
Client HTTP synchrone moderne (Spring 6.1 / Boot 3.2) à API fluide ; c'est le remplaçant officiel de RestTemplate.
Il partage l'infrastructure de RestTemplate (convertisseurs, intercepteurs) mais avec une API lisible et chaînée. Il s'injecte via RestClient.Builder, pré-configuré par Boot (observabilité, Jackson).
@Service
public class TauxService {
private final RestClient client;
public TauxService(RestClient.Builder builder) {
this.client = builder.baseUrl("https://api.bceao.exemple").build();
}
public TauxDto taux(String devise) {
return client.get().uri("/taux/{d}", devise)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
(req, res) -> { throw new NotFoundException("Devise inconnue " + devise); })
.body(TauxDto.class);
}
}RestTemplateAncien client HTTP synchrone de Spring ; déprécié pour suppression depuis Spring Framework 7.1 (Boot 4.1), suppression prévue en 8.0. Utiliser RestClient.
Ancien client HTTP synchrone de Spring ; déprécié pour suppression depuis Spring Framework 7.1 (Boot 4.1), suppression prévue en 8.0. Utiliser RestClient.
// avant
User u = restTemplate.getForObject("/users/{id}", User.class, 42);
// après
User u = restClient.get().uri("/users/{id}", 42).retrieve().body(User.class);WebClientClient HTTP non bloquant et réactif (Spring WebFlux), qui renvoie des Mono/Flux. À réserver aux applications réactives ou aux appels massivement concurrents.
Client HTTP non bloquant et réactif (Spring WebFlux), qui renvoie des Mono/Flux. À réserver aux applications réactives ou aux appels massivement concurrents.
Mono<TauxDto> taux = webClient.get().uri("/taux/{d}", "EUR")
.retrieve().bodyToMono(TauxDto.class)
.timeout(Duration.ofSeconds(2));Virtual threads dans Spring BootDepuis Boot 3.2, la propriété spring.threads.virtual.enabled=true fait traiter chaque requête Tomcat, chaque tâche @Async et chaque listener par un virtual thread. Toujours opt-in en Boot 4.
Depuis Boot 3.2, la propriété spring.threads.virtual.enabled=true fait traiter chaque requête Tomcat, chaque tâche @Async et chaque listener par un virtual thread. Toujours opt-in en Boot 4.
spring.threads.virtual.enabled=trueRésilience intégrée : @Retryable et @ConcurrencyLimit (Spring 7)Spring Framework 7 intègre dans le cœur la relance automatique (@Retryable avec backoff exponentiel et jitter) et la limitation de concurrence (@ConcurrencyLimit).
Spring Framework 7 intègre dans le cœur la relance automatique (@Retryable avec backoff exponentiel et jitter) et la limitation de concurrence (@ConcurrencyLimit).
@Configuration
@EnableResilientMethods
class ResilienceConfig {}
@Service
class NotificationService {
@Retryable(includes = OperateurIndisponibleException.class,
maxRetries = 3, delay = 200, multiplier = 2, jitter = 50, maxDelay = 2000)
public void envoyerSms(String tel, String msg) { … }
@ConcurrencyLimit(10) // au plus 10 appels simultanés
public void appelerOperateur() { … }
}3API REST avec Spring MVC11
REST (principes)Style d'architecture où l'on manipule des ressources identifiées par des URL avec les verbes HTTP, de façon sans état (stateless), en échangeant des représentations (souvent JSON).
Style d'architecture où l'on manipule des ressources identifiées par des URL avec les verbes HTTP, de façon sans état (stateless), en échangeant des représentations (souvent JSON).
Bonnes pratiques : noms au pluriel (/clients), pas de verbe dans l'URL (POST /clients et non /creerClient), codes HTTP significatifs, pagination, versioning.
| Action | Requête | Réponse |
|---|---|---|
| Lister | GET /api/v1/clients?page=0&size=20 | 200 + page |
| Lire | GET /api/v1/clients/42 | 200 ou 404 |
| Créer | POST /api/v1/clients | 201 + Location |
| Remplacer | PUT /api/v1/clients/42 | 200 / 204 |
| Modifier partiellement | PATCH /api/v1/clients/42 | 200 |
| Supprimer | DELETE /api/v1/clients/42 | 204 |
Verbes HTTP, sûreté et idempotenceGET lit sans effet de bord (sûr) ; PUT et DELETE sont idempotents (les répéter donne le même état) ; POST et PATCH ne le sont pas par nature.
GET lit sans effet de bord (sûr) ; PUT et DELETE sont idempotents (les répéter donne le même état) ; POST et PATCH ne le sont pas par nature.
C'est essentiel pour les relances : un client peut rejouer un GET ou un PUT sans risque, mais rejouer un POST de paiement peut débiter deux fois. D'où l'Idempotency-Key pour les POST sensibles.
| Verbe | Sûr | Idempotent |
|---|---|---|
| GET | ✅ | ✅ |
| PUT | ❌ | ✅ |
| DELETE | ❌ | ✅ |
| POST | ❌ | ❌ |
| PATCH | ❌ | ❌ (en général) |
@RestController@Controller + @ResponseBody : chaque méthode renvoie directement des données sérialisées en JSON, et non une vue HTML.
@Controller + @ResponseBody : chaque méthode renvoie directement des données sérialisées en JSON, et non une vue HTML.
@RestController
@RequestMapping("/api/v1/clients")
public class ClientController {
private final ClientService service;
public ClientController(ClientService service) { this.service = service; }
@GetMapping("/{id}")
public ClientDto get(@PathVariable Long id) { return service.get(id); } // → JSON
}@RequestMapping, @GetMapping, @PostMapping…Annotations qui associent une URL et un verbe HTTP à une méthode Java ; @RequestMapping sur la classe fixe le préfixe commun.
Annotations qui associent une URL et un verbe HTTP à une méthode Java ; @RequestMapping sur la classe fixe le préfixe commun.
@RestController
@RequestMapping("/api/v1/comptes") // préfixe
class CompteController {
@GetMapping List<CompteDto> lister() { … } // GET /api/v1/comptes
@PostMapping CompteDto creer(@RequestBody …) { … } // POST
@PatchMapping("/{id}/gel") void geler(@PathVariable Long id) { … } // PATCH
@DeleteMapping("/{id}") void supprimer(@PathVariable Long id) { … }
}@PathVariable vs @RequestParam@PathVariable lit une partie du chemin (/clients/{id}) pour identifier une ressource ; @RequestParam lit un paramètre de requête (?ville=Dakar) pour filtrer, trier ou paginer.
@PathVariable lit une partie du chemin (/clients/{id}) pour identifier une ressource ; @RequestParam lit un paramètre de requête (?ville=Dakar) pour filtrer, trier ou paginer.
@GetMapping("/clients/{id}") // GET /clients/42
ClientDto get(@PathVariable Long id) { … }
@GetMapping("/clients") // GET /clients?ville=Dakar&actif=true
List<ClientDto> chercher(@RequestParam String ville,
@RequestParam(defaultValue = "true") boolean actif) { … }@RequestBodyIndique que le corps JSON de la requête doit être désérialisé (par Jackson) dans l'objet Java du paramètre.
Indique que le corps JSON de la requête doit être désérialisé (par Jackson) dans l'objet Java du paramètre.
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
ClientDto creer(@Valid @RequestBody CreateClientRequest req) { return service.creer(req); }POST /api/v1/clients
Content-Type: application/json
{"nom": "Awa Ndiaye", "email": "awa@exemple.sn"}ResponseEntityObjet qui représente la réponse HTTP complète : code de statut, en-têtes et corps. Utile quand le code ou les headers varient.
Objet qui représente la réponse HTTP complète : code de statut, en-têtes et corps. Utile quand le code ou les headers varient.
@PostMapping
ResponseEntity<ClientDto> creer(@Valid @RequestBody CreateClientRequest req) {
ClientDto cree = service.creer(req);
URI location = URI.create("/api/v1/clients/" + cree.id());
return ResponseEntity.created(location).body(cree); // 201 + header Location
}
@GetMapping("/{id}")
ResponseEntity<ClientDto> get(@PathVariable Long id) {
return service.chercher(id).map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()); // 404
}@ResponseStatusFixe le code HTTP renvoyé par une méthode de contrôleur (ex. 201 Created) ou associé à une exception.
Fixe le code HTTP renvoyé par une méthode de contrôleur (ex. 201 Created) ou associé à une exception.
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 au lieu de 200
OrderDto create(@Valid @RequestBody CreateOrderRequest req) { … }
@ResponseStatus(HttpStatus.CONFLICT) // 409 quand cette exception remonte
class EmailDejaUtiliseException extends RuntimeException { … }Pagination avec PageableSpring Data lit automatiquement ?page=0&size=20&sort=nom,asc dans un objet Pageable et renvoie une Page (contenu + nombre total d'éléments).
Spring Data lit automatiquement ?page=0&size=20&sort=nom,asc dans un objet Pageable et renvoie une Page (contenu + nombre total d'éléments).
@GetMapping
Page<ClientDto> lister(@PageableDefault(size = 20, sort = "nom") Pageable pageable) {
return repo.findAll(pageable).map(mapper::toDto);
}spring.data.web.pageable.max-page-size=100) : sinon ?size=1000000 peut faire tomber le service.DTO (Data Transfer Object)Objet simple qui transporte les données entre couches ou sur le réseau ; c'est le contrat de l'API, distinct de l'entité JPA.
Objet simple qui transporte les données entre couches ou sur le réseau ; c'est le contrat de l'API, distinct de l'entité JPA.
Le DTO expose uniquement ce que le client doit voir (pas de mot de passe haché, pas de relations lazy), reste stable même si le schéma de base change, et porte les annotations de validation. Un record est idéal.
public record ClientDto(Long id, String nom, String email, String ville) {}
public record CreateClientRequest(@NotBlank String nom, @Email String email) {}Mapper DTO ↔ Entity (MapStruct)Composant qui convertit entité ↔ DTO ; MapStruct génère ce code à la compilation à partir d'une interface, sans réflexion.
Composant qui convertit entité ↔ DTO ; MapStruct génère ce code à la compilation à partir d'une interface, sans réflexion.
@Mapper(componentModel = "spring")
public interface ClientMapper {
ClientDto toDto(Client entity);
@Mapping(target = "id", ignore = true)
Client toEntity(CreateClientRequest req);
}4Validation & gestion des erreurs4
Bean Validation (@Valid, @NotBlank, @Email, @Size)Standard Jakarta Validation : on annote les champs du DTO avec des contraintes, et @Valid déclenche leur vérification avant l'exécution du contrôleur.
Standard Jakarta Validation : on annote les champs du DTO avec des contraintes, et @Valid déclenche leur vérification avant l'exécution du contrôleur.
Si une contrainte échoue, Spring lève MethodArgumentNotValidException (→ 400). Contraintes courantes : @NotNull, @NotBlank, @Size, @Email, @Pattern, @Positive, @DecimalMin, @Past.
public record VirementRequest(
@NotBlank @Pattern(regexp = "SN\\d{2}[A-Z0-9]{20,}") String ibanDest,
@NotNull @Positive @Digits(integer = 12, fraction = 0) BigDecimal montant,
@Size(max = 140) String motif) {}
@PostMapping("/virements")
VirementDto virer(@Valid @RequestBody VirementRequest req) { … }MethodArgumentNotValidExceptionException levée par Spring quand un objet annoté @Valid ne respecte pas ses contraintes ; son BindingResult contient la liste des champs en erreur.
Exception levée par Spring quand un objet annoté @Valid ne respecte pas ses contraintes ; son BindingResult contient la liste des champs en erreur.
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
Map<String, String> erreurs(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (a, b) -> a));
} // {"montant": "doit être supérieur à 0"}@RestControllerAdvice et @ExceptionHandler@RestControllerAdvice centralise la gestion des erreurs de tous les contrôleurs ; chaque méthode @ExceptionHandler traduit un type d'exception en réponse HTTP cohérente.
@RestControllerAdvice centralise la gestion des erreurs de tous les contrôleurs ; chaque méthode @ExceptionHandler traduit un type d'exception en réponse HTTP cohérente.
Sans cela, chaque contrôleur ferait ses try/catch et les formats d'erreur divergeraient. On mappe : validation → 400, NotFound → 404, conflit métier → 409, exception inattendue → 500 (sans détail technique, mais loggée).
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<ProblemDetail> notFound(NotFoundException ex) {
return ResponseEntity.status(404).body(ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()));
}
@ExceptionHandler(Exception.class)
ResponseEntity<ProblemDetail> inattendu(Exception ex) {
log.error("Erreur non gérée", ex); // détail dans les logs, pas au client
return ResponseEntity.status(500).body(ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR));
}
}ProblemDetail (RFC 9457)Format standard de réponse d'erreur JSON (type, title, status, detail, instance) supporté nativement par Spring 6+.
Format standard de réponse d'erreur JSON (type, title, status, detail, instance) supporté nativement par Spring 6+.
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "Solde insuffisant");
pd.setTitle("Virement refusé");
pd.setProperty("code", "SOLDE_INSUFFISANT");{ "type": "about:blank", "title": "Virement refusé", "status": 409,
"detail": "Solde insuffisant", "instance": "/api/v1/virements", "code": "SOLDE_INSUFFISANT" }spring.mvc.problemdetails.enabled=true fait renvoyer ce format pour les erreurs standard de Spring MVC.5Données avec JPA16
JPA vs Hibernate vs Spring Data JPAJPA est la spécification Java de mapping objet-relationnel (ORM) ; Hibernate en est l'implémentation la plus utilisée ; Spring Data JPA génère les repositories au-dessus.
JPA est la spécification Java de mapping objet-relationnel (ORM) ; Hibernate en est l'implémentation la plus utilisée ; Spring Data JPA génère les repositories au-dessus.
L'ORM traduit les objets Java en lignes SQL et inversement. On écrit repo.save(client) et Hibernate génère l'INSERT. Il faut quand même comprendre le SQL produit (activer les logs) pour éviter les problèmes de performance.
spring:
jpa:
show-sql: true # en dev seulement
properties.hibernate.format_sql: true
open-in-view: falseEntité JPA (@Entity, @Id)Classe Java mappée sur une table : @Entity la déclare, @Id marque la clé primaire, @Column/@Table ajustent le mapping.
Classe Java mappée sur une table : @Entity la déclare, @Id marque la clé primaire, @Column/@Table ajustent le mapping.
@Entity
@Table(name = "compte")
public class Compte {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 34)
private String iban;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal solde;
@Version
private Long version; // verrouillage optimiste
protected Compte() {} // requis par JPA
}BigDecimal + colonne NUMERIC(15,2), jamais double.Repository Spring Data (JpaRepository)Interface qui hérite de JpaRepository<Entité, TypeId> ; Spring génère l'implémentation avec save, findById, findAll, delete, pagination…
Interface qui hérite de JpaRepository<Entité, TypeId> ; Spring génère l'implémentation avec save, findById, findAll, delete, pagination…
public interface CompteRepository extends JpaRepository<Compte, Long> {
Optional<Compte> findByIban(String iban); // requête dérivée du nom
List<Compte> findByClientIdAndStatut(Long clientId, Statut statut);
boolean existsByIban(String iban);
}Requêtes dérivées du nom de méthodeSpring Data construit la requête à partir du nom de la méthode : findByStatutAndVille, countByActifTrue, findTop10ByOrderByDateDesc.
Spring Data construit la requête à partir du nom de la méthode : findByStatutAndVille, countByActifTrue, findTop10ByOrderByDateDesc.
List<Commande> findByStatutAndDateCreationAfter(Statut s, LocalDate d);
// → SELECT c FROM Commande c WHERE c.statut = ?1 AND c.dateCreation > ?2
long countByStatut(Statut s);
List<Commande> findTop10ByClientIdOrderByDateCreationDesc(Long clientId);@Query ou à une Specification.@Query et @Param (JPQL)@Query écrit explicitement la requête en JPQL (sur les entités) ou en SQL natif ; @Param lie un paramètre nommé.
@Query écrit explicitement la requête en JPQL (sur les entités) ou en SQL natif ; @Param lie un paramètre nommé.
JPQL manipule des entités et leurs attributs (o.status), pas des tables et colonnes.
@Query("SELECT o FROM Order o WHERE o.status = :s AND o.total > :min")
List<Order> findGrosses(@Param("s") Status s, @Param("min") BigDecimal min);
@Query(value = "SELECT * FROM commande WHERE created_at > now() - interval '1 day'", nativeQuery = true)
List<Order> dernieres24h();
@Modifying
@Query("UPDATE Compte c SET c.statut = 'GELE' WHERE c.id = :id")
int geler(@Param("id") Long id);Relations @OneToMany / @ManyToOneAnnotations qui mappent les liens entre entités ; le côté @ManyToOne porte la clé étrangère, le côté @OneToMany (mappedBy) est l'inverse.
Annotations qui mappent les liens entre entités ; le côté @ManyToOne porte la clé étrangère, le côté @OneToMany (mappedBy) est l'inverse.
@Entity class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLine> lines = new ArrayList<>();
}
@Entity class OrderLine {
@ManyToOne(fetch = FetchType.LAZY) // FK order_id dans la table order_line
@JoinColumn(name = "order_id")
private Order order;
}@ManyToOne est EAGER par défaut : toujours forcer fetch = FetchType.LAZY.Lazy vs Eager loadingLAZY charge une relation seulement au premier accès (proxy) ; EAGER la charge immédiatement avec l'entité. Par défaut : @OneToMany LAZY, @ManyToOne EAGER.
LAZY charge une relation seulement au premier accès (proxy) ; EAGER la charge immédiatement avec l'entité. Par défaut : @OneToMany LAZY, @ManyToOne EAGER.
La bonne pratique est tout en LAZY, puis charger explicitement ce dont un cas d'usage a besoin (JOIN FETCH, @EntityGraph). EAGER partout provoque des requêtes inutiles et souvent des N+1 cachés.
@ManyToOne(fetch = FetchType.LAZY)
private Client client; // client.getNom() déclenche le SELECT au moment de l'accèsProblème N+1Anti-pattern où l'on exécute 1 requête pour charger N parents, puis 1 requête supplémentaire par parent pour charger ses enfants : N+1 requêtes au lieu d'une ou deux.
Anti-pattern où l'on exécute 1 requête pour charger N parents, puis 1 requête supplémentaire par parent pour charger ses enfants : N+1 requêtes au lieu d'une ou deux.
Typique avec des relations LAZY parcourues dans une boucle ou lors de la sérialisation JSON. Invisible en dev avec 10 lignes, catastrophique en production avec 10 000.
List<Order> orders = repo.findAll(); // 1 requête
for (Order o : orders) {
o.getLines().size(); // + 1 requête PAR commande → N requêtes
}show-sql), statistiques Hibernate (hibernate.generate_statistics=true) ou un test qui compte les requêtes. Correction : JOIN FETCH, @EntityGraph ou @BatchSize.JOIN FETCHClause JPQL qui charge l'entité et sa relation dans la même requête SQL (jointure), ce qui supprime le N+1 pour ce cas d'usage.
Clause JPQL qui charge l'entité et sa relation dans la même requête SQL (jointure), ce qui supprime le N+1 pour ce cas d'usage.
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.lines WHERE o.status = :s")
List<Order> findWithLines(@Param("s") Status s);@EntityGraphAnnotation qui indique quelles relations charger en même temps que l'entité pour une méthode de repository, sans écrire de JPQL.
Annotation qui indique quelles relations charger en même temps que l'entité pour une méthode de repository, sans écrire de JPQL.
@EntityGraph(attributePaths = {"lines", "customer"})
Optional<Order> findById(Long id);
@EntityGraph(attributePaths = "customer")
List<Order> findByStatus(Status status);@BatchSizeAnnotation Hibernate qui charge les relations LAZY par lots (ex. 50 parents à la fois avec un IN (...)) : N+1 devient 1 + N/50 requêtes.
Annotation Hibernate qui charge les relations LAZY par lots (ex. 50 parents à la fois avec un IN (...)) : N+1 devient 1 + N/50 requêtes.
@OneToMany(mappedBy = "order")
@BatchSize(size = 50)
private List<OrderLine> lines;spring.jpa.properties.hibernate.default_batch_fetch_size: 50 # globalLazyInitializationExceptionErreur levée quand on accède à une relation LAZY alors que la session Hibernate (transaction) est fermée, typiquement dans le contrôleur ou lors de la sérialisation JSON.
Erreur levée quand on accède à une relation LAZY alors que la session Hibernate (transaction) est fermée, typiquement dans le contrôleur ou lors de la sérialisation JSON.
@Transactional(readOnly = true)
public OrderDto get(Long id) {
Order o = repo.findById(id).orElseThrow();
return mapper.toDto(o); // ✅ mapping DANS la transaction : les lignes se chargent
}
// ❌ renvoyer l'entité au contrôleur puis o.getLines() hors transaction → LazyInitializationExceptionOpen Session In View (OSIV)Option activée par défaut dans Spring Boot qui garde la session Hibernate ouverte jusqu'à la fin de la requête HTTP ; elle masque les LazyInitializationException mais génère des requêtes cachées.
Option activée par défaut dans Spring Boot qui garde la session Hibernate ouverte jusqu'à la fin de la requête HTTP ; elle masque les LazyInitializationException mais génère des requêtes cachées.
spring.jpa.open-in-view: false # recommandé : charger explicitement dans le serviceContexte de persistance et dirty checkingLe contexte de persistance (EntityManager) suit les entités chargées dans une transaction ; au commit, Hibernate détecte leurs modifications (dirty checking) et génère les UPDATE automatiquement.
Le contexte de persistance (EntityManager) suit les entités chargées dans une transaction ; au commit, Hibernate détecte leurs modifications (dirty checking) et génère les UPDATE automatiquement.
@Transactional
public void renommer(Long id, String nom) {
Client c = repo.findById(id).orElseThrow();
c.setNom(nom); // pas de save() nécessaire : UPDATE généré au commit
}ProjectionsNe charger que les colonnes utiles en renvoyant une interface ou un record au lieu de l'entité complète ; plus rapide et pas de piège lazy.
Ne charger que les colonnes utiles en renvoyant une interface ou un record au lieu de l'entité complète ; plus rapide et pas de piège lazy.
public record ClientResume(Long id, String nom) {}
@Query("SELECT new sn.banque.ClientResume(c.id, c.nom) FROM Client c WHERE c.ville = :v")
List<ClientResume> resumes(@Param("v") String ville);Migrations de schéma (Flyway / Liquibase)Scripts versionnés qui font évoluer le schéma de base de façon reproductible sur tous les environnements ; exécutés au démarrage de l'application.
Scripts versionnés qui font évoluer le schéma de base de façon reproductible sur tous les environnements ; exécutés au démarrage de l'application.
-- src/main/resources/db/migration/V3__ajout_statut_compte.sql
ALTER TABLE compte ADD COLUMN statut VARCHAR(20) NOT NULL DEFAULT 'ACTIF';
CREATE INDEX idx_compte_statut ON compte(statut);spring.jpa.hibernate.ddl-auto=validate (ou none), jamais update ou create.6Transactions11
@TransactionalAnnotation qui exécute une méthode dans une transaction : commit si elle se termine normalement, rollback si une RuntimeException est levée.
Annotation qui exécute une méthode dans une transaction : commit si elle se termine normalement, rollback si une RuntimeException est levée.
Elle se place sur les méthodes publiques des services (pas sur les contrôleurs). Spring l'applique via un Proxy Spring qui ouvre la transaction avant l'appel et la termine après.
@Service
public class VirementService {
@Transactional
public void virer(Long source, Long dest, BigDecimal montant) {
Compte a = repo.findById(source).orElseThrow();
Compte b = repo.findById(dest).orElseThrow();
a.debiter(montant);
b.crediter(montant); // si exception ici → le débit est annulé (rollback)
}
}Proxy Spring (JDK dynamic proxy / CGLIB)Objet intermédiaire généré par Spring qui enveloppe ton bean : il exécute du code avant/après chaque appel externe (transaction, sécurité, cache, @Async).
Objet intermédiaire généré par Spring qui enveloppe ton bean : il exécute du code avant/après chaque appel externe (transaction, sécurité, cache, @Async).
L'appelant reçoit le proxy, pas l'instance réelle. Spring utilise CGLIB (sous-classe générée) par défaut dans Boot. Conséquences : les méthodes private ou final ne sont pas interceptées, et un appel interne this.methode() contourne le proxy.
Contrôleur ──► [Proxy VirementService] ──► begin tx ──► VirementService.virer() ──► commit/rollbackAOP (Programmation orientée aspect)Technique qui ajoute un comportement transversal (logs, transactions, sécurité, métriques) autour de méthodes sans modifier leur code.
Technique qui ajoute un comportement transversal (logs, transactions, sécurité, métriques) autour de méthodes sans modifier leur code.
@Transactional, @Cacheable, @PreAuthorize, @Async sont implémentés par AOP. On peut écrire ses propres aspects.
@Aspect @Component
public class DureeAspect {
@Around("@annotation(sn.banque.Chronometre)")
public Object mesurer(ProceedingJoinPoint pjp) throws Throwable {
long t0 = System.nanoTime();
try { return pjp.proceed(); }
finally { log.info("{} : {} ms", pjp.getSignature(), (System.nanoTime() - t0) / 1_000_000); }
}
}Auto-invocation (proxy bypass)Quand une méthode d'un bean appelle une autre méthode du même bean (this.methode()), l'appel ne passe pas par le proxy : @Transactional, @Cacheable, @Async de la méthode appelée sont ignorés.
Quand une méthode d'un bean appelle une autre méthode du même bean (this.methode()), l'appel ne passe pas par le proxy : @Transactional, @Cacheable, @Async de la méthode appelée sont ignorés.
@Service
public class PaiementService {
public void traiter() {
journaliser(); // ❌ appel interne : pas de REQUIRES_NEW
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void journaliser() { … }
}@Lazy sur le paramètre pour éviter le cycle).Self-injectionInjecter un bean dans lui-même pour appeler ses méthodes via le proxy ; contournement du problème d'auto-invocation.
Injecter un bean dans lui-même pour appeler ses méthodes via le proxy ; contournement du problème d'auto-invocation.
@Service
public class PaiementService {
private final PaiementService self;
public PaiementService(@Lazy PaiementService self) { this.self = self; } // @Lazy évite la dépendance circulaire
public void traiter() { self.journaliser(); } // ✅ passe par le proxy
@Transactional(propagation = Propagation.REQUIRES_NEW) public void journaliser() { … }
}JournalService est plus clair.Propagation REQUIRED (défaut)Rejoint la transaction en cours s'il y en a une, sinon en crée une nouvelle. Une erreur n'importe où annule tout l'ensemble.
Rejoint la transaction en cours s'il y en a une, sinon en crée une nouvelle. Une erreur n'importe où annule tout l'ensemble.
@Transactional // = propagation REQUIRED
public void commander() {
stockService.reserver(); // @Transactional aussi → même transaction
paiementService.debiter(); // si échec → la réservation est annulée aussi
}Propagation REQUIRES_NEWSuspend la transaction en cours et en ouvre une nouvelle, indépendante : son commit ou rollback n'affecte pas l'appelant.
Suspend la transaction en cours et en ouvre une nouvelle, indépendante : son commit ou rollback n'affecte pas l'appelant.
Cas classique : écrire un journal d'audit qui doit être conservé même si l'opération principale échoue.
@Service class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void tracer(String evenement) { auditRepo.save(new Audit(evenement)); } // commit immédiat
}Autres propagations : SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER, NESTEDSUPPORTS utilise une transaction si elle existe ; NOT_SUPPORTED suspend toute transaction ; MANDATORY exige qu'une transaction existe ; NEVER interdit qu'elle existe ; NESTED crée un savepoint.
SUPPORTS utilise une transaction si elle existe ; NOT_SUPPORTED suspend toute transaction ; MANDATORY exige qu'une transaction existe ; NEVER interdit qu'elle existe ; NESTED crée un savepoint.
| Propagation | Transaction existante | Pas de transaction |
|---|---|---|
| REQUIRED | rejoint | crée |
| REQUIRES_NEW | suspend + crée | crée |
| SUPPORTS | rejoint | s'exécute sans |
| NOT_SUPPORTED | suspend | s'exécute sans |
| MANDATORY | rejoint | exception |
| NEVER | exception | s'exécute sans |
| NESTED | savepoint | crée |
Règles de rollback (rollbackFor)Par défaut, Spring annule la transaction sur RuntimeException et Error, mais commit sur exception checked. rollbackFor étend ce comportement.
Par défaut, Spring annule la transaction sur RuntimeException et Error, mais commit sur exception checked. rollbackFor étend ce comportement.
@Transactional(rollbackFor = Exception.class) // rollback aussi sur les checked
public void importer(Path fichier) throws IOException { … }try { … } catch (Exception e) { log… }) empêche le rollback : la transaction ne voit jamais l'erreur.@Transactional(readOnly = true)Marque une transaction en lecture seule : Hibernate désactive le dirty checking (moins de mémoire, plus rapide) et certains drivers routent vers un réplica.
Marque une transaction en lecture seule : Hibernate désactive le dirty checking (moins de mémoire, plus rapide) et certains drivers routent vers un réplica.
@Service
@Transactional(readOnly = true) // défaut pour toute la classe
public class RapportService {
public List<LigneRapport> mensuel(YearMonth m) { … }
@Transactional // écriture : surcharge locale
public void archiver(YearMonth m) { … }
}@Cacheable (abstraction de cache Spring)Met en cache le résultat d'une méthode selon ses paramètres ; les appels suivants avec la même clé ne réexécutent pas la méthode. @CacheEvict invalide.
Met en cache le résultat d'une méthode selon ses paramètres ; les appels suivants avec la même clé ne réexécutent pas la méthode. @CacheEvict invalide.
@EnableCaching @Configuration class CacheConfig {}
@Cacheable(cacheNames = "taux", key = "#devise")
public TauxDto taux(String devise) { return client.taux(devise); } // 1er appel seulement
@CacheEvict(cacheNames = "taux", allEntries = true)
@Scheduled(cron = "0 0 6 * * *")
public void rafraichir() {}7Spring Security8
Spring Security et SecurityFilterChainFramework d'authentification et d'autorisation basé sur une chaîne de filtres Servlet exécutés avant les contrôleurs ; on la configure avec un bean SecurityFilterChain.
Framework d'authentification et d'autorisation basé sur une chaîne de filtres Servlet exécutés avant les contrôleurs ; on la configure avec un bean SecurityFilterChain.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // API stateless JWT
.cors(Customizer.withDefaults())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/virements").hasRole("CLIENT")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
}CORS (côté Spring)Cross-Origin Resource Sharing : règle du navigateur qui bloque les appels vers un autre domaine sauf si le serveur l'autorise via des en-têtes Access-Control-Allow-*. Ce n'est pas une sécurité d'authentification.
Cross-Origin Resource Sharing : règle du navigateur qui bloque les appels vers un autre domaine sauf si le serveur l'autorise via des en-têtes Access-Control-Allow-*. Ce n'est pas une sécurité d'authentification.
Une SPA Angular sur https://app.banque.sn qui appelle https://api.banque.sn est « cross-origin ». Le navigateur envoie d'abord une requête OPTIONS (preflight) ; Spring doit répondre avec les origines, méthodes et en-têtes autorisés.
@Bean
CorsConfigurationSource corsConfigurationSource() {
var cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of("https://app.banque.sn", "http://localhost:4200"));
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
cfg.setAllowedHeaders(List.of("Authorization", "Content-Type"));
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", cfg);
return source;
}CSRFCross-Site Request Forgery : un site malveillant fait envoyer par le navigateur de la victime une requête authentifiée (les cookies partent automatiquement). Se protège par un jeton CSRF ou des cookies SameSite.
Cross-Site Request Forgery : un site malveillant fait envoyer par le navigateur de la victime une requête authentifiée (les cookies partent automatiquement). Se protège par un jeton CSRF ou des cookies SameSite.
Concerne les applications qui s'authentifient par cookie de session. Une API qui exige Authorization: Bearer <JWT> n'est pas exposée : un site tiers ne peut pas ajouter cet en-tête. D'où csrf.disable() en API stateless.
// Application à session cookie : garder CSRF, exposer le jeton au front Angular
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
// Angular lit le cookie XSRF-TOKEN et renvoie le header X-XSRF-TOKEN automatiquementPasswordEncoder et BCryptBCrypt est une fonction de hachage de mot de passe lente et salée, avec un facteur de coût réglable ; PasswordEncoder est l'interface Spring qui l'encapsule.
BCrypt est une fonction de hachage de mot de passe lente et salée, avec un facteur de coût réglable ; PasswordEncoder est l'interface Spring qui l'encapsule.
Le sel (aléatoire, intégré au hash) empêche les tables précalculées ; la lenteur volontaire (coût 10-12) rend le brute force très coûteux. On ne « déchiffre » jamais un mot de passe : on compare avec matches().
@Bean PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); // {bcrypt} par défaut
}
String hash = encoder.encode("MotDePasse!2026"); // {bcrypt}$2a$10$Qm… (sel inclus)
encoder.matches("MotDePasse!2026", hash); // trueAPI stateless (SessionCreationPolicy.STATELESS)Le serveur ne conserve aucune session : chaque requête porte sa preuve d'authentification (JWT). Facilite la montée en charge horizontale (n'importe quel pod peut répondre).
Le serveur ne conserve aucune session : chaque requête porte sa preuve d'authentification (JWT). Facilite la montée en charge horizontale (n'importe quel pod peut répondre).
http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));OAuth2 Resource Server (validation JWT)Configuration Spring Security où l'API valide les JWT émis par un serveur d'autorisation (Keycloak) : signature via JWKS, expiration, émetteur.
Configuration Spring Security où l'API valide les JWT émis par un serveur d'autorisation (Keycloak) : signature via JWKS, expiration, émetteur.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.banque.sn/realms/banque # Spring récupère les clés publiques (JWKS)http.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));realm_access.roles : il faut un JwtAuthenticationConverter pour les transformer en ROLE_… Spring.@PreAuthorize (sécurité des méthodes)Vérifie une règle d'autorisation (rôle, propriété de la ressource) avant l'exécution d'une méthode, avec une expression SpEL.
Vérifie une règle d'autorisation (rôle, propriété de la ressource) avant l'exécution d'une méthode, avec une expression SpEL.
@PreAuthorize("hasRole('CONSEILLER')")
public List<ClientDto> portefeuille() { … }
@PreAuthorize("#compteId == authentication.principal.claims['compte_id'] or hasRole('ADMIN')")
public SoldeDto solde(Long compteId) { … } // un client ne voit que SON compteSecurityContextConteneur (par requête) de l'utilisateur authentifié et de ses autorités ; rempli par les filtres de sécurité, lisible partout dans le traitement.
Conteneur (par requête) de l'utilisateur authentifié et de ses autorités ; rempli par les filtres de sécurité, lisible partout dans le traitement.
@GetMapping("/moi")
ProfilDto moi(@AuthenticationPrincipal Jwt jwt) {
return new ProfilDto(jwt.getSubject(), jwt.getClaimAsString("email"));
}
// ou : SecurityContextHolder.getContext().getAuthentication().getName()📋 Synthèse express — Spring
🗓 Versions Spring Boot
| Version | Date | Java | Nouveautés clés |
|---|---|---|---|
| 2.7 | 2022 | 11+ | Dernière branche 2.x, support Kotlin, Actuator amélioré |
| 3.0 | Déc 2022 | 17+ | Jakarta EE 10 (javax→jakarta), AOT, GraalVM natif |
| 3.2 | Nov 2023 | 17+ | RestClient, JdbcClient, support des Virtual Threads (opt-in : spring.threads.virtual.enabled) |
| 3.4 | Nov 2024 | 17+ | Logs structurés, @MockitoBean remplace @MockBean, arrêt gracieux par défaut |
| 3.5 | Mai 2025 | 17+ | Dernière version mineure de la ligne 3.x |
| 4.0 | Nov 2025 | 17+ (25 recommandé) | Spring Framework 7, Jakarta EE 11, Jackson 3, versioning d'API, HTTP Service Clients, JSpecify, JUnit 6 |
| 4.1 | 2026 | 17+ | RestTemplate déprécié (Framework 7.1), Spring gRPC, protection SSRF |
javax.* → jakarta.* (Servlet, Persistence, Validation…)
🔄 IoC & Injection de Dépendances
singleton— 1 instance/contexte (défaut)prototype— nouvelle instance à chaque injectrequest— 1/requête HTTPsession— 1/session HTTP
🌐 REST API — Bonnes pratiques
💾 JPA & N+1 Problem
@EntityGraph ou JOIN FETCH dans JPQL.
✅ Validation & Gestion d'erreurs globale
⚠️ @Transactional — Pièges à éviter
@Transactional appelle une autre méthode @Transactional de la même classe, le proxy Spring est court-circuité → la 2e transaction n'est PAS créée !
| Propagation | Comportement |
|---|---|
| REQUIRED (défaut) | Rejoint la transaction existante ou en crée une |
| REQUIRES_NEW | Suspend l'actuelle, crée une nouvelle (commit/rollback indépendant) |
| SUPPORTS | Utilise la transaction si elle existe, sinon sans |
| NOT_SUPPORTED | Suspend toute transaction active |
| NEVER | Exception si une transaction est active |
🛡️ Spring Security — CORS, CSRF, BCrypt
Cross-Origin Resource Sharing. Contrôle quelles origines peuvent appeler l'API depuis un navigateur. N'est pas un mécanisme d'authentification. Configurer côté serveur Spring avec @CrossOrigin ou CorsConfigurationSource.
Cross-Site Request Forgery. Concerne les requêtes authentifiées automatiquement (cookies). Désactivé par défaut en API stateless JWT car pas de cookie de session. Activer pour apps avec session cookie.
Algorithme adaptatif de hachage. Intègre un salt automatique. Coût configurable (factor 10-12 typique). Jamais MD5/SHA1 pour mots de passe — utilisés pour l'intégrité, pas le stockage de secrets.
🏗 Architecture couches — Règle de dépendance
DTO = contrat d'API (stable) · Entity = contrat de persistance (interne). Ne jamais exposer l'Entity JPA dans la réponse HTTP : fuite de données, couplage fort, risque de lazy load en sérialisation.
🆕 RestClient (Spring Boot 3.2+)
JOIN FETCH, @EntityGraph, ou batch fetching avec @BatchSize.spring.threads.virtual.enabled=true.this.methode()) ne passe pas par le proxy → les aspects (@Transactional, @Cacheable…) sont ignorés. Solution : injecter le service dans lui-même (@Autowired PaymentService self) ou séparer en deux beans distincts.Authorization: Bearer ... header, un site malveillant ne peut pas injecter ce header. En session cookie, il faut CSRF. En JWT stateless, désactiver CSRF est standard et sûr.1Nouveautés Angular 17 → 2219
Composant standaloneComposant autonome qui déclare lui-même ses dépendances dans imports, sans NgModule. Introduit en v14, standalone par défaut depuis v19.
Composant autonome qui déclare lui-même ses dépendances dans imports, sans NgModule. Introduit en v14, standalone par défaut depuis v19.
Avant, chaque composant devait être déclaré dans un NgModule, source de confusion (« dans quel module est-il déclaré ? »). Un composant standalone importe directement les composants, directives et pipes qu'il utilise ; l'application démarre avec bootstrapApplication.
@Component({
selector: 'app-compte-carte',
imports: [CurrencyPipe, RouterLink], // ses dépendances, ici et pas dans un module
template: `<a [routerLink]="['/comptes', compte().id]">{{ compte().solde | currency:'XOF' }}</a>`,
})
export class CompteCarteComponent {
compte = input.required<Compte>();
}standalone: true est implicite : il ne faut plus l'écrire.bootstrapApplication et providers fonctionnelsPoint d'entrée d'une application standalone : on démarre le composant racine et on enregistre les services globaux avec des fonctions provideXxx().
Point d'entrée d'une application standalone : on démarre le composant racine et on enregistre les services globaux avec des fonctions provideXxx().
// main.ts
bootstrapApplication(AppComponent, appConfig);
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor, erreurInterceptor])),
],
};Nouveau control flow (@if, @for, @switch)Syntaxe de template intégrée (v17, stable v18) qui remplace *ngIf, *ngFor et ngSwitch : plus lisible, typée, plus rapide et sans import.
Syntaxe de template intégrée (v17, stable v18) qui remplace *ngIf, *ngFor et ngSwitch : plus lisible, typée, plus rapide et sans import.
@if (client(); as c) {
<h2>{{ c.nom }}</h2>
} @else if (chargement()) {
<p>Chargement…</p>
} @else {
<p>Aucun client</p>
}
@switch (compte().statut) {
@case ('ACTIF') { <span class="ok">Actif</span> }
@case ('GELE') { <span class="ko">Gelé</span> }
@default { <span>Inconnu</span> }
}ng generate @angular/core:control-flow.track dans @for (et trackBy)Expression obligatoire dans @for qui identifie chaque élément (souvent son id) : Angular ne recrée que les lignes réellement ajoutées, supprimées ou déplacées.
Expression obligatoire dans @for qui identifie chaque élément (souvent son id) : Angular ne recrée que les lignes réellement ajoutées, supprimées ou déplacées.
Sans identifiant, quand la liste est rechargée depuis l'API (nouveaux objets), Angular détruit et recrée tout le DOM de la liste. Avec track t.id, il réutilise les nœuds existants. trackBy est l'équivalent pour l'ancien *ngFor.
@for (t of transactions(); track t.id; let i = $index) {
<li>{{ i + 1 }}. {{ t.libelle }} — {{ t.montant | number }}</li>
} @empty {
<li>Aucune transaction</li>
}@emptyBloc optionnel de @for affiché quand la liste est vide ; remplace un @if séparé.
Bloc optionnel de @for affiché quand la liste est vide ; remplace un @if séparé.
@for (b of beneficiaires(); track b.id) { <app-beneficiaire [b]="b" /> }
@empty { <p>Ajoutez votre premier bénéficiaire.</p> }@defer (vues différées)Bloc de template (v17, stable v18) dont le code est chargé à part (lazy) et affiché seulement quand un déclencheur survient : visibilité, interaction, inactivité, timer, condition.
Bloc de template (v17, stable v18) dont le code est chargé à part (lazy) et affiché seulement quand un déclencheur survient : visibilité, interaction, inactivité, timer, condition.
Le composant lourd (graphique, éditeur) sort du bundle initial : la page s'affiche plus vite. @placeholder s'affiche avant le déclenchement, @loading pendant le chargement, @error en cas d'échec.
@defer (on viewport; prefetch on idle) {
<app-graphique-depenses [donnees]="depenses()" />
} @placeholder (minimum 300ms) {
<div class="squelette"></div>
} @loading (after 100ms) {
<p>Chargement du graphique…</p>
} @error {
<p>Graphique indisponible</p>
}| Déclencheur | Quand |
|---|---|
on viewport | Le placeholder entre dans l'écran |
on interaction | Clic / focus sur le placeholder |
on idle | Le navigateur est inactif (défaut) |
on timer(2s) | Après un délai |
when cond() | Quand une condition devient vraie |
Signals : signal()Un signal est un conteneur de valeur réactive : on le lit en l'appelant count(), on l'écrit avec set() ou update(), et Angular sait précisément quels templates en dépendent.
Un signal est un conteneur de valeur réactive : on le lit en l'appelant count(), on l'écrit avec set() ou update(), et Angular sait précisément quels templates en dépendent.
Contrairement à Zone.js qui revérifie tout l'arbre après chaque événement, Angular met à jour uniquement les vues qui lisent le signal modifié. Introduits en v16, stables depuis v17 (et l'essentiel de l'API réactive stable en v20).
export class PanierComponent {
quantite = signal(1);
articles = signal<Article[]>([]);
ajouter(a: Article) {
this.articles.update(liste => [...liste, a]); // nouvelle référence (immutabilité)
}
reset() { this.quantite.set(1); }
}<p>Quantité : {{ quantite() }}</p>articles().push(a)) : la référence ne change pas, le signal ne notifie pas.computed()Signal dérivé, en lecture seule, recalculé automatiquement (et paresseusement, avec mémorisation) quand les signaux qu'il lit changent.
Signal dérivé, en lecture seule, recalculé automatiquement (et paresseusement, avec mémorisation) quand les signaux qu'il lit changent.
articles = signal<Article[]>([]);
total = computed(() => this.articles().reduce((s, a) => s + a.prix * a.qte, 0));
fraisLivraison = computed(() => this.total() > 50_000 ? 0 : 2_000);effect()Fonction exécutée à chaque fois qu'un signal qu'elle lit change ; sert aux effets de bord (log, localStorage, synchronisation avec une API non Angular).
Fonction exécutée à chaque fois qu'un signal qu'elle lit change ; sert aux effets de bord (log, localStorage, synchronisation avec une API non Angular).
constructor() {
effect(() => {
localStorage.setItem('filtre', JSON.stringify(this.filtre())); // relu à chaque changement
});
}computed() ; pour réinitialiser un état dépendant, linkedSignal(). Un effect qui écrit des signaux crée vite des boucles.linkedSignal()Signal modifiable dont la valeur est réinitialisée automatiquement quand une source change (v19, stable v20). Ex. : la sélection revient au premier élément quand la liste change.
Signal modifiable dont la valeur est réinitialisée automatiquement quand une source change (v19, stable v20). Ex. : la sélection revient au premier élément quand la liste change.
comptes = input.required<Compte[]>();
compteSelectionne = linkedSignal(() => this.comptes()[0]); // reset si la liste change
choisir(c: Compte) { this.compteSelectionne.set(c); } // reste modifiable à la maininput(), output() et model()Versions « signal » de @Input, @Output et du two-way binding (stables depuis v19) : input() renvoie un signal en lecture seule, output() émet un événement, model() est un input modifiable par l'enfant.
Versions « signal » de @Input, @Output et du two-way binding (stables depuis v19) : input() renvoie un signal en lecture seule, output() émet un événement, model() est un input modifiable par l'enfant.
export class FiltreComponent {
options = input<string[]>([]); // [options]="..."
min = input.required<number>(); // obligatoire, vérifié à la compilation
recherche = model(''); // [(recherche)]="terme" côté parent
valide = output<string>(); // (valide)="onValide($event)"
envoyer() { this.valide.emit(this.recherche()); }
}<app-filtre [min]="0" [(recherche)]="terme" (valide)="charger($event)" />resource() et httpResource()API pour charger des données asynchrones sous forme de signaux (value, isLoading, error) qui se rechargent quand leurs paramètres changent. Déclarées prêtes pour la production en v22.
API pour charger des données asynchrones sous forme de signaux (value, isLoading, error) qui se rechargent quand leurs paramètres changent. Déclarées prêtes pour la production en v22.
clientId = input.required<number>();
client = httpResource<Client>(() => `/api/v1/clients/${this.clientId()}`); // rechargé si l'id change@if (client.isLoading()) { <p>Chargement…</p> }
@else if (client.error()) { <p>Erreur</p> }
@else { <h2>{{ client.value()?.nom }}</h2> }Zone.jsBibliothèque qui « patche » les API asynchrones du navigateur (événements, setTimeout, XHR) pour prévenir Angular qu'il doit lancer la détection de changements.
Bibliothèque qui « patche » les API asynchrones du navigateur (événements, setTimeout, XHR) pour prévenir Angular qu'il doit lancer la détection de changements.
Simple mais coûteux : à chaque clic ou timer, Angular revérifie potentiellement tout l'arbre de composants. Elle complique aussi les stack traces et pèse sur le bundle. Depuis v21, elle n'est plus incluse par défaut dans les nouveaux projets.
ZonelessMode sans Zone.js (stable depuis v20.2, défaut des nouveaux projets en v21) : la détection de changements est déclenchée par les signals, les événements du template, l'async pipe et markForCheck.
Mode sans Zone.js (stable depuis v20.2, défaut des nouveaux projets en v21) : la détection de changements est déclenchée par les signals, les événements du template, l'async pipe et markForCheck.
// projet existant (≤ v20) : activation explicite
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});Signal FormsNouvelle API de formulaires basée sur les signals : le modèle est un signal, form() construit l'arbre de champs et un schéma déclare les validations. Expérimentale en v21, stable en v22.
Nouvelle API de formulaires basée sur les signals : le modèle est un signal, form() construit l'arbre de champs et un schéma déclare les validations. Expérimentale en v21, stable en v22.
import { form, required, email, minLength, FormField } from '@angular/forms/signals';
@Component({
imports: [FormField],
template: `
<input [formField]="inscription.nom" />
<input [formField]="inscription.email" />
@if (inscription.email().invalid()) { <small>Email invalide</small> }
`,
})
export class InscriptionComponent {
modele = signal({ nom: '', email: '' });
inscription = form(this.modele, (p) => {
required(p.nom); minLength(p.nom, 2);
required(p.email); email(p.email);
});
}[control] → [field] → [formField]) : vérifier la doc de la version utilisée.OnPush par défaut (v22) et ChangeDetectionStrategy.EagerDepuis Angular 22, les nouvelles applications utilisent OnPush par défaut ; l'ancienne stratégie « Default » est renommée Eager.
Depuis Angular 22, les nouvelles applications utilisent OnPush par défaut ; l'ancienne stratégie « Default » est renommée Eager.
Cela entérine la direction prise avec les signals : un composant ne se met à jour que si ses inputs changent, qu'un événement survient dans son template ou qu'un signal lu change.
@Service (Angular 22)Nouveau décorateur (v22) plus court pour un service singleton global, alternative à @Injectable({ providedIn: 'root' }).
Nouveau décorateur (v22) plus court pour un service singleton global, alternative à @Injectable({ providedIn: 'root' }).
@Service()
export class TauxService {
private http = inject(HttpClient);
}
// équivalent historique :
@Injectable({ providedIn: 'root' }) export class TauxService2 {}inject()Fonction qui récupère une dépendance depuis l'injecteur courant, utilisable dans un champ, un constructeur, un guard ou un intercepteur fonctionnel.
Fonction qui récupère une dépendance depuis l'injecteur courant, utilisable dans un champ, un constructeur, un guard ou un intercepteur fonctionnel.
export class ClientService {
private http = inject(HttpClient);
private config = inject(APP_CONFIG);
}
export const adminGuard: CanActivateFn = () => inject(AuthService).estAdmin();Vitest (runner de tests par défaut)Depuis Angular 21, les nouveaux projets utilisent Vitest à la place de Karma/Jasmine : exécution plus rapide, mode watch, API compatible Jest.
Depuis Angular 21, les nouveaux projets utilisent Vitest à la place de Karma/Jasmine : exécution plus rapide, mode watch, API compatible Jest.
import { describe, it, expect } from 'vitest';
describe('total', () => {
it('additionne les lignes', () => {
expect(total([{ prix: 1000, qte: 2 }])).toBe(2000);
});
});2Composants & templates12
Composant (@Component)Brique d'interface : une classe TypeScript (état + logique) associée à un template HTML et des styles, identifiée par un sélecteur.
Brique d'interface : une classe TypeScript (état + logique) associée à un template HTML et des styles, identifiée par un sélecteur.
@Component({
selector: 'app-solde', // <app-solde />
template: `<p>Solde : {{ solde() | number }} XOF</p>`,
styles: `p { font-weight: 600 }`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SoldeComponent {
solde = input(0);
}NgModule (historique)Ancien mécanisme de regroupement (declarations, imports, providers) ; remplacé par les composants standalone, mais encore présent dans beaucoup d'applications existantes.
Ancien mécanisme de regroupement (declarations, imports, providers) ; remplacé par les composants standalone, mais encore présent dans beaucoup d'applications existantes.
@NgModule({
declarations: [ListeComptesComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
})
export class ComptesModule {}CommonModule fournissait *ngIf, *ngFor et les pipes communs ; avec le nouveau control flow, on n'en a souvent plus besoin.Interpolation {{ }}Affiche dans le template la valeur d'une expression du composant, convertie en texte et échappée (protection XSS).
Affiche dans le template la valeur d'une expression du composant, convertie en texte et échappée (protection XSS).
<h1>Bonjour {{ utilisateur().prenom }}</h1>
<p>Frais : {{ montant() * 0.01 | number:'1.0-0' }} XOF</p>Property binding [ ][propriete]="expression" lie une propriété DOM ou un input de composant à une valeur du composant (sens composant → template).
[propriete]="expression" lie une propriété DOM ou un input de composant à une valeur du composant (sens composant → template).
<button [disabled]="enCours()">Valider</button>
<img [src]="avatarUrl()" [alt]="nom()" />
<app-graph [donnees]="serie()" />
<div [class.alerte]="solde() < 0" [style.width.%]="progression()"></div>Event binding ( )(evenement)="methode($event)" exécute du code du composant quand un événement DOM ou un output survient (sens template → composant).
(evenement)="methode($event)" exécute du code du composant quand un événement DOM ou un output survient (sens template → composant).
<button (click)="valider()">Valider</button>
<input (keyup.enter)="chercher(saisie.value)" #saisie />
<app-carte (selectionne)="ouvrir($event)" />Two-way binding [( )] et ngModel[(x)]="valeur" combine property binding et event binding : la valeur circule dans les deux sens. ngModel l'applique aux champs de formulaire (template-driven).
[(x)]="valeur" combine property binding et event binding : la valeur circule dans les deux sens. ngModel l'applique aux champs de formulaire (template-driven).
<input [(ngModel)]="recherche" /> <!-- nécessite FormsModule -->
<app-filtre [(recherche)]="terme" /> <!-- avec un model() côté enfant -->[(x)] est du sucre pour [x]="v" (xChange)="v = $event" (« banana in a box »).DirectiveClasse qui ajoute un comportement à un élément existant : directive d'attribut (change l'apparence/le comportement) ou structurelle (ajoute/retire du DOM, ex. l'ancien *ngIf).
Classe qui ajoute un comportement à un élément existant : directive d'attribut (change l'apparence/le comportement) ou structurelle (ajoute/retire du DOM, ex. l'ancien *ngIf).
@Directive({ selector: '[appMontantNegatif]', host: { '[class.negatif]': 'valeur() < 0' } })
export class MontantNegatifDirective {
valeur = input.required<number>({ alias: 'appMontantNegatif' });
}<span [appMontantNegatif]="solde()">{{ solde() }}</span>PipeFonction de transformation utilisée dans le template avec | (date, currency, number, uppercase…) ; on peut créer les siens. « Pure » par défaut : recalculée seulement si l'entrée change.
Fonction de transformation utilisée dans le template avec | (date, currency, number, uppercase…) ; on peut créer les siens. « Pure » par défaut : recalculée seulement si l'entrée change.
@Pipe({ name: 'iban' })
export class IbanPipe implements PipeTransform {
transform(v: string): string { return v.replace(/(.{4})/g, '$1 ').trim(); } // groupes de 4
}{{ compte.iban | iban }} · {{ compte.ouverture | date:'dd/MM/yyyy' }} · {{ solde | currency:'XOF':'symbol':'1.0-0' }}@InputDécorateur (API classique) qui expose une propriété d'un composant enfant pour recevoir une valeur du parent ; required: true (v16) la rend obligatoire.
Décorateur (API classique) qui expose une propriété d'un composant enfant pour recevoir une valeur du parent ; required: true (v16) la rend obligatoire.
export class CardComponent {
@Input({ required: true }) user!: User;
@Input() compact = false;
}input() / input.required() qui donnent un signal.@Output et EventEmitter@Output expose un événement que l'enfant émet vers le parent via un EventEmitter ; le parent l'écoute avec (nomEvenement).
@Output expose un événement que l'enfant émet vers le parent via un EventEmitter ; le parent l'écoute avec (nomEvenement).
export class CardComponent {
@Output() selected = new EventEmitter<User>();
select(u: User) { this.selected.emit(u); }
}<app-card [user]="u" (selected)="onUserSelected($event)" />Projection de contenu (ng-content)Permet au parent d'insérer son propre HTML à l'intérieur d'un composant enfant (comme un « slot »), éventuellement à plusieurs emplacements via select.
Permet au parent d'insérer son propre HTML à l'intérieur d'un composant enfant (comme un « slot »), éventuellement à plusieurs emplacements via select.
@Component({
selector: 'app-panneau',
template: `
<header><ng-content select="[titre]" /></header>
<section><ng-content /></section>`,
})
export class PanneauComponent {}<app-panneau>
<h3 titre>Mes comptes</h3>
<app-liste-comptes />
</app-panneau>ViewChild et viewChild()Donne au composant une référence vers un élément ou un composant enfant de son template ; disponible à partir de ngAfterViewInit (ou via le signal viewChild()).
Donne au composant une référence vers un élément ou un composant enfant de son template ; disponible à partir de ngAfterViewInit (ou via le signal viewChild()).
export class RechercheComponent {
champ = viewChild.required<ElementRef<HTMLInputElement>>('saisie');
focus() { this.champ().nativeElement.focus(); }
}<input #saisie />3Cycle de vie des composants10
Lifecycle hooks (vue d'ensemble)Méthodes qu'Angular appelle à des moments précis de la vie d'un composant : création, changement d'inputs, rendu, destruction.
Méthodes qu'Angular appelle à des moments précis de la vie d'un composant : création, changement d'inputs, rendu, destruction.
Ordre : constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → … → ngOnDestroy. Avec les signals, on en utilise de moins en moins (computed/effect/DestroyRef remplacent beaucoup de cas).
constructor (composant)Appelé à la création de la classe : sert uniquement à l'injection de dépendances ; les inputs ne sont pas encore disponibles.
Appelé à la création de la classe : sert uniquement à l'injection de dépendances ; les inputs ne sont pas encore disponibles.
constructor(private service: ClientService) {} // ✅ DI
// ❌ ne pas appeler l'API ici : les @Input ne sont pas encore renseignésngOnChanges et SimpleChangesAppelé avant ngOnInit puis à chaque fois qu'un @Input change de référence ; reçoit un objet SimpleChanges (valeur précédente / actuelle).
Appelé avant ngOnInit puis à chaque fois qu'un @Input change de référence ; reçoit un objet SimpleChanges (valeur précédente / actuelle).
ngOnChanges(changes: SimpleChanges) {
if (changes['compteId'] && !changes['compteId'].firstChange) {
this.chargerTransactions(changes['compteId'].currentValue);
}
}input(), on remplace souvent ngOnChanges par un computed() ou un httpResource() qui réagit tout seul.ngOnInitAppelé une fois, après le premier ngOnChanges : les inputs sont disponibles. C'est l'endroit pour charger les données initiales et s'abonner aux services.
Appelé une fois, après le premier ngOnChanges : les inputs sont disponibles. C'est l'endroit pour charger les données initiales et s'abonner aux services.
export class ListeComptesComponent implements OnInit {
private service = inject(CompteService);
comptes = signal<Compte[]>([]);
ngOnInit() {
this.service.lister().subscribe(c => this.comptes.set(c));
}
}ngDoCheckAppelé à chaque cycle de détection de changements ; permet une détection personnalisée. Très fréquent, donc à éviter ou garder ultra léger.
Appelé à chaque cycle de détection de changements ; permet une détection personnalisée. Très fréquent, donc à éviter ou garder ultra léger.
ngAfterContentInitAppelé une fois après que le contenu projeté (ng-content) a été initialisé ; @ContentChild est alors disponible.
Appelé une fois après que le contenu projeté (ng-content) a été initialisé ; @ContentChild est alors disponible.
ngAfterViewInitAppelé une fois après le rendu de la vue du composant et de ses enfants : les @ViewChild sont disponibles, on peut manipuler le DOM (initialiser un graphique, un focus).
Appelé une fois après le rendu de la vue du composant et de ses enfants : les @ViewChild sont disponibles, on peut manipuler le DOM (initialiser un graphique, un focus).
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
ngAfterViewInit() { this.chart = new Chart(this.canvas.nativeElement, this.config); }ngOnDestroyAppelé juste avant la destruction du composant : on y libère les ressources (désabonnements, timers, listeners globaux) pour éviter les fuites mémoire.
Appelé juste avant la destruction du composant : on y libère les ressources (désabonnements, timers, listeners globaux) pour éviter les fuites mémoire.
private sub?: Subscription;
private timer?: number;
ngOnInit() {
this.sub = this.flux.subscribe(…);
this.timer = window.setInterval(() => this.rafraichir(), 30_000);
}
ngOnDestroy() {
this.sub?.unsubscribe();
clearInterval(this.timer);
}DestroyRef et takeUntilDestroyedOpérateur RxJS (v16, @angular/core/rxjs-interop) qui se désabonne automatiquement à la destruction du composant ; plus besoin de ngOnDestroy pour les abonnements.
Opérateur RxJS (v16, @angular/core/rxjs-interop) qui se désabonne automatiquement à la destruction du composant ; plus besoin de ngOnDestroy pour les abonnements.
export class NotificationsComponent {
constructor() {
inject(NotificationService).flux$
.pipe(takeUntilDestroyed()) // dans un contexte d'injection
.subscribe(n => this.afficher(n));
}
}Fuite mémoire côté AngularUn abonnement, un interval ou un listener jamais libéré continue de vivre après la destruction du composant : mémoire qui grimpe et traitements fantômes.
Un abonnement, un interval ou un listener jamais libéré continue de vivre après la destruction du composant : mémoire qui grimpe et traitements fantômes.
Les observables HTTP se terminent seuls (une valeur puis complete), mais pas les flux infinis : interval, fromEvent, WebSocket, Subjects de service, valueChanges des formulaires. Remèdes : async pipe, takeUntilDestroyed, toSignal, ou ngOnDestroy.
4Formulaires6
Reactive Forms (FormGroup, FormControl)Formulaires définis dans le TypeScript : un FormGroup regroupe des FormControl avec valeur, validateurs et état (valid, dirty, touched) ; le template s'y relie par formControlName.
Formulaires définis dans le TypeScript : un FormGroup regroupe des FormControl avec valeur, validateurs et état (valid, dirty, touched) ; le template s'y relie par formControlName.
Avantages : typés (depuis v14), testables sans DOM, adaptés aux formulaires complexes et dynamiques (FormArray pour une liste de bénéficiaires…).
form = new FormGroup({
nom: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
montant: new FormControl<number | null>(null, [Validators.required, Validators.min(100)]),
});<form [formGroup]="form" (ngSubmit)="valider()">
<input formControlName="nom" />
<input formControlName="montant" type="number" />
<button [disabled]="form.invalid">Envoyer</button>
</form>FormBuilderService qui raccourcit la création des formulaires réactifs ; NonNullableFormBuilder évite que reset() remette les valeurs à null.
Service qui raccourcit la création des formulaires réactifs ; NonNullableFormBuilder évite que reset() remette les valeurs à null.
private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
age: [18, [Validators.min(18)]],
});ValidatorsFonctions de validation fournies par Angular : required, min, max, minLength, maxLength, email, pattern. Chaque erreur apparaît dans control.errors.
Fonctions de validation fournies par Angular : required, min, max, minLength, maxLength, email, pattern. Chaque erreur apparaît dans control.errors.
@if (form.controls.email.hasError('email') && form.controls.email.touched) {
<small>Adresse email invalide</small>
}Validateur personnaliséFonction qui reçoit un contrôle et renvoie null (valide) ou un objet d'erreur ; peut aussi porter sur un groupe (ex. confirmation de mot de passe).
Fonction qui reçoit un contrôle et renvoie null (valide) ou un objet d'erreur ; peut aussi porter sur un groupe (ex. confirmation de mot de passe).
export const telephoneSenegal: ValidatorFn = (c) =>
/^(\+221)?7[05678]\d{7}$/.test(c.value ?? '') ? null : { telephone: true };
export const memesMotsDePasse: ValidatorFn = (g) =>
g.get('mdp')?.value === g.get('confirmation')?.value ? null : { differents: true };Template-driven formsFormulaires définis dans le HTML avec ngModel et des attributs de validation (required, minlength) ; simples pour un petit formulaire, moins testables et moins typés.
Formulaires définis dans le HTML avec ngModel et des attributs de validation (required, minlength) ; simples pour un petit formulaire, moins testables et moins typés.
<form #f="ngForm" (ngSubmit)="envoyer(f.value)">
<input name="email" [(ngModel)]="email" required email />
<button [disabled]="f.invalid">OK</button>
</form>value vs getRawValue()form.value exclut les champs désactivés ; form.getRawValue() renvoie toutes les valeurs, y compris désactivées, avec un type complet.
form.value exclut les champs désactivés ; form.getRawValue() renvoie toutes les valeurs, y compris désactivées, avec un type complet.
this.form.controls.iban.disable();
this.form.value; // { nom: 'Awa' } (iban absent)
this.form.getRawValue(); // { nom: 'Awa', iban: 'SN…' }5Services, HTTP & routage8
Service et @InjectableClasse qui porte la logique réutilisable (appels HTTP, état partagé) ; @Injectable({ providedIn: 'root' }) en fait un singleton disponible dans toute l'application.
Classe qui porte la logique réutilisable (appels HTTP, état partagé) ; @Injectable({ providedIn: 'root' }) en fait un singleton disponible dans toute l'application.
@Injectable({ providedIn: 'root' })
export class ClientService {
private http = inject(HttpClient);
lister() { return this.http.get<ClientDto[]>('/api/v1/clients'); }
}Injection de dépendances Angular (injecteurs hiérarchiques)Angular résout les dépendances en remontant une hiérarchie d'injecteurs (élément → composant parent → route → racine) ; un provider déclaré sur un composant donne une instance propre à ce composant et ses enfants.
Angular résout les dépendances en remontant une hiérarchie d'injecteurs (élément → composant parent → route → racine) ; un provider déclaré sur un composant donne une instance propre à ce composant et ses enfants.
@Component({
selector: 'app-assistant-virement',
providers: [EtatVirementService], // nouvelle instance par assistant ouvert
})
export class AssistantVirementComponent {}HttpClientService Angular pour les appels HTTP ; renvoie des Observables typés, parse le JSON et passe par les intercepteurs.
Service Angular pour les appels HTTP ; renvoie des Observables typés, parse le JSON et passe par les intercepteurs.
create(data: CreateClientRequest): Observable<ClientDto> {
return this.http.post<ClientDto>('/api/v1/clients', data);
}
search(ville: string) {
return this.http.get<Page<ClientDto>>('/api/v1/clients', { params: { ville, page: 0, size: 20 } });
}subscribe() (ou via async pipe / toSignal) : un Observable HTTP est « froid ».Intercepteur HTTP (HttpInterceptorFn)Fonction qui intercepte chaque requête/réponse HttpClient pour un traitement transversal : ajout du token, gestion globale des erreurs, loader, journalisation.
Fonction qui intercepte chaque requête/réponse HttpClient pour un traitement transversal : ajout du token, gestion globale des erreurs, loader, journalisation.
Les requêtes sont immuables : on les clone() pour les modifier.
export const erreurInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) router.navigate(['/login']);
return throwError(() => err);
})
);
};
// enregistrement : provideHttpClient(withInterceptors([authInterceptor, erreurInterceptor]))Router et routesLe routeur associe des URL à des composants, affichés dans <router-outlet> ; routerLink navigue sans recharger la page.
Le routeur associe des URL à des composants, affichés dans <router-outlet> ; routerLink navigue sans recharger la page.
export const routes: Routes = [
{ path: '', redirectTo: 'comptes', pathMatch: 'full' },
{ path: 'comptes', component: ListeComptesComponent },
{ path: 'comptes/:id', component: DetailCompteComponent }, // id lu via input() avec withComponentInputBinding
{ path: '**', component: PageIntrouvableComponent },
];Lazy loading des routesChargement à la demande du code d'une route (loadComponent / loadChildren) : le bundle initial est plus petit, la première page s'affiche plus vite.
Chargement à la demande du code d'une route (loadComponent / loadChildren) : le bundle initial est plus petit, la première page s'affiche plus vite.
{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) },
{ path: 'rapports', loadComponent: () => import('./rapports/rapports.component').then(m => m.RapportsComponent) },Guard (CanActivateFn)Fonction exécutée avant d'activer une route pour l'autoriser ou rediriger (connexion, rôle, modifications non sauvegardées).
Fonction exécutée avant d'activer une route pour l'autoriser ou rediriger (connexion, rôle, modifications non sauvegardées).
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.estConnecte() ? true : inject(Router).createUrlTree(['/login']);
};
{ path: 'virements', canActivate: [authGuard], loadComponent: … }Environnements (environment.ts)Fichiers de configuration remplacés au build selon la cible (dev/prod) : URL de l'API, options. Ils finissent dans le bundle public.
Fichiers de configuration remplacés au build selon la cible (dev/prod) : URL de l'API, options. Ils finissent dans le bundle public.
// src/environments/environment.prod.ts
export const environment = { production: true, apiUrl: 'https://api.banque.sn' };6RxJS18
ObservableFlux de valeurs asynchrones (0, 1 ou plusieurs) qui ne démarre qu'à l'abonnement et peut se terminer (complete) ou échouer (error).
Flux de valeurs asynchrones (0, 1 ou plusieurs) qui ne démarre qu'à l'abonnement et peut se terminer (complete) ou échouer (error).
const tic$ = new Observable<number>(observer => {
let i = 0;
const id = setInterval(() => observer.next(i++), 1000);
return () => clearInterval(id); // nettoyage à l'unsubscribe
});
const sub = tic$.subscribe({ next: v => console.log(v), error: e => console.error(e) });
sub.unsubscribe();$ pour les variables qui contiennent un Observable (clients$).subscribe et Subscriptionsubscribe() démarre l'Observable et fournit les callbacks next/error/complete ; il renvoie une Subscription dont unsubscribe() arrête le flux.
subscribe() démarre l'Observable et fournit les callbacks next/error/complete ; il renvoie une Subscription dont unsubscribe() arrête le flux.
Observable vs PromiseUne Promise produit une seule valeur, démarre immédiatement et ne s'annule pas ; un Observable peut produire plusieurs valeurs, est paresseux, annulable et composable avec des opérateurs.
Une Promise produit une seule valeur, démarre immédiatement et ne s'annule pas ; un Observable peut produire plusieurs valeurs, est paresseux, annulable et composable avec des opérateurs.
const p = fetch('/api/taux'); // la requête part TOUT DE SUITE
const o$ = this.http.get('/api/taux'); // rien ne part…
const sub = o$.subscribe(); // …jusqu'ici
sub.unsubscribe(); // annule la requête HTTP en cours
const valeur = await firstValueFrom(o$); // conversion vers Promise si besoinObservable froid vs chaudFroid : chaque abonné déclenche sa propre exécution (ex. requête HTTP rejouée à chaque subscribe). Chaud : la source émet indépendamment des abonnés, qui partagent les mêmes valeurs (événements, Subject).
Froid : chaque abonné déclenche sa propre exécution (ex. requête HTTP rejouée à chaque subscribe). Chaud : la source émet indépendamment des abonnés, qui partagent les mêmes valeurs (événements, Subject).
async pipes sur le même http.get(...) = deux requêtes HTTP. Solution : shareReplay(1) ou toSignal.Subject et BehaviorSubjectUn Subject est à la fois Observable et émetteur (next) : il diffuse à tous ses abonnés. BehaviorSubject garde la dernière valeur et la donne immédiatement à tout nouvel abonné.
Un Subject est à la fois Observable et émetteur (next) : il diffuse à tous ses abonnés. BehaviorSubject garde la dernière valeur et la donne immédiatement à tout nouvel abonné.
@Injectable({ providedIn: 'root' })
export class SessionService {
private utilisateur = new BehaviorSubject<Utilisateur | null>(null);
utilisateur$ = this.utilisateur.asObservable(); // lecture seule à l'extérieur
connecter(u: Utilisateur) { this.utilisateur.next(u); }
}signal() dans le service remplace avantageusement le BehaviorSubject.pipe() et opérateurspipe() chaîne des opérateurs (fonctions pures) qui transforment un Observable en un autre : map, filter, switchMap, catchError…
pipe() chaîne des opérateurs (fonctions pures) qui transforment un Observable en un autre : map, filter, switchMap, catchError…
this.clients$ = this.http.get<ClientDto[]>('/api/v1/clients').pipe(
map(liste => liste.filter(c => c.actif)),
tap(liste => console.log(liste.length, 'clients actifs')),
catchError(() => of([]))
);map, filter et tapmap transforme chaque valeur ; filter laisse passer celles qui respectent une condition ; tap exécute un effet de bord (log) sans changer la valeur.
map transforme chaque valeur ; filter laisse passer celles qui respectent une condition ; tap exécute un effet de bord (log) sans changer la valeur.
montants$.pipe(
filter(m => m > 0),
map(m => Math.round(m * 1.01)),
tap(m => console.debug('montant avec frais', m))
);switchMapPour chaque valeur, lance un nouvel Observable et annule le précédent s'il n'est pas terminé : « la dernière demande gagne ». Idéal pour l'autocomplétion et les changements de paramètres de route.
Pour chaque valeur, lance un nouvel Observable et annule le précédent s'il n'est pas terminé : « la dernière demande gagne ». Idéal pour l'autocomplétion et les changements de paramètres de route.
resultats$ = this.recherche.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(terme => this.api.chercher(terme)) // annule la requête précédente
);mergeMapLance un Observable interne pour chaque valeur et les exécute tous en parallèle, sans annuler ; l'ordre des résultats n'est pas garanti.
Lance un Observable interne pour chaque valeur et les exécute tous en parallèle, sans annuler ; l'ordre des résultats n'est pas garanti.
from(fichiers).pipe(
mergeMap(f => this.upload(f), 3) // 3 envois simultanés au maximum
).subscribe();concatMapTraite les Observables internes l'un après l'autre, dans l'ordre : le suivant ne démarre que lorsque le précédent est terminé.
Traite les Observables internes l'un après l'autre, dans l'ordre : le suivant ne démarre que lorsque le précédent est terminé.
from(operations).pipe(
concatMap(op => this.http.post('/api/v1/operations', op)) // ordre garanti
).subscribe();exhaustMapIgnore les nouvelles valeurs tant que l'Observable interne en cours n'est pas terminé. Parfait contre le double clic sur « Payer ».
Ignore les nouvelles valeurs tant que l'Observable interne en cours n'est pas terminé. Parfait contre le double clic sur « Payer ».
this.clicPayer$.pipe(
exhaustMap(() => this.paiement.payer(this.panier())) // les clics pendant l'appel sont ignorés
).subscribe();combineLatestCombine plusieurs Observables et émet un tableau des dernières valeurs dès que l'un d'eux émet (après que tous ont émis au moins une fois).
Combine plusieurs Observables et émet un tableau des dernières valeurs dès que l'un d'eux émet (après que tous ont émis au moins une fois).
vue$ = combineLatest([this.filtre$, this.tri$, this.page$]).pipe(
switchMap(([filtre, tri, page]) => this.api.lister({ filtre, tri, page }))
);forkJoinLance plusieurs Observables en parallèle et émet une seule fois, quand tous sont terminés, avec leurs dernières valeurs (l'équivalent de Promise.all).
Lance plusieurs Observables en parallèle et émet une seule fois, quand tous sont terminés, avec leurs dernières valeurs (l'équivalent de Promise.all).
forkJoin({
client: this.api.client(id),
comptes: this.api.comptes(id),
cartes: this.api.cartes(id),
}).subscribe(({ client, comptes, cartes }) => this.initialiser(client, comptes, cartes));debounceTime et distinctUntilChangeddebounceTime attend un silence (ex. 300 ms) avant d'émettre la dernière valeur ; distinctUntilChanged ignore une valeur identique à la précédente. Ensemble, ils évitent des appels inutiles pendant la frappe.
debounceTime attend un silence (ex. 300 ms) avant d'émettre la dernière valeur ; distinctUntilChanged ignore une valeur identique à la précédente. Ensemble, ils évitent des appels inutiles pendant la frappe.
catchError et retrycatchError intercepte une erreur pour renvoyer une valeur de repli ou relancer une erreur enrichie ; retry réessaie l'Observable source un certain nombre de fois.
catchError intercepte une erreur pour renvoyer une valeur de repli ou relancer une erreur enrichie ; retry réessaie l'Observable source un certain nombre de fois.
this.http.get<Taux>('/api/taux').pipe(
retry({ count: 2, delay: 500 }),
catchError((err: HttpErrorResponse) => {
this.toast.erreur('Taux indisponible');
return of(TAUX_PAR_DEFAUT);
})
);async pipePipe de template qui s'abonne à un Observable (ou une Promise), affiche sa dernière valeur et se désabonne automatiquement à la destruction.
Pipe de template qui s'abonne à un Observable (ou une Promise), affiche sa dernière valeur et se désabonne automatiquement à la destruction.
@if (clients$ | async; as clients) {
@for (c of clients; track c.id) { <li>{{ c.nom }}</li> }
}toSignal et toObservablePonts entre RxJS et les signals (@angular/core/rxjs-interop) : toSignal s'abonne et expose la dernière valeur comme signal (désabonnement automatique) ; toObservable fait l'inverse.
Ponts entre RxJS et les signals (@angular/core/rxjs-interop) : toSignal s'abonne et expose la dernière valeur comme signal (désabonnement automatique) ; toObservable fait l'inverse.
clients = toSignal(this.service.lister(), { initialValue: [] });
terme = signal('');
resultats = toSignal(
toObservable(this.terme).pipe(debounceTime(300), switchMap(t => this.api.chercher(t))),
{ initialValue: [] }
);7Performance4
Détection de changements (change detection)Mécanisme qui compare l'état des composants à ce qui est affiché et met à jour le DOM. Historiquement déclenché par Zone.js sur tout l'arbre ; aujourd'hui de plus en plus ciblé grâce aux signals.
Mécanisme qui compare l'état des composants à ce qui est affiché et met à jour le DOM. Historiquement déclenché par Zone.js sur tout l'arbre ; aujourd'hui de plus en plus ciblé grâce aux signals.
OnPushStratégie où un composant n'est revérifié que si une de ses références d'input change, qu'un événement survient dans son template, qu'un signal lu change ou que l'async pipe émet. Défaut des nouveaux projets en v22.
Stratégie où un composant n'est revérifié que si une de ses références d'input change, qu'un événement survient dans son template, qu'un signal lu change ou que l'async pipe émet. Défaut des nouveaux projets en v22.
@Component({ changeDetection: ChangeDetectionStrategy.OnPush, … })
export class ListeComponent {
items = input<Item[]>([]);
}
// parent : this.items = [...this.items, nouvel]; ✅ nouvelle référence
// this.items.push(nouvel); ❌ même référence : l'enfant OnPush ne bouge pasSSR et hydratationLe Server-Side Rendering génère le HTML côté serveur (affichage rapide, SEO) ; l'hydratation réutilise ensuite ce DOM côté navigateur au lieu de le reconstruire. L'hydratation incrémentale (avec @defer) n'active que les parties nécessaires.
Le Server-Side Rendering génère le HTML côté serveur (affichage rapide, SEO) ; l'hydratation réutilise ensuite ce DOM côté navigateur au lieu de le reconstruire. L'hydratation incrémentale (avec @defer) n'active que les parties nécessaires.
ng new banque-public --ssrprovideClientHydration(withIncrementalHydration())Budgets de bundleSeuils définis dans angular.json : le build avertit ou échoue si le JavaScript initial dépasse une taille donnée. Garde-fou contre l'alourdissement progressif.
Seuils définis dans angular.json : le build avertit ou échoue si le JavaScript initial dépasse une taille donnée. Garde-fou contre l'alourdissement progressif.
"budgets": [{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" }]8TypeScript13
TypeScriptSur-ensemble typé de JavaScript : les types sont vérifiés à la compilation puis effacés ; le navigateur exécute du JavaScript.
Sur-ensemble typé de JavaScript : les types sont vérifiés à la compilation puis effacés ; le navigateur exécute du JavaScript.
Les types n'existent pas à l'exécution : une réponse HTTP typée ClientDto n'est pas vérifiée. Si le backend renvoie autre chose, TypeScript ne le verra pas (valider avec Zod si nécessaire).
function frais(montant: number, taux = 0.01): number { return Math.round(montant * taux); }
frais('1000'); // ❌ erreur de compilationinterface vs typeLes deux décrivent la forme d'un objet. interface peut être étendue et fusionnée (declaration merging) ; type permet en plus unions, intersections, tuples et types calculés.
Les deux décrivent la forme d'un objet. interface peut être étendue et fusionnée (declaration merging) ; type permet en plus unions, intersections, tuples et types calculés.
interface Client { id: number; nom: string; }
interface ClientVip extends Client { conseiller: string; }
type Statut = 'ACTIF' | 'GELE' | 'CLOTURE'; // union : impossible avec interface
type CompteDetaille = Compte & { transactions: Transaction[] }; // intersectionGenerics (génériques)Types paramétrés (<T>) qui rendent une fonction ou une classe réutilisable pour plusieurs types tout en gardant le typage.
Types paramétrés (<T>) qui rendent une fonction ou une classe réutilisable pour plusieurs types tout en gardant le typage.
interface Page<T> { content: T[]; totalElements: number; number: number; }
function premier<T>(liste: T[]): T | undefined { return liste[0]; }
const c = premier<Client>(clients); // c : Client | undefinedkeyof et contraintes extendskeyof T donne l'union des clés d'un type ; K extends keyof T contraint un paramètre générique à être une clé valide.
keyof T donne l'union des clés d'un type ; K extends keyof T contraint un paramètre générique à être une clé valide.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
getProperty(client, 'nom'); // string
getProperty(client, 'adresse'); // ❌ erreur : 'adresse' n'est pas une clé de ClientUtility types : Partial, Required, Readonly, Pick, Omit, RecordTypes utilitaires intégrés qui dérivent un nouveau type d'un existant : tout optionnel, tout requis, lecture seule, sous-ensemble, exclusion, dictionnaire.
Types utilitaires intégrés qui dérivent un nouveau type d'un existant : tout optionnel, tout requis, lecture seule, sous-ensemble, exclusion, dictionnaire.
interface Client { id: number; nom: string; email: string; telephone?: string; }
type MajClient = Partial<Omit<Client, 'id'>>; // PATCH : tout optionnel sauf id exclu
type ClientCree = Required<Client>; // telephone devient obligatoire
type ClientLu = Readonly<Client>; // aucune propriété modifiable
type Resume = Pick<Client, 'id' | 'nom'>; // { id; nom }
type SoldesParDevise = Record<'XOF' | 'EUR', number>; // { XOF: number; EUR: number }Union types et types littérauxUn type union A | B accepte l'une ou l'autre forme ; combiné à des littéraux ('ACTIF' | 'GELE'), il remplace avantageusement les chaînes libres.
Un type union A | B accepte l'une ou l'autre forme ; combiné à des littéraux ('ACTIF' | 'GELE'), il remplace avantageusement les chaînes libres.
type Devise = 'XOF' | 'EUR' | 'USD';
function convertir(m: number, d: Devise) { … }
convertir(100, 'GBP'); // ❌ refusé à la compilationDiscriminated unionUnion d'objets qui partagent une propriété littérale commune (le discriminant) ; TypeScript en déduit automatiquement le type exact dans chaque branche.
Union d'objets qui partagent une propriété littérale commune (le discriminant) ; TypeScript en déduit automatiquement le type exact dans chaque branche.
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function afficher(r: Result<Client>) {
if (r.success) console.log(r.data.nom); // ici TS sait que data existe
else console.error(r.error); // et ici que error existe
}Narrowing et type guardsLe narrowing affine un type dans une branche (typeof, instanceof, in, ===) ; un type guard est une fonction x is T qui enseigne un test personnalisé au compilateur.
Le narrowing affine un type dans une branche (typeof, instanceof, in, ===) ; un type guard est une fonction x is T qui enseigne un test personnalisé au compilateur.
function estErreurApi(e: unknown): e is { code: string; message: string } {
return typeof e === 'object' && e !== null && 'code' in e;
}
if (estErreurApi(err)) console.log(err.code); // err est typéany vs unknownany désactive toute vérification (à bannir) ; unknown accepte n'importe quelle valeur mais oblige à vérifier son type avant de l'utiliser.
any désactive toute vérification (à bannir) ; unknown accepte n'importe quelle valeur mais oblige à vérifier son type avant de l'utiliser.
const a: any = JSON.parse(texte); a.foo.bar(); // compile, plante à l'exécution
const u: unknown = JSON.parse(texte);
u.foo; // ❌ erreur : il faut d'abord affiner
if (typeof u === 'object' && u && 'foo' in u) { … }Optional chaining (?.) et nullish coalescing (??)a?.b renvoie undefined au lieu de planter si a est null/undefined ; x ?? defaut remplace seulement null/undefined (contrairement à || qui remplace aussi 0 et '').
a?.b renvoie undefined au lieu de planter si a est null/undefined ; x ?? defaut remplace seulement null/undefined (contrairement à || qui remplace aussi 0 et '').
const ville = client?.adresse?.ville ?? 'Non renseignée';
const remise = saisie ?? 0; // 0 saisi reste 0 (avec || il deviendrait la valeur par défaut)Non-null assertion (!) et assignation certainevaleur! dit au compilateur « je garantis que ce n'est pas null » ; champ!: Type dit qu'un champ sera initialisé plus tard (ex. un @Input). À utiliser avec parcimonie : aucun contrôle à l'exécution.
valeur! dit au compilateur « je garantis que ce n'est pas null » ; champ!: Type dit qu'un champ sera initialisé plus tard (ex. un @Input). À utiliser avec parcimonie : aucun contrôle à l'exécution.
@Input({ required: true }) user!: User; // initialisé par Angular
const el = document.getElementById('total')!; // risqué si l'élément n'existe pasMode strictEnsemble d'options du compilateur (strictNullChecks, noImplicitAny…) qui interdit les approximations ; activé par défaut dans les projets Angular récents.
Ensemble d'options du compilateur (strictNullChecks, noImplicitAny…) qui interdit les approximations ; activé par défaut dans les projets Angular récents.
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } }DécorateursFonctions préfixées par @ qui ajoutent des métadonnées à une classe ou un membre ; Angular s'en sert pour déclarer composants, directives, services et inputs.
Fonctions préfixées par @ qui ajoutent des métadonnées à une classe ou un membre ; Angular s'en sert pour déclarer composants, directives, services et inputs.
@Component({ selector: 'app-x', template: '' }) // métadonnées lues par le compilateur Angular
export class XComponent {}📋 Synthèse express — Angular
🆕 Nouveautés Angular 17 → 22
- Control flow @if/@for
- @defer
- Signals stables
- Control flow et @defer stables
- Zoneless expérimental
- Standalone par défaut
- input()/output()/model() stables
- linkedSignal, resource
- effect, linkedSignal, toSignal stables
- Zoneless stable (20.2)
- Zoneless par défaut
- Signal Forms (expérimental)
- Vitest par défaut
- OnPush par défaut
- Signal Forms stables
- httpResource stable, @Service
🔗 Data Binding — Les 4 types
| Type | Syntaxe | Direction | Cas d'usage |
|---|---|---|---|
| Interpolation | {{ titre }} | Component → Template | Afficher une valeur |
| Property Binding | [disabled]="loading" | Component → Template | Propriété DOM, Input |
| Event Binding | (click)="save()" | Template → Component | Événements utilisateur |
| Two-Way Binding | [(ngModel)]="name" | Bidirectionnel | Formulaires template-driven |
🔄 Lifecycle Hooks — Ordre d'exécution
Avec les Signals (Angular 17+) et
takeUntilDestroyed() de @angular/core/rxjs-interop, la gestion du cycle de vie est automatique.
📡 @Input / @Output — Communication parent-enfant
📋 Reactive Forms
🔌 Intercepteurs HTTP
⚖️ Observable vs Promise
| Critère | Observable (RxJS) | Promise |
|---|---|---|
| Valeurs | Multiple (flux) | Une seule |
| Annulation | ✅ unsubscribe() | ❌ non natif |
| Lazy | ✅ (subscribe déclenche) | ❌ démarre immédiatement |
| Opérateurs | ✅ map, filter, switchMap… | ❌ limité (.then, .catch) |
| Cas d'usage | HTTP, events, WebSocket, formulaires | Opération async unique |
🔄 RxJS — Opérateurs essentiels
| Opérateur | Comportement | Cas d'usage |
|---|---|---|
switchMap | Annule l'observable précédent | Autocomplete, dernière requête gagne |
mergeMap | Parallélise tous les observables | Upload multiple simultané |
concatMap | Séquentiel, attend la fin | Ordre garanti (save séquentiel) |
exhaustMap | Ignore les nouvelles émissions si en cours | Submit bouton (évite double-submit) |
combineLatest | Émet quand tous ont émis au moins 1 fois | Combiner plusieurs filtres |
forkJoin | Attend la fin de tous | Chargement initial multiple |
shareReplay(1) | Partage + rejoue la dernière valeur | Cache HTTP dans service |
takeUntilDestroyed | Unsubscribe auto (v16+) | Gestion cycle de vie |
⚡ Performance Angular
Par défaut Angular vérifie tout le composant tree. OnPush ne vérifie que si Input change (référence) ou Observable émet.
Sans trackBy, Angular re-rend tous les items sur chaque changement. Avec track item.id, seuls les items modifiés sont re-rendus.
🔷 TypeScript — Types essentiels
debounceTime(300) et distinctUntilChanged().A | B), intersections (A & B) et mapped types. En pratique, interface pour les objets/classes, type pour tout le reste.FormBuilder, FormGroup) : logique dans le composant TypeScript, entièrement testables, typage fort, meilleur pour les formulaires complexes et dynamiques. Template-Driven (ngModel) : plus simple mais logique dans le HTML, moins testable, adapté aux petits formulaires.1Styles d'architecture7
MonolitheApplication déployée comme un seul artefact (un JAR, une base) qui contient toutes les fonctionnalités. Simple à développer, tester et déployer au départ.
Application déployée comme un seul artefact (un JAR, une base) qui contient toutes les fonctionnalités. Simple à développer, tester et déployer au départ.
Le monolithe n'est pas un défaut : transactions ACID locales, appels en mémoire (nanosecondes), un seul pipeline. Il devient problématique quand il est mal structuré (« big ball of mud ») ou quand des équipes nombreuses se bloquent mutuellement sur le même code et le même déploiement.
Monolithe modulaireUn seul déployable, mais découpé en modules métier aux frontières strictes (API publiques, pas d'accès direct aux tables des autres). Souvent le meilleur point de départ.
Un seul déployable, mais découpé en modules métier aux frontières strictes (API publiques, pas d'accès direct aux tables des autres). Souvent le meilleur point de départ.
On obtient l'essentiel de la clarté des microservices sans leur complexité opérationnelle, et l'on peut extraire un module en service plus tard. Spring Modulith vérifie ces frontières par des tests.
// Test qui échoue si un module accède aux classes internes d'un autre
class ArchitectureTest {
@Test void verifierModules() { ApplicationModules.of(BanqueApplication.class).verify(); }
}MicroserviceService autonome, centré sur une capacité métier, avec sa propre base de données, déployable et scalable indépendamment, qui communique par le réseau (HTTP, messages).
Service autonome, centré sur une capacité métier, avec sa propre base de données, déployable et scalable indépendamment, qui communique par le réseau (HTTP, messages).
Bénéfices : déploiements indépendants, équipes autonomes, scalabilité ciblée, choix technologiques locaux. Coûts : réseau non fiable, cohérence à terme, observabilité distribuée, tests plus complexes, infrastructure (Kubernetes, CI/CD par service).
Bounded Context (DDD)Frontière à l'intérieur de laquelle un modèle métier et son vocabulaire ont un sens précis ; c'est la bonne unité pour découper des microservices.
Frontière à l'intérieur de laquelle un modèle métier et son vocabulaire ont un sens précis ; c'est la bonne unité pour découper des microservices.
Le mot « Client » n'a pas le même sens pour le service KYC (identité, pièces justificatives) que pour le service Crédit (scoring, capacité de remboursement). Chaque contexte a son propre modèle ; ils communiquent par des contrats explicites.
Database per serviceChaque microservice possède sa base et est le seul à y accéder ; les autres passent par son API ou ses événements.
Chaque microservice possède sa base et est le seul à y accéder ; les autres passent par son API ou ses événements.
Une base partagée recrée un couplage fort (un changement de schéma casse plusieurs services). Le prix : plus de jointure SQL entre services, il faut composer les données via API ou répliquer par événements.
Loi de Conway« Les organisations produisent des systèmes dont la structure reproduit leurs structures de communication. » Le découpage technique suit le découpage des équipes.
« Les organisations produisent des systèmes dont la structure reproduit leurs structures de communication. » Le découpage technique suit le découpage des équipes.
Conséquence pratique (« Inverse Conway Maneuver ») : on organise les équipes selon les domaines métier que l'on veut voir apparaître dans l'architecture.
Strangler Fig (migration progressive)Migrer un système legacy pièce par pièce : une façade (gateway) route certaines URL vers le nouveau service, le reste vers l'ancien, jusqu'à extinction de l'ancien.
Migrer un système legacy pièce par pièce : une façade (gateway) route certaines URL vers le nouveau service, le reste vers l'ancien, jusqu'à extinction de l'ancien.
routes:
- id: virements-nouveau
uri: http://virements-service:8080
predicates: [ Path=/api/virements/** ] # déjà migré
- id: legacy
uri: http://core-banking-legacy:8080
predicates: [ Path=/** ] # tout le reste2Communication entre services6
Communication synchrone vs asynchroneSynchrone : l'appelant attend la réponse (REST, gRPC) ; asynchrone : il publie un message ou un événement et continue (Kafka, RabbitMQ).
Synchrone : l'appelant attend la réponse (REST, gRPC) ; asynchrone : il publie un message ou un événement et continue (Kafka, RabbitMQ).
| Synchrone (REST/gRPC) | Asynchrone (messages) | |
|---|---|---|
| Couplage temporel | Fort (l'autre doit être disponible) | Faible |
| Réponse immédiate | Oui | Non (eventual) |
| Gestion de charge | Pics transmis | Lissés par la file |
| Débogage | Simple | Traçage distribué nécessaire |
API GatewayPoint d'entrée unique devant les microservices : routage, authentification, rate limiting, CORS, agrégation, terminaison TLS.
Point d'entrée unique devant les microservices : routage, authentification, rate limiting, CORS, agrégation, terminaison TLS.
Le front Angular parle à une seule URL ; la gateway redirige vers le bon service. Outils : Spring Cloud Gateway, Kong, NGINX, AWS API Gateway.
Service DiscoveryMécanisme qui permet à un service de trouver l'adresse réseau des instances d'un autre service, qui changent en permanence (scaling, redémarrages).
Mécanisme qui permet à un service de trouver l'adresse réseau des instances d'un autre service, qui changent en permanence (scaling, redémarrages).
Sur Kubernetes, c'est natif : un Service k8s a un nom DNS stable (http://comptes-service:8080). Hors Kubernetes : Eureka, Consul.
Configuration centralisée (Config Server)Externaliser la configuration de tous les services dans un dépôt central (Git, Vault, ConfigMaps) pour la modifier sans reconstruire les images.
Externaliser la configuration de tous les services dans un dépôt central (Git, Vault, ConfigMaps) pour la modifier sans reconstruire les images.
spring:
config:
import: "configserver:http://config:8888" # Spring Cloud ConfigService mesh (Istio, Linkerd)Couche d'infrastructure (un proxy « sidecar » à côté de chaque service) qui gère le trafic réseau : mTLS, retries, timeouts, routage canary, métriques — sans toucher au code.
Couche d'infrastructure (un proxy « sidecar » à côté de chaque service) qui gère le trafic réseau : mTLS, retries, timeouts, routage canary, métriques — sans toucher au code.
Utile à grande échelle (dizaines de services) ; ajoute de la complexité et de la latence pour une petite équipe.
Les illusions de l'informatique distribuéeHuit hypothèses fausses que l'on fait souvent : le réseau est fiable, la latence est nulle, la bande passante infinie, le réseau sûr, la topologie fixe, un seul administrateur, transport gratuit, réseau homogène.
Huit hypothèses fausses que l'on fait souvent : le réseau est fiable, la latence est nulle, la bande passante infinie, le réseau sûr, la topologie fixe, un seul administrateur, transport gratuit, réseau homogène.
Chaque appel réseau peut échouer, être lent ou réussir sans que l'appelant le sache : d'où timeouts, retries, idempotence et circuit breakers.
3Données distribuées11
Transaction distribuéeOpération qui modifie des données dans plusieurs services ou bases et devrait être « tout ou rien ». Sans base commune, il n'y a plus de transaction ACID unique.
Opération qui modifie des données dans plusieurs services ou bases et devrait être « tout ou rien ». Sans base commune, il n'y a plus de transaction ACID unique.
Exemple : commande → réservation de stock (service Stock) → paiement (service Paiement) → expédition. Solutions : SAGA (cohérence à terme) plutôt que 2PC.
2PC (Two-Phase Commit)Protocole où un coordinateur demande à tous les participants de se préparer (phase 1) puis de valider (phase 2). Garantit l'atomicité mais bloque les ressources et dépend du coordinateur.
Protocole où un coordinateur demande à tous les participants de se préparer (phase 1) puis de valider (phase 2). Garantit l'atomicité mais bloque les ressources et dépend du coordinateur.
Peu adapté aux microservices : verrous longs, faible disponibilité, peu supporté par les brokers et bases NoSQL. On lui préfère les SAGA.
Cohérence à terme (eventual consistency)Les données des différents services finissent par être cohérentes, mais pas instantanément : pendant un court laps de temps, deux vues peuvent diverger.
Les données des différents services finissent par être cohérentes, mais pas instantanément : pendant un court laps de temps, deux vues peuvent diverger.
L'interface doit l'assumer : « Virement en cours de traitement » plutôt qu'un solde faux. On le paie en simplicité, on le gagne en disponibilité et en découplage (théorème CAP).
SAGASuite de transactions locales, une par service ; si une étape échoue, on exécute des transactions de compensation pour annuler les étapes précédentes.
Suite de transactions locales, une par service ; si une étape échoue, on exécute des transactions de compensation pour annuler les étapes précédentes.
Commande créée → Stock réservé → Paiement débité → ✅ Commande confirmée
↘ Paiement refusé → compensation : libérer le stock → Commande annuléeChorégraphie (SAGA)Style de SAGA sans chef : chaque service réagit aux événements des autres et publie les siens. Couplage faible, mais flux global difficile à visualiser et à déboguer.
Style de SAGA sans chef : chaque service réagit aux événements des autres et publie les siens. Couplage faible, mais flux global difficile à visualiser et à déboguer.
@KafkaListener(topics = "stock.reserve")
public void onStockReserve(StockReserve evt) {
var resultat = paiementService.debiter(evt.commandeId(), evt.montant());
kafka.send(resultat.ok() ? "paiement.accepte" : "paiement.refuse", evt.commandeId(), resultat);
}Orchestration (SAGA)Style de SAGA où un orchestrateur central envoie les commandes à chaque service, attend les réponses et déclenche les compensations. Flux explicite et traçable, au prix d'un composant central.
Style de SAGA où un orchestrateur central envoie les commandes à chaque service, attend les réponses et déclenche les compensations. Flux explicite et traçable, au prix d'un composant central.
Outils : un service dédié avec une machine à états, Camunda/Zeebe (BPMN), Temporal.
public void executer(Commande c) {
stock.reserver(c);
try { paiement.debiter(c); }
catch (PaiementRefuseException e) { stock.liberer(c); commande.annuler(c); return; }
commande.confirmer(c);
}Transaction de compensationOpération métier qui annule l'effet d'une étape déjà validée (rembourser, libérer un stock) ; ce n'est pas un rollback technique, l'étape a bien eu lieu.
Opération métier qui annule l'effet d'une étape déjà validée (rembourser, libérer un stock) ; ce n'est pas un rollback technique, l'étape a bien eu lieu.
Outbox pattern (Transactional Outbox)Écrire l'événement à publier dans une table « outbox » dans la même transaction que la donnée métier ; un relais le publie ensuite sur Kafka. Évite de modifier la base sans publier (ou l'inverse).
Écrire l'événement à publier dans une table « outbox » dans la même transaction que la donnée métier ; un relais le publie ensuite sur Kafka. Évite de modifier la base sans publier (ou l'inverse).
Le problème : repo.save() puis kafka.send() n'est pas atomique. Si Kafka est indisponible après le commit, l'événement est perdu. Avec l'outbox, les deux écritures sont dans la même transaction SQL ; un processus (ou Debezium par CDC) lit la table et publie.
@Transactional
public void creerVirement(Virement v) {
virementRepo.save(v);
outboxRepo.save(new OutboxEvent("virement.cree", v.id(), json(v))); // même transaction
}
// Relais : @Scheduled qui lit les lignes non publiées, envoie à Kafka, marque "publié"CQRSSéparer le modèle d'écriture (commandes, règles métier) du modèle de lecture (requêtes, vues optimisées), éventuellement avec des bases différentes synchronisées par événements.
Séparer le modèle d'écriture (commandes, règles métier) du modèle de lecture (requêtes, vues optimisées), éventuellement avec des bases différentes synchronisées par événements.
Utile quand les besoins de lecture (tableaux de bord, recherches multicritères) divergent fortement de l'écriture. Prix : deux modèles, synchronisation, cohérence à terme.
// Écriture : agrégat avec règles métier
commandHandler.handle(new EffectuerVirement(source, dest, montant));
// Lecture : vue dénormalisée, alimentée par les événements, interrogée directement
List<ReleveLigne> releve = releveQueryRepo.findByCompteOrderByDateDesc(compte);Event SourcingStocker la suite des événements (CompteOuvert, Crédité, Débité…) au lieu de l'état courant ; l'état est reconstruit en rejouant les événements.
Stocker la suite des événements (CompteOuvert, Crédité, Débité…) au lieu de l'état courant ; l'état est reconstruit en rejouant les événements.
Avantages : historique complet et audit naturel, possibilité de reconstruire n'importe quel état passé, nouvelles projections à partir de l'historique. Inconvénients : complexité, évolution des schémas d'événements, besoin de snapshots.
BigDecimal solde = evenements.stream().map(e -> switch (e) {
case Credite c -> c.montant();
case Debite d -> d.montant().negate();
default -> BigDecimal.ZERO;
}).reduce(BigDecimal.ZERO, BigDecimal::add);Événement de domaineFait métier passé et immuable, nommé au passé (VirementEffectué), publié pour que d'autres parties du système réagissent.
Fait métier passé et immuable, nommé au passé (VirementEffectué), publié pour que d'autres parties du système réagissent.
public record VirementEffectue(UUID id, String source, String dest, BigDecimal montant, Instant date) {}4Résilience12
Panne en cascadeUn service lent ou en panne bloque les threads de ses appelants, qui deviennent lents à leur tour, jusqu'à faire tomber tout le système.
Un service lent ou en panne bloque les threads de ses appelants, qui deviennent lents à leur tour, jusqu'à faire tomber tout le système.
Remèdes : timeouts courts, circuit breaker, bulkhead, dégradation gracieuse.
TimeoutDurée maximale d'attente d'un appel réseau ; sans timeout, un service lent peut bloquer indéfiniment les threads de l'appelant.
Durée maximale d'attente d'un appel réseau ; sans timeout, un service lent peut bloquer indéfiniment les threads de l'appelant.
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(2));
factory.setReadTimeout(Duration.ofSeconds(3));
RestClient client = RestClient.builder().requestFactory(factory).build();Retry et backoff exponentielRéessayer automatiquement un appel en échec transitoire, en espaçant les tentatives de plus en plus (100 ms, 200 ms, 400 ms…) pour ne pas surcharger le service qui récupère.
Réessayer automatiquement un appel en échec transitoire, en espaçant les tentatives de plus en plus (100 ms, 200 ms, 400 ms…) pour ne pas surcharger le service qui récupère.
Ne réessayer que les erreurs transitoires (timeout, 503) et les opérations idempotentes. Limiter le nombre de tentatives (3).
resilience4j.retry.instances.operateur:
max-attempts: 3
wait-duration: 200ms
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
retry-exceptions: [ java.io.IOException, java.util.concurrent.TimeoutException ]JitterPetite part aléatoire ajoutée aux délais (retries, TTL de cache) pour que des milliers de clients ne réessaient pas tous au même instant (effet « troupeau »).
Petite part aléatoire ajoutée aux délais (retries, TTL de cache) pour que des milliers de clients ne réessaient pas tous au même instant (effet « troupeau »).
délai = base × 2^tentative + aléatoire(0..100 ms)Circuit BreakerDisjoncteur logiciel : après trop d'échecs, il « s'ouvre » et rejette immédiatement les appels vers le service défaillant pendant un temps, puis teste prudemment son rétablissement.
Disjoncteur logiciel : après trop d'échecs, il « s'ouvre » et rejette immédiatement les appels vers le service défaillant pendant un temps, puis teste prudemment son rétablissement.
États : Closed (normal, échecs comptés) → Open (seuil dépassé, appels rejetés directement, fallback) → Half-Open (après un délai, quelques appels de test ; succès → Closed, échec → Open).
@CircuitBreaker(name = "operateur", fallbackMethod = "statutInconnu")
public StatutPaiement statut(String ref) { return operateurClient.statut(ref); }
private StatutPaiement statutInconnu(String ref, Throwable t) {
return StatutPaiement.A_VERIFIER; // dégradation gracieuse
}resilience4j.circuitbreaker.instances.operateur:
sliding-window-size: 20
failure-rate-threshold: 50 # % d'échecs pour ouvrir
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3États Closed / Open / Half-OpenLes trois états d'un circuit breaker : fermé (le courant passe, appels normaux), ouvert (appels coupés), semi-ouvert (quelques appels de test).
Les trois états d'un circuit breaker : fermé (le courant passe, appels normaux), ouvert (appels coupés), semi-ouvert (quelques appels de test).
Resilience4jBibliothèque Java légère de résilience : CircuitBreaker, Retry, RateLimiter, Bulkhead, TimeLimiter, intégrée à Spring Boot par annotations et propriétés.
Bibliothèque Java légère de résilience : CircuitBreaker, Retry, RateLimiter, Bulkhead, TimeLimiter, intégrée à Spring Boot par annotations et propriétés.
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>Fallback et dégradation gracieuseRéponse de repli quand une dépendance échoue (valeur en cache, valeur par défaut, fonctionnalité masquée) pour que le service reste utilisable.
Réponse de repli quand une dépendance échoue (valeur en cache, valeur par défaut, fonctionnalité masquée) pour que le service reste utilisable.
Exemple : si le service de recommandations tombe, la page d'accueil s'affiche sans le bloc recommandations plutôt qu'avec une erreur 500.
Bulkhead (cloisonnement)Isoler les ressources (pools de threads, connexions) par dépendance pour qu'un service lent n'épuise pas toutes les ressources de l'appelant, comme les cloisons étanches d'un navire.
Isoler les ressources (pools de threads, connexions) par dépendance pour qu'un service lent n'épuise pas toutes les ressources de l'appelant, comme les cloisons étanches d'un navire.
@Bulkhead(name = "rapports", type = Bulkhead.Type.SEMAPHORE) // max N appels simultanés
public Rapport generer() { … }Rate limiter (limitation de débit)Limiter le nombre de requêtes acceptées par unité de temps (par client, par IP, par API key) pour protéger un service et assurer l'équité.
Limiter le nombre de requêtes acceptées par unité de temps (par client, par IP, par API key) pour protéger un service et assurer l'équité.
Algorithmes : token bucket, fenêtre glissante. Réponse au dépassement : 429 Too Many Requests avec Retry-After.
resilience4j.ratelimiter.instances.otp:
limit-for-period: 5 # 5 envois d'OTP…
limit-refresh-period: 1m # …par minuteIdempotenceUne opération est idempotente si l'exécuter plusieurs fois produit le même effet qu'une seule fois. Indispensable dès qu'il y a des retries ou une livraison « au moins une fois ».
Une opération est idempotente si l'exécuter plusieurs fois produit le même effet qu'une seule fois. Indispensable dès qu'il y a des retries ou une livraison « au moins une fois ».
SET solde = 1000 est idempotent ; solde = solde - 100 ne l'est pas. Pour rendre un traitement idempotent : clé d'idempotence, contrainte d'unicité en base, table des messages déjà traités.
@Transactional
public void traiter(PaiementRecu evt) {
if (dejaTraiteRepo.existsById(evt.id())) return; // doublon ignoré
compteService.crediter(evt.compte(), evt.montant());
dejaTraiteRepo.save(new MessageTraite(evt.id())); // + contrainte UNIQUE
}Idempotency-KeyEn-tête HTTP contenant un identifiant unique généré par le client pour une opération ; le serveur mémorise la réponse associée et la renvoie telle quelle si la requête est rejouée.
En-tête HTTP contenant un identifiant unique généré par le client pour une opération ; le serveur mémorise la réponse associée et la renvoie telle quelle si la requête est rejouée.
POST /api/v1/paiements
Idempotency-Key: 7c1f6c3e-2b8d-4f6a-9a51-0f2d6e1b9c44
{"compte": "SN01…", "montant": 25000}@PostMapping("/paiements")
ResponseEntity<PaiementDto> payer(@RequestHeader("Idempotency-Key") UUID cle, @Valid @RequestBody PaiementRequest r) {
return idempotence.executerUneFois(cle, () -> service.payer(r)); // stocke clé + réponse (Redis/DB)
}5Architecture du code10
Architecture hexagonale (Ports & Adapters)Le domaine métier est au centre et ne dépend d'aucune technologie ; il définit des ports (interfaces) que des adaptateurs (HTTP, JPA, Kafka) implémentent. Toutes les dépendances pointent vers l'intérieur.
Le domaine métier est au centre et ne dépend d'aucune technologie ; il définit des ports (interfaces) que des adaptateurs (HTTP, JPA, Kafka) implémentent. Toutes les dépendances pointent vers l'intérieur.
On peut tester le métier sans base ni serveur, et changer de technologie (PostgreSQL → MongoDB, REST → Kafka) en ne remplaçant qu'un adaptateur.
[REST Controller] ─► (port entrant: EffectuerVirement) ─► [DOMAINE] ─► (port sortant: ComptePort) ◄─ [JpaCompteAdapter]PortInterface définie par le domaine : port entrant = ce que l'application offre (cas d'usage) ; port sortant = ce dont le domaine a besoin (persistance, notifications).
Interface définie par le domaine : port entrant = ce que l'application offre (cas d'usage) ; port sortant = ce dont le domaine a besoin (persistance, notifications).
public interface EffectuerVirement { VirementId executer(DemandeVirement d); } // port entrant
public interface ComptePort { Compte charger(Iban iban); void sauver(Compte c); } // port sortantAdapter (adaptateur)Implémentation technique d'un port : contrôleur REST (adaptateur entrant), repository JPA ou client Kafka (adaptateur sortant).
Implémentation technique d'un port : contrôleur REST (adaptateur entrant), repository JPA ou client Kafka (adaptateur sortant).
@Component
class JpaCompteAdapter implements ComptePort {
private final CompteJpaRepository jpa;
private final CompteMapper mapper;
public Compte charger(Iban iban) { return mapper.toDomain(jpa.findByIban(iban.valeur()).orElseThrow()); }
public void sauver(Compte c) { jpa.save(mapper.toEntity(c)); }
}Couche DomaineCœur métier : entités, value objects, règles et services du domaine, en Java pur (sans annotation Spring ni JPA idéalement).
Cœur métier : entités, value objects, règles et services du domaine, en Java pur (sans annotation Spring ni JPA idéalement).
Couche Application (use cases)Orchestre un cas d'usage : charge les objets via les ports, appelle le domaine, gère la transaction, publie les événements. Aucune règle métier propre.
Orchestre un cas d'usage : charge les objets via les ports, appelle le domaine, gère la transaction, publie les événements. Aucune règle métier propre.
@Service @Transactional
class EffectuerVirementService implements EffectuerVirement {
public VirementId executer(DemandeVirement d) {
Compte source = comptes.charger(d.source());
Compte dest = comptes.charger(d.destination());
Virement v = source.virerVers(dest, d.montant()); // règle métier dans le domaine
comptes.sauver(source); comptes.sauver(dest);
return v.id();
}
}Couche InfrastructureTout ce qui est technique : contrôleurs REST, repositories JPA, consommateurs Kafka, clients HTTP, configuration. Elle dépend du domaine, jamais l'inverse.
Tout ce qui est technique : contrôleurs REST, repositories JPA, consommateurs Kafka, clients HTTP, configuration. Elle dépend du domaine, jamais l'inverse.
Entité (DDD)Objet métier défini par son identité, qui persiste dans le temps même si ses attributs changent (un Compte reste le même compte quand son solde change).
Objet métier défini par son identité, qui persiste dans le temps même si ses attributs changent (un Compte reste le même compte quand son solde change).
Value ObjectObjet défini uniquement par ses valeurs, immuable et sans identité (Montant, Iban, Adresse) ; deux value objects égaux sont interchangeables. Il porte ses propres validations.
Objet défini uniquement par ses valeurs, immuable et sans identité (Montant, Iban, Adresse) ; deux value objects égaux sont interchangeables. Il porte ses propres validations.
public record Montant(BigDecimal valeur, String devise) {
public Montant {
if (valeur.signum() < 0) throw new IllegalArgumentException("Montant négatif");
valeur = valeur.setScale(2, RoundingMode.HALF_EVEN);
}
public Montant plus(Montant o) { verifierDevise(o); return new Montant(valeur.add(o.valeur), devise); }
}Domain ServiceRègle métier qui ne relève naturellement d'aucune entité (ex. calcul de frais entre deux comptes de banques différentes) ; sans état, en Java pur.
Règle métier qui ne relève naturellement d'aucune entité (ex. calcul de frais entre deux comptes de banques différentes) ; sans état, en Java pur.
Règle de dépendance (architecture en couches)Les dépendances vont toujours dans un seul sens : Controller → Service → Repository. Une couche basse ne connaît jamais une couche haute.
Les dépendances vont toujours dans un seul sens : Controller → Service → Repository. Une couche basse ne connaît jamais une couche haute.
6SOLID & qualité de conception8
SOLIDCinq principes de conception orientée objet (Robert C. Martin) : Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion.
Cinq principes de conception orientée objet (Robert C. Martin) : Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion.
S — Single Responsibility PrincipleUne classe ne doit avoir qu'une seule raison de changer, c'est-à-dire répondre à un seul acteur ou besoin métier.
Une classe ne doit avoir qu'une seule raison de changer, c'est-à-dire répondre à un seul acteur ou besoin métier.
// ❌ calcule, persiste, envoie l'email et génère le PDF
class FactureService { … }
// ✅ responsabilités séparées
class CalculFacture { … } class FactureRepository { … }
class EnvoiFacture { … } class GenerateurPdf { … }O — Open/Closed PrincipleOuvert à l'extension, fermé à la modification : on ajoute un comportement en ajoutant du code (nouvelle implémentation), pas en modifiant du code existant et testé.
Ouvert à l'extension, fermé à la modification : on ajoute un comportement en ajoutant du code (nouvelle implémentation), pas en modifiant du code existant et testé.
interface RegleFrais { boolean concerne(Operation o); BigDecimal frais(Operation o); }
@Component class FraisVirementInterne implements RegleFrais { … }
@Component class FraisMobileMoney implements RegleFrais { … } // nouveau cas = nouvelle classe
BigDecimal frais(Operation o) { // ce code ne change plus
return regles.stream().filter(r -> r.concerne(o)).findFirst().map(r -> r.frais(o)).orElse(BigDecimal.ZERO);
}L — Liskov Substitution PrincipleUne sous-classe doit pouvoir remplacer sa classe mère sans casser le comportement attendu par l'appelant (mêmes contrats, pas d'exception surprise).
Une sous-classe doit pouvoir remplacer sa classe mère sans casser le comportement attendu par l'appelant (mêmes contrats, pas d'exception surprise).
class Compte { void debiter(BigDecimal m) { … } }
class CompteBloque extends Compte {
@Override void debiter(BigDecimal m) { throw new UnsupportedOperationException(); } // ❌ viole LSP
}
// ✅ modéliser "bloqué" comme un état (statut) plutôt que comme une sous-classeI — Interface Segregation PrinciplePlusieurs petites interfaces spécifiques valent mieux qu'une grosse interface générale : un client ne doit pas dépendre de méthodes qu'il n'utilise pas.
Plusieurs petites interfaces spécifiques valent mieux qu'une grosse interface générale : un client ne doit pas dépendre de méthodes qu'il n'utilise pas.
// ❌ interface CompteOperations { debiter(); crediter(); exporterPdf(); calculerInterets(); envoyerReleve(); }
interface Debitable { void debiter(Montant m); }
interface Exportable { byte[] exporterPdf(); }D — Dependency Inversion PrincipleLes modules de haut niveau (métier) ne dépendent pas des modules de bas niveau (techniques) : les deux dépendent d'abstractions. Spring réalise l'injection, le principe dit dans quel sens vont les dépendances.
Les modules de haut niveau (métier) ne dépendent pas des modules de bas niveau (techniques) : les deux dépendent d'abstractions. Spring réalise l'injection, le principe dit dans quel sens vont les dépendances.
class VirementService {
private final Notificateur notificateur; // abstraction
VirementService(Notificateur n) { this.notificateur = n; }
// ❌ private final SmsOrangeClient sms = new SmsOrangeClient();
}God classClasse « qui fait tout » (des milliers de lignes, des dizaines de dépendances) : symptôme d'une violation du SRP, difficile à tester et à faire évoluer.
Classe « qui fait tout » (des milliers de lignes, des dizaines de dépendances) : symptôme d'une violation du SRP, difficile à tester et à faire évoluer.
Couplage et cohésionCouplage : degré de dépendance entre modules (à minimiser). Cohésion : degré auquel les éléments d'un module servent un même but (à maximiser). Objectif : faible couplage, forte cohésion.
Couplage : degré de dépendance entre modules (à minimiser). Cohésion : degré auquel les éléments d'un module servent un même but (à maximiser). Objectif : faible couplage, forte cohésion.
📋 Synthèse express — Microservices
🏛 Patterns essentiels
Gestion de transactions distribuées sans 2PC. Deux variantes :
- Choreography — chaque service émet des événements
- Orchestration — un orchestrateur central coordonne
Command Query Responsibility Segregation : séparer lectures (Query) et écritures (Command). Permet d'optimiser chaque côté indépendamment. Souvent couplé à Event Sourcing.
Stocker les événements plutôt que l'état courant. L'état est reconstitué en rejouant les événements. Audit trail complet. Replay possible.
3 états : Closed (normal) → Open (erreurs dépassent seuil) → Half-Open (test). Resilience4j en Spring. Évite la propagation de pannes en cascade.
🏗 Architecture Hexagonale (Ports & Adapters)
| Couche | Rôle | Exemples |
|---|---|---|
| Domain | Logique métier pure | Entités, Value Objects, Domain Services |
| Application | Orchestration cas d'usage | Use Cases, Application Services, Ports |
| Infrastructure | Adaptateurs techniques | JPA Repos, REST Controllers, Kafka Consumers |
📐 SOLID — Rappel
| Principe | Définition | Violation typique |
|---|---|---|
| Single Responsibility | Une classe = une seule raison de changer | God class qui fait tout |
| Open/Closed | Ouvert à l'extension, fermé à la modification | Modifier la classe au lieu d'étendre |
| Liskov Substitution | Sous-classe remplaçable par sa classe mère | Override qui casse le contrat |
| Interface Segregation | Plusieurs interfaces spécialisées > une grosse | Interface avec 20 méthodes |
| Dependency Inversion | Dépendre d'abstractions, pas d'implémentations | new ConcreteService() dans le code |
⚖️ Monolithe vs Microservices
| Critère | Monolithe modulaire | Microservices |
|---|---|---|
| Déploiement | Un seul artefact | Indépendant par service |
| Scalabilité | Tout ou rien | Par service (ciblée) |
| Complexité opérationnelle | Faible | Élevée (k8s, service mesh, observabilité) |
| Cohérence des données | ACID natif (1 DB) | Éventuelle (SAGA, CQRS) |
| Latence inter-services | Appel local (nanosec) | Réseau (ms) + timeouts |
| Équipe | Petite/moyenne, 1 tech | Grandes équipes autonomes (Conway's Law) |
| Quand choisir | Début de projet, équipe <10 | Besoin réel d'autonomie/scale/orga |
🔁 Idempotence & Resilience
Une opération est idempotente si elle peut être répétée sans changer le résultat. Critique pour les paiements et les APIs REST. Implémenter via un idempotency-key header.
Toujours définir un timeout sur les appels réseau. Retry avec backoff exponentiel + jitter. Éviter les retries illimités → utiliser un circuit breaker.
- Timeout : 2-5s pour les APIs synchonres
- Retry : max 3 fois avec backoff
- Fallback : valeur par défaut ou dégradé gracieux
1Transactions & intégrité8
Transaction (SQL)Ensemble d'opérations exécuté comme une unité : soit tout est validé (COMMIT), soit tout est annulé (ROLLBACK).
Ensemble d'opérations exécuté comme une unité : soit tout est validé (COMMIT), soit tout est annulé (ROLLBACK).
BEGIN;
UPDATE compte SET solde = solde - 50000 WHERE id = 1;
UPDATE compte SET solde = solde + 50000 WHERE id = 2;
INSERT INTO mouvement(compte_id, montant, type) VALUES (1, 50000, 'DEBIT'), (2, 50000, 'CREDIT');
COMMIT; -- ou ROLLBACK; en cas d'erreur : aucun des trois changements n'est conservéACIDLes quatre garanties d'une transaction relationnelle : Atomicité (tout ou rien), Cohérence (contraintes respectées), Isolation (pas d'interférence entre transactions concurrentes), Durabilité (validé = conservé, même après un crash).
Les quatre garanties d'une transaction relationnelle : Atomicité (tout ou rien), Cohérence (contraintes respectées), Isolation (pas d'interférence entre transactions concurrentes), Durabilité (validé = conservé, même après un crash).
- Atomicité : un virement ne peut pas débiter sans créditer.
- Cohérence : la transaction amène la base d'un état valide à un autre (clés étrangères,
CHECK solde >= 0, unicité). - Isolation : deux virements simultanés ne voient pas les états intermédiaires l'un de l'autre, selon le niveau d'isolation.
- Durabilité : une fois le COMMIT confirmé, la donnée est écrite dans le journal (WAL) et survit à une panne.
Niveaux d'isolationRéglage du compromis entre cohérence et performance pour les transactions concurrentes : READ UNCOMMITTED, READ COMMITTED (défaut PostgreSQL), REPEATABLE READ, SERIALIZABLE.
Réglage du compromis entre cohérence et performance pour les transactions concurrentes : READ UNCOMMITTED, READ COMMITTED (défaut PostgreSQL), REPEATABLE READ, SERIALIZABLE.
| Niveau | Lecture sale | Lecture non répétable | Lecture fantôme |
|---|---|---|---|
| READ UNCOMMITTED | possible* | possible | possible |
| READ COMMITTED | ❌ | possible | possible |
| REPEATABLE READ | ❌ | ❌ | possible* |
| SERIALIZABLE | ❌ | ❌ | ❌ |
*PostgreSQL est plus strict que la norme : pas de lecture sale même en READ UNCOMMITTED, pas de fantôme en REPEATABLE READ.
@Transactional(isolation = Isolation.SERIALIZABLE) // à réserver aux cas critiques : risque d'échecs à relancer
public void cloturerJournee() { … }Anomalies de concurrenceLecture sale : lire une donnée non validée. Lecture non répétable : relire une ligne et trouver une autre valeur. Fantôme : relancer une requête et trouver de nouvelles lignes. Mise à jour perdue : deux écritures concurrentes, la seconde écrase la première.
Lecture sale : lire une donnée non validée. Lecture non répétable : relire une ligne et trouver une autre valeur. Fantôme : relancer une requête et trouver de nouvelles lignes. Mise à jour perdue : deux écritures concurrentes, la seconde écrase la première.
T1 lit solde = 100 T2 lit solde = 100
T1 écrit 100 - 30 = 70 T2 écrit 100 - 50 = 50 → solde final 50 : le débit de 30 est perduUPDATE … SET solde = solde - 30 atomique, verrou optimiste (@Version) ou pessimiste (FOR UPDATE).Verrouillage optimiste (@Version)On ne verrouille pas : chaque ligne a un numéro de version ; à l'UPDATE, on vérifie qu'il n'a pas changé. Si oui, l'écriture échoue (conflit) et on relance ou on informe l'utilisateur.
On ne verrouille pas : chaque ligne a un numéro de version ; à l'UPDATE, on vérifie qu'il n'a pas changé. Si oui, l'écriture échoue (conflit) et on relance ou on informe l'utilisateur.
Idéal quand les conflits sont rares (édition d'un dossier client). Pas de verrou tenu, donc bonne scalabilité.
@Entity class Dossier { @Version private Long version; … }
// Hibernate génère : UPDATE dossier SET …, version = 6 WHERE id = 42 AND version = 5
// 0 ligne modifiée → ObjectOptimisticLockingFailureException (→ HTTP 409 Conflict)Verrouillage pessimiste (SELECT … FOR UPDATE)On verrouille la ligne dès la lecture : les autres transactions qui veulent la modifier attendent la fin de la transaction. Adapté aux conflits fréquents (solde d'un compte très sollicité).
On verrouille la ligne dès la lecture : les autres transactions qui veulent la modifier attendent la fin de la transaction. Adapté aux conflits fréquents (solde d'un compte très sollicité).
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Compte c WHERE c.id = :id")
Optional<Compte> findForUpdate(@Param("id") Long id); // SELECT … FOR UPDATEContraintes d'intégritéRègles garanties par la base elle-même : clé primaire (identifiant unique), clé étrangère (référence valide), UNIQUE, NOT NULL, CHECK.
Règles garanties par la base elle-même : clé primaire (identifiant unique), clé étrangère (référence valide), UNIQUE, NOT NULL, CHECK.
CREATE TABLE compte (
id BIGSERIAL PRIMARY KEY,
client_id BIGINT NOT NULL REFERENCES client(id),
iban VARCHAR(34) NOT NULL UNIQUE,
solde NUMERIC(15,2) NOT NULL CHECK (solde >= 0),
devise CHAR(3) NOT NULL DEFAULT 'XOF'
);NormalisationOrganiser les tables pour éliminer la redondance (chaque information stockée une seule fois). La dénormalisation duplique volontairement des données pour accélérer la lecture.
Organiser les tables pour éliminer la redondance (chaque information stockée une seule fois). La dénormalisation duplique volontairement des données pour accélérer la lecture.
1NF : valeurs atomiques ; 2NF : pas de dépendance partielle à la clé ; 3NF : pas de dépendance entre colonnes non-clés. Exemple : on ne recopie pas l'adresse du client dans chaque commande, on référence client_id.
2Requêtes SQL12
SELECT et projectionSELECT choisit les colonnes à renvoyer (la projection) ; ne demander que les colonnes utiles réduit les I/O, le réseau et permet les index couvrants.
SELECT choisit les colonnes à renvoyer (la projection) ; ne demander que les colonnes utiles réduit les I/O, le réseau et permet les index couvrants.
-- ❌ ramène 40 colonnes dont des textes volumineux
SELECT * FROM transaction WHERE compte_id = 42;
-- ✅
SELECT id, date_operation, montant, libelle FROM transaction WHERE compte_id = 42;Jointure (JOIN) : vue d'ensembleUne jointure combine les lignes de deux tables selon une condition (souvent clé étrangère = clé primaire) ; son type décide du sort des lignes sans correspondance.
Une jointure combine les lignes de deux tables selon une condition (souvent clé étrangère = clé primaire) ; son type décide du sort des lignes sans correspondance.
| Type | Lignes gardées |
|---|---|
| INNER JOIN | Seulement celles qui correspondent des deux côtés |
| LEFT JOIN | Toutes celles de gauche (+ NULL à droite si pas de correspondance) |
| RIGHT JOIN | Toutes celles de droite (+ NULL à gauche) |
| FULL OUTER JOIN | Toutes celles des deux tables |
| CROSS JOIN | Produit cartésien (chaque ligne × chaque ligne) |
INNER JOINRenvoie uniquement les lignes qui ont une correspondance dans les deux tables.
Renvoie uniquement les lignes qui ont une correspondance dans les deux tables.
SELECT c.nom, co.iban
FROM client c
INNER JOIN compte co ON co.client_id = c.id; -- les clients sans compte disparaissentLEFT JOINRenvoie toutes les lignes de la table de gauche, avec les colonnes de droite remplies si une correspondance existe, NULL sinon.
Renvoie toutes les lignes de la table de gauche, avec les colonnes de droite remplies si une correspondance existe, NULL sinon.
SELECT c.id, c.nom, COUNT(co.id) AS nombre_commandes
FROM client c
LEFT JOIN commande co ON co.client_id = c.id
GROUP BY c.id, c.nom; -- les clients sans commande apparaissent avec 0
-- Trouver les clients SANS commande
SELECT c.* FROM client c LEFT JOIN commande co ON co.client_id = c.id WHERE co.id IS NULL;COUNT(*) compterait 1 pour un client sans commande (la ligne avec NULL) : utiliser COUNT(co.id).RIGHT JOINSymétrique du LEFT JOIN : toutes les lignes de la table de droite, NULL à gauche si pas de correspondance. On l'écrit rarement (on inverse les tables et on fait un LEFT JOIN).
Symétrique du LEFT JOIN : toutes les lignes de la table de droite, NULL à gauche si pas de correspondance. On l'écrit rarement (on inverse les tables et on fait un LEFT JOIN).
FULL OUTER JOINToutes les lignes des deux tables, appariées quand c'est possible, complétées par des NULL sinon. Utile pour la réconciliation (comparer deux sources).
Toutes les lignes des deux tables, appariées quand c'est possible, complétées par des NULL sinon. Utile pour la réconciliation (comparer deux sources).
-- Rapprochement : opérations banque vs relevé opérateur mobile money
SELECT b.reference, b.montant AS montant_banque, o.montant AS montant_operateur
FROM operations_banque b
FULL OUTER JOIN releve_operateur o ON o.reference = b.reference
WHERE b.reference IS NULL OR o.reference IS NULL OR b.montant <> o.montant; -- écartsGROUP BY et fonctions d'agrégationGROUP BY regroupe les lignes qui partagent une valeur ; les fonctions d'agrégation (COUNT, SUM, AVG, MIN, MAX) calculent un résultat par groupe.
GROUP BY regroupe les lignes qui partagent une valeur ; les fonctions d'agrégation (COUNT, SUM, AVG, MIN, MAX) calculent un résultat par groupe.
SELECT agence, COUNT(*) AS nb_comptes, SUM(solde) AS encours, AVG(solde) AS solde_moyen
FROM compte
GROUP BY agence
ORDER BY encours DESC;WHERE vs HAVINGWHERE filtre les lignes avant le regroupement ; HAVING filtre les groupes après le calcul des agrégats.
WHERE filtre les lignes avant le regroupement ; HAVING filtre les groupes après le calcul des agrégats.
Ordre logique d'exécution : FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. C'est pourquoi WHERE ne peut pas utiliser COUNT(*).
SELECT client_id, COUNT(*) AS nb_virements
FROM virement
WHERE date_operation >= CURRENT_DATE - 30 -- lignes : 30 derniers jours
GROUP BY client_id
HAVING COUNT(*) > 50; -- groupes : clients très actifs (alerte AML)ORDER BY et LIMITORDER BY trie le résultat ; LIMIT restreint le nombre de lignes renvoyées et OFFSET en saute un certain nombre.
ORDER BY trie le résultat ; LIMIT restreint le nombre de lignes renvoyées et OFFSET en saute un certain nombre.
SELECT * FROM transaction WHERE compte_id = 42 ORDER BY date_operation DESC LIMIT 20 OFFSET 40; -- page 3Sous-requête et CTE (WITH)Une sous-requête est une requête imbriquée ; une CTE (WITH nom AS (...)) lui donne un nom pour rendre une requête complexe lisible, et peut être récursive.
Une sous-requête est une requête imbriquée ; une CTE (WITH nom AS (...)) lui donne un nom pour rendre une requête complexe lisible, et peut être récursive.
WITH mouvements_mois AS (
SELECT compte_id, SUM(montant) AS total
FROM mouvement
WHERE date_operation >= date_trunc('month', CURRENT_DATE)
GROUP BY compte_id
)
SELECT c.iban, m.total
FROM compte c JOIN mouvements_mois m ON m.compte_id = c.id
WHERE m.total > 10000000;Fonctions de fenêtrage (window functions)Calculs sur un ensemble de lignes liées à la ligne courante (classement, cumul, comparaison avec la précédente) sans réduire le nombre de lignes, via OVER (PARTITION BY … ORDER BY …).
Calculs sur un ensemble de lignes liées à la ligne courante (classement, cumul, comparaison avec la précédente) sans réduire le nombre de lignes, via OVER (PARTITION BY … ORDER BY …).
SELECT compte_id, date_operation, montant,
SUM(montant) OVER (PARTITION BY compte_id ORDER BY date_operation) AS solde_cumule,
ROW_NUMBER() OVER (PARTITION BY compte_id ORDER BY montant DESC) AS rang
FROM mouvement;Requête préparéeRequête dont la structure SQL est envoyée séparément des valeurs (paramètres ?) : la base ne peut jamais interpréter une valeur comme du code SQL. Protection de base contre l'injection SQL.
Requête dont la structure SQL est envoyée séparément des valeurs (paramètres ?) : la base ne peut jamais interpréter une valeur comme du code SQL. Protection de base contre l'injection SQL.
// ❌ concaténation : "'; DROP TABLE users; --" devient du SQL
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
// ✅ paramètre lié
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
// ✅ JPA : @Query("… WHERE u.name = :name") avec @Param, ou les requêtes dérivées3Index & performance16
IndexStructure de données annexe qui permet de retrouver des lignes sans parcourir toute la table, comme l'index d'un livre. Accélère les lectures, ralentit un peu les écritures et occupe de l'espace.
Structure de données annexe qui permet de retrouver des lignes sans parcourir toute la table, comme l'index d'un livre. Accélère les lectures, ralentit un peu les écritures et occupe de l'espace.
CREATE INDEX idx_transaction_compte_date ON transaction (compte_id, date_operation DESC);
CREATE INDEX CONCURRENTLY idx_client_email ON client (email); -- PostgreSQL : sans bloquer les écrituresIndex B-TreeIndex par défaut : arbre équilibré trié qui gère égalité, plages (<, >, BETWEEN), tri et préfixes (LIKE 'abc%'). Couvre la grande majorité des besoins.
Index par défaut : arbre équilibré trié qui gère égalité, plages (<, >, BETWEEN), tri et préfixes (LIKE 'abc%'). Couvre la grande majorité des besoins.
Index HashIndex basé sur une fonction de hachage, utilisable uniquement pour l'égalité (=). Rarement préférable au B-Tree en PostgreSQL.
Index basé sur une fonction de hachage, utilisable uniquement pour l'égalité (=). Rarement préférable au B-Tree en PostgreSQL.
Index GINIndex inversé : pour chaque élément (clé JSON, mot, élément de tableau), la liste des lignes qui le contiennent. Idéal pour JSONB, recherche plein texte et tableaux.
Index inversé : pour chaque élément (clé JSON, mot, élément de tableau), la liste des lignes qui le contiennent. Idéal pour JSONB, recherche plein texte et tableaux.
CREATE INDEX idx_client_meta ON client USING GIN (metadonnees);
SELECT * FROM client WHERE metadonnees @> '{"segment": "PME"}'; -- utilise l'index GINIndex GiSTIndex généraliste pour les données géométriques et les plages : géolocalisation (PostGIS), intervalles de dates, recherche de proximité.
Index généraliste pour les données géométriques et les plages : géolocalisation (PostGIS), intervalles de dates, recherche de proximité.
CREATE INDEX idx_agence_position ON agence USING GIST (position);
SELECT nom FROM agence ORDER BY position <-> ST_MakePoint(-17.4677, 14.7167) LIMIT 3; -- 3 agences les plus proches de DakarIndex BRINIndex minuscule qui stocke le min/max par bloc de pages ; très efficace sur de très grosses tables dont la colonne suit l'ordre physique d'insertion (horodatages de logs, transactions).
Index minuscule qui stocke le min/max par bloc de pages ; très efficace sur de très grosses tables dont la colonne suit l'ordre physique d'insertion (horodatages de logs, transactions).
CREATE INDEX idx_audit_date ON journal_audit USING BRIN (cree_le);Index partielIndex qui ne couvre qu'un sous-ensemble de lignes (clause WHERE) : plus petit et plus rapide quand on interroge toujours ce sous-ensemble.
Index qui ne couvre qu'un sous-ensemble de lignes (clause WHERE) : plus petit et plus rapide quand on interroge toujours ce sous-ensemble.
CREATE INDEX idx_virement_en_attente ON virement (date_creation) WHERE statut = 'EN_ATTENTE';Index composite et ordre des colonnesIndex sur plusieurs colonnes ; il sert aux requêtes qui filtrent sur ses premières colonnes (règle du préfixe gauche). L'ordre des colonnes compte.
Index sur plusieurs colonnes ; il sert aux requêtes qui filtrent sur ses premières colonnes (règle du préfixe gauche). L'ordre des colonnes compte.
CREATE INDEX idx_tx ON transaction (compte_id, date_operation);
-- ✅ WHERE compte_id = 42
-- ✅ WHERE compte_id = 42 AND date_operation > '2026-01-01'
-- ❌ WHERE date_operation > '2026-01-01' (pas de compte_id : index peu utile)Index couvrant (INCLUDE) et Index Only ScanIndex qui contient toutes les colonnes demandées par la requête : la base répond sans lire la table (Index Only Scan).
Index qui contient toutes les colonnes demandées par la requête : la base répond sans lire la table (Index Only Scan).
CREATE INDEX idx_tx_couvrant ON transaction (compte_id, date_operation) INCLUDE (montant);
SELECT date_operation, montant FROM transaction WHERE compte_id = 42; -- Index Only ScanEXPLAIN ANALYZEAffiche le plan d'exécution choisi par la base et, avec ANALYZE, exécute réellement la requête pour donner temps et nombre de lignes réels à chaque étape.
Affiche le plan d'exécution choisi par la base et, avec ANALYZE, exécute réellement la requête pour donner temps et nombre de lignes réels à chaque étape.
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM transaction WHERE compte_id = 42 AND date_operation > now() - interval '30 days';
-- Index Scan using idx_tx on transaction (cost=0.43..85.20 rows=120) (actual time=0.05..0.31 rows=118)
-- Execution Time: 0.42 msrows estimé et réel (statistiques fausses ?), et les nœuds les plus coûteux.BEGIN; … ROLLBACK;.Seq Scan, Index Scan, Bitmap Heap ScanStratégies de lecture : Seq Scan lit toute la table ; Index Scan suit l'index ligne par ligne ; Bitmap Heap Scan collecte via l'index les pages à lire puis les lit en bloc (bon compromis pour beaucoup de lignes ou plusieurs conditions).
Stratégies de lecture : Seq Scan lit toute la table ; Index Scan suit l'index ligne par ligne ; Bitmap Heap Scan collecte via l'index les pages à lire puis les lit en bloc (bon compromis pour beaucoup de lignes ou plusieurs conditions).
Un Seq Scan n'est pas toujours mauvais : sur une petite table ou si la requête ramène une grande partie des lignes, c'est le plus rapide. Il devient suspect sur une grande table filtrée sur peu de lignes.
Cardinalité et sélectivitéCardinalité : nombre de valeurs distinctes d'une colonne. Sélectivité : proportion de lignes retenues par un filtre. Un index est utile sur un filtre très sélectif (peu de lignes).
Cardinalité : nombre de valeurs distinctes d'une colonne. Sélectivité : proportion de lignes retenues par un filtre. Un index est utile sur un filtre très sélectif (peu de lignes).
Un index sur sexe (2 valeurs) sert rarement ; un index sur email (unique) est très sélectif.
Statistiques : ANALYZE et pg_statsANALYZE collecte des statistiques (distribution des valeurs) que l'optimiseur utilise pour estimer les coûts ; pg_stats permet de les consulter.
ANALYZE collecte des statistiques (distribution des valeurs) que l'optimiseur utilise pour estimer les coûts ; pg_stats permet de les consulter.
ANALYZE transaction;
SELECT attname, n_distinct, most_common_vals FROM pg_stats WHERE tablename = 'transaction';MVCC et VACUUMPostgreSQL ne modifie pas une ligne en place : il crée une nouvelle version (MVCC), ce qui permet aux lecteurs de ne pas bloquer les écrivains. VACUUM récupère l'espace des anciennes versions.
PostgreSQL ne modifie pas une ligne en place : il crée une nouvelle version (MVCC), ce qui permet aux lecteurs de ne pas bloquer les écrivains. VACUUM récupère l'espace des anciennes versions.
L'autovacuum s'en charge normalement. Sur une table très mise à jour, un autovacuum en retard entraîne du « bloat » (table gonflée, requêtes plus lentes).
VACUUM (ANALYZE, VERBOSE) transaction;
SELECT relname, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;Pagination : OFFSET vs keysetOFFSET relit et jette toutes les lignes précédentes (lent sur les pages lointaines, instable si des lignes arrivent). La pagination keyset repart de la dernière valeur vue : coût constant.
OFFSET relit et jette toutes les lignes précédentes (lent sur les pages lointaines, instable si des lignes arrivent). La pagination keyset repart de la dernière valeur vue : coût constant.
-- OFFSET : page 5000 = 100 000 lignes lues puis ignorées
SELECT * FROM transaction ORDER BY id DESC LIMIT 20 OFFSET 100000;
-- Keyset : on transmet le dernier id reçu
SELECT * FROM transaction WHERE id < :dernier_id ORDER BY id DESC LIMIT 20;Pool de connexions (HikariCP)Ensemble de connexions base ouvertes à l'avance et réutilisées (ouvrir une connexion coûte des dizaines de ms). HikariCP est le pool par défaut de Spring Boot (10 connexions par défaut).
Ensemble de connexions base ouvertes à l'avance et réutilisées (ouvrir une connexion coûte des dizaines de ms). HikariCP est le pool par défaut de Spring Boot (10 connexions par défaut).
spring.datasource.hikari:
maximum-pool-size: 20
connection-timeout: 3000 # ms d'attente max pour obtenir une connexion
leak-detection-threshold: 200004PostgreSQL4
PostgreSQLBase relationnelle open source robuste et très complète : ACID, MVCC, JSONB, index avancés, CTE, fonctions fenêtres, extensions (PostGIS, pg_trgm).
Base relationnelle open source robuste et très complète : ACID, MVCC, JSONB, index avancés, CTE, fonctions fenêtres, extensions (PostGIS, pg_trgm).
JSONBType PostgreSQL qui stocke du JSON sous forme binaire indexable (GIN) ; permet de mêler données relationnelles et attributs flexibles.
Type PostgreSQL qui stocke du JSON sous forme binaire indexable (GIN) ; permet de mêler données relationnelles et attributs flexibles.
ALTER TABLE client ADD COLUMN metadonnees JSONB DEFAULT '{}';
UPDATE client SET metadonnees = metadonnees || '{"canal": "USSD"}' WHERE id = 42;
SELECT metadonnees->>'canal' FROM client WHERE metadonnees @> '{"segment": "PME"}';Partitionnement de tableDécouper une très grosse table en sous-tables (par date, par région) : les requêtes ne lisent que les partitions concernées et l'archivage devient un simple DETACH.
Découper une très grosse table en sous-tables (par date, par région) : les requêtes ne lisent que les partitions concernées et l'archivage devient un simple DETACH.
CREATE TABLE transaction (id BIGINT, date_operation DATE, montant NUMERIC(15,2))
PARTITION BY RANGE (date_operation);
CREATE TABLE transaction_2026_10 PARTITION OF transaction
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');Réplication et réplicas de lectureCopie continue de la base principale vers une ou plusieurs répliques ; on y envoie les lectures lourdes (rapports) pour soulager le primaire, et elles servent de secours en cas de panne.
Copie continue de la base principale vers une ou plusieurs répliques ; on y envoie les lectures lourdes (rapports) pour soulager le primaire, et elles servent de secours en cas de panne.
5MongoDB & NoSQL7
MongoDBBase NoSQL orientée documents : les données sont stockées en documents BSON (proches du JSON) dans des collections, sans schéma imposé par la base.
Base NoSQL orientée documents : les données sont stockées en documents BSON (proches du JSON) dans des collections, sans schéma imposé par la base.
Adaptée aux données hétérogènes ou évolutives (catalogues, contenus, logs métier, profils). Pour des écritures financières fortement relationnelles, PostgreSQL reste préféré.
db.clients.insertOne({
nom: "Awa Ndiaye",
telephones: ["+221770000000"],
adresse: { ville: "Dakar", quartier: "Plateau" }
});Document et collectionUn document est un enregistrement JSON/BSON (avec un _id unique) ; une collection regroupe des documents, comme une table regroupe des lignes, mais sans structure imposée.
Un document est un enregistrement JSON/BSON (avec un _id unique) ; une collection regroupe des documents, comme une table regroupe des lignes, mais sans structure imposée.
Embedding vs referencingModélisation MongoDB : imbriquer les données lues ensemble dans le même document (embedding), ou stocker une référence vers un autre document (referencing) quand elles sont volumineuses, partagées ou à croissance illimitée.
Modélisation MongoDB : imbriquer les données lues ensemble dans le même document (embedding), ou stocker une référence vers un autre document (referencing) quand elles sont volumineuses, partagées ou à croissance illimitée.
// Embedding : adresse lue toujours avec le client
{ _id: 1, nom: "Awa", adresse: { ville: "Dakar" } }
// Referencing : des milliers de transactions → collection séparée
{ _id: 9001, clientId: 1, montant: 25000 }Aggregation PipelineSuite d'étapes ($match, $group, $sort, $project, $lookup…) que traversent les documents, chaque étape transformant le résultat de la précédente ; l'équivalent MongoDB du GROUP BY/JOIN.
Suite d'étapes ($match, $group, $sort, $project, $lookup…) que traversent les documents, chaque étape transformant le résultat de la précédente ; l'équivalent MongoDB du GROUP BY/JOIN.
db.orders.aggregate([
{ $match: { status: "ACTIVE", createdAt: { $gte: ISODate("2026-01-01") } } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" }, count: { $sum: 1 } } },
{ $sort: { total: -1 } },
{ $limit: 10 }
]);Étapes $match, $group, $sort, $limit, $lookup$match filtre (comme WHERE), $group regroupe et agrège, $sort trie, $limit coupe, $project choisit les champs, $lookup joint une autre collection.
$match filtre (comme WHERE), $group regroupe et agrège, $sort trie, $limit coupe, $project choisit les champs, $lookup joint une autre collection.
$match le plus tôt possible : il peut utiliser un index et réduit le volume pour les étapes suivantes.Index MongoDBComme en SQL, sans index une requête parcourt toute la collection (COLLSCAN) ; on crée des index simples, composés, uniques, TTL ou texte.
Comme en SQL, sans index une requête parcourt toute la collection (COLLSCAN) ; on crée des index simples, composés, uniques, TTL ou texte.
db.transactions.createIndex({ clientId: 1, date: -1 });
db.sessions.createIndex({ creeLe: 1 }, { expireAfterSeconds: 3600 }); // TTL : suppression auto
db.transactions.find({ clientId: 1 }).sort({ date: -1 }).explain("executionStats");Théorème CAPEn cas de partition réseau, un système distribué doit choisir entre Cohérence (tout le monde voit la même donnée) et Disponibilité (tout le monde reçoit une réponse) ; la tolérance au partitionnement n'est pas négociable.
En cas de partition réseau, un système distribué doit choisir entre Cohérence (tout le monde voit la même donnée) et Disponibilité (tout le monde reçoit une réponse) ; la tolérance au partitionnement n'est pas négociable.
Un système de paiement favorise la cohérence (refuser plutôt que débiter deux fois) ; un fil d'actualité favorise la disponibilité.
6Redis & cache13
RedisBase clé-valeur en mémoire, ultra-rapide (sub-milliseconde), avec structures de données riches ; utilisée comme cache, store de sessions, compteur, verrou distribué, file légère.
Base clé-valeur en mémoire, ultra-rapide (sub-milliseconde), avec structures de données riches ; utilisée comme cache, store de sessions, compteur, verrou distribué, file légère.
SET taux:EUR "655.957" EX 3600 # valeur avec expiration 1 h
GET taux:EUR
INCR compteur:otp:+221770000000TTL (Time To Live)Durée de vie d'une clé : passé ce délai, elle est supprimée automatiquement. Indispensable pour qu'un cache ne serve pas éternellement des données périmées.
Durée de vie d'une clé : passé ce délai, elle est supprimée automatiquement. Indispensable pour qu'un cache ne serve pas éternellement des données périmées.
EXPIRE session:abc 1800
TTL session:abc # secondes restantesStructures Redis : String, Hash, List, SetString : valeur simple ou compteur. Hash : objet à champs (user:123 → nom, email). List : liste ordonnée (file FIFO/LIFO). Set : ensemble sans doublons.
String : valeur simple ou compteur. Hash : objet à champs (user:123 → nom, email). List : liste ordonnée (file FIFO/LIFO). Set : ensemble sans doublons.
HSET client:42 nom "Awa" ville "Dakar"
HGET client:42 ville
LPUSH file:notifications "msg1" # puis BRPOP pour consommer
SADD agences:ouvertes "Plateau" "Medina"
SISMEMBER agences:ouvertes "Plateau"Sorted Set (ZSET)Ensemble dont chaque élément a un score, maintenu trié : classements, files de priorité, fenêtres glissantes pour le rate limiting.
Ensemble dont chaque élément a un score, maintenu trié : classements, files de priorité, fenêtres glissantes pour le rate limiting.
ZADD classement:agences 1250000000 "Plateau" 980000000 "Medina"
ZREVRANGE classement:agences 0 9 WITHSCORES # top 10
# Rate limiting fenêtre glissante : un élément par requête, score = timestamp
ZADD rl:client:42 1760440000123 "req-uuid"
ZREMRANGEBYSCORE rl:client:42 0 1760439940123 # purge > 60 s
ZCARD rl:client:42 # requêtes dans la dernière minuteRedis StreamsJournal d'événements en append-only dans Redis, avec groupes de consommateurs et accusés de réception : une alternative légère à Kafka pour des volumes modérés.
Journal d'événements en append-only dans Redis, avec groupes de consommateurs et accusés de réception : une alternative légère à Kafka pour des volumes modérés.
XADD evenements * type "VIREMENT" montant "25000"
XREADGROUP GROUP notif c1 COUNT 10 STREAMS evenements >
XACK evenements notif 1760440000123-0Cache-Aside (lazy loading)L'application lit d'abord le cache ; en cas d'absence (miss), elle lit la base, puis écrit le résultat dans le cache. C'est le pattern le plus courant (ce que fait @Cacheable).
L'application lit d'abord le cache ; en cas d'absence (miss), elle lit la base, puis écrit le résultat dans le cache. C'est le pattern le plus courant (ce que fait @Cacheable).
public Taux taux(String devise) {
Taux t = redis.opsForValue().get("taux:" + devise);
if (t != null) return t; // hit
t = repo.findByDevise(devise); // miss → base
redis.opsForValue().set("taux:" + devise, t, Duration.ofMinutes(30));
return t;
}Read-ThroughLe cache lui-même charge la donnée depuis la base en cas de miss (via un « loader »), l'application ne parle qu'au cache.
Le cache lui-même charge la donnée depuis la base en cas de miss (via un « loader »), l'application ne parle qu'au cache.
LoadingCache<String, Taux> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30))
.build(devise -> repo.findByDevise(devise)); // loader appelé en cas de missWrite-ThroughChaque écriture met à jour le cache et la base de façon synchrone : le cache est toujours à jour, au prix d'une écriture un peu plus lente.
Chaque écriture met à jour le cache et la base de façon synchrone : le cache est toujours à jour, au prix d'une écriture un peu plus lente.
Write-Behind (Write-Back)L'écriture va d'abord dans le cache, et la base est mise à jour plus tard, en asynchrone et par lots : très rapide, mais risque de perte si le cache tombe avant la synchronisation.
L'écriture va d'abord dans le cache, et la base est mise à jour plus tard, en asynchrone et par lots : très rapide, mais risque de perte si le cache tombe avant la synchronisation.
Cache Stampede (thundering herd)Quand une clé très demandée expire, des centaines de requêtes ratent le cache en même temps et frappent toutes la base, qui peut s'effondrer.
Quand une clé très demandée expire, des centaines de requêtes ratent le cache en même temps et frappent toutes la base, qui peut s'effondrer.
Remèdes : un seul recalcul grâce à un verrou (SETNX), TTL avec jitter pour étaler les expirations, rafraîchissement anticipé avant expiration, ou servir la valeur périmée pendant le recalcul.
SETNX et verrou distribuéSET cle valeur NX PX 5000 n'écrit la clé que si elle n'existe pas : le premier qui réussit obtient le verrou, avec une expiration de sécurité. Sert à coordonner plusieurs instances.
SET cle valeur NX PX 5000 n'écrit la clé que si elle n'existe pas : le premier qui réussit obtient le verrou, avec une expiration de sécurité. Sert à coordonner plusieurs instances.
SET lock:recalcul:taux "pod-7" NX PX 5000 # OK → j'ai le verrou ; (nil) → quelqu'un d'autre l'aRLock lock = redisson.getLock("lock:cloture:" + date); // Redisson gère renouvellement et libération
if (lock.tryLock(0, 30, TimeUnit.SECONDS)) {
try { cloturerJournee(date); } finally { lock.unlock(); }
}Invalidation de cacheSupprimer ou mettre à jour une entrée de cache quand la donnée source change ; « il n'y a que deux choses difficiles en informatique : l'invalidation de cache et nommer les choses ».
Supprimer ou mettre à jour une entrée de cache quand la donnée source change ; « il n'y a que deux choses difficiles en informatique : l'invalidation de cache et nommer les choses ».
Stratégies : TTL court, suppression explicite après écriture (@CacheEvict), événement de changement diffusé à toutes les instances.
Politique d'éviction (LRU, LFU)Règle qui choisit quelles clés supprimer quand la mémoire est pleine : LRU (moins récemment utilisée), LFU (moins fréquemment utilisée), TTL le plus proche…
Règle qui choisit quelles clés supprimer quand la mémoire est pleine : LRU (moins récemment utilisée), LFU (moins fréquemment utilisée), TTL le plus proche…
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lru7Elasticsearch2
Elasticsearch et index inverséMoteur de recherche et d'analyse distribué basé sur Lucene ; son index inversé associe chaque mot aux documents qui le contiennent, ce qui rend la recherche plein texte très rapide.
Moteur de recherche et d'analyse distribué basé sur Lucene ; son index inversé associe chaque mot aux documents qui le contiennent, ce qui rend la recherche plein texte très rapide.
Usage : recherche de clients par nom approximatif, recherche dans les logs (pile ELK), tableaux de bord analytiques. Ce n'est pas une base transactionnelle de référence : on y réplique les données depuis la base principale.
GET /clients/_search
{ "query": { "match": { "nom": { "query": "ndiay", "fuzziness": "AUTO" } } } }Analyseur et recherche plein texteL'analyseur découpe le texte en termes (tokenisation), met en minuscules, retire accents et mots vides, applique la racinisation ; la même analyse est faite sur la requête pour pouvoir comparer.
L'analyseur découpe le texte en termes (tokenisation), met en minuscules, retire accents et mots vides, applique la racinisation ; la même analyse est faite sur la requête pour pouvoir comparer.
PUT /clients
{ "mappings": { "properties": {
"nom": { "type": "text", "analyzer": "french" },
"iban": { "type": "keyword" } } } }text = analysé (recherche floue) ; keyword = valeur exacte (filtres, tri, agrégations).📋 Synthèse express — BDD
🗃️ SQL — Fondamentaux
- Atomicité — tout ou rien
- Cohérence — respecte les contraintes
- Isolation — transactions invisibles entre elles
- Durabilité — committed = persisté même après crash
- INNER JOIN — lignes matchant des deux côtés
- LEFT JOIN — toutes les lignes de gauche + match si existant
- RIGHT JOIN — toutes les lignes de droite
- FULL OUTER JOIN — toutes les lignes des deux tables
WHERE filtre les lignes avant l'agrégation (GROUP BY).HAVING filtre les groupes après l'agrégation.
📊 Optimisation SQL — 6 étapes
| # | Étape | Outils |
|---|---|---|
| 1 | Mesurer avec le plan d'exécution | EXPLAIN ANALYZE |
| 2 | Identifier Seq Scan sur grandes tables | Seq Scan → créer index |
| 3 | Éviter SELECT * si inutile | Projections ciblées |
| 4 | Paginer les listes | LIMIT/OFFSET ou keyset pagination |
| 5 | Corriger le N+1 JPA | JOIN FETCH, @EntityGraph |
| 6 | Vérifier la cardinalité des index | pg_stats, ANALYZE |
🐘 PostgreSQL
| Type d'index | Usage |
|---|---|
| B-Tree (défaut) | Equality, range, ORDER BY — 90% des cas |
| Hash | Equality only, plus rapide pour = |
| GIN | JSONB, full-text search, arrays |
| GiST | Géospatial (PostGIS), formes géométriques |
| BRIN | Colonnes corrélées à leur position physique (timestamps séquentiels) |
| Partial index | Index sur sous-ensemble : WHERE status = 'ACTIVE' |
🍃 MongoDB
⚡ Redis — Structures & Patterns
- String — cache simple, compteurs
- Hash — objets (user:123 → {name, email})
- List — queues, LIFO/FIFO
- Set — tags, unicité
- Sorted Set — leaderboards, rate limiting
- Stream — event log, alternative Kafka léger
- Cache-Aside — app gère miss/hit manuellement
- Write-Through — écriture sync en cache+DB
- Write-Behind — écriture async en DB
- Read-Through — cache charge depuis DB si miss
Attention au Cache Stampede (thundering herd) sur expiration.
@> ou ?.SETNX comme lock distribué (1 seul recharge), TTL avec jitter (expiration aléatoire), ou pre-warming avant expiration.WHERE s'exécute avant GROUP BY et filtre les lignes individuelles. HAVING s'exécute après GROUP BY et filtre les groupes agrégés. Exemple : HAVING COUNT(*) > 5 ne peut pas s'écrire avec WHERE car COUNT() n'existe pas encore à ce stade.1Le contrat entre le front et l'API9
Architecture SPA + API RESTLe front Angular (Single Page Application) est servi une fois en fichiers statiques puis échange uniquement des données JSON avec l'API Spring Boot ; le serveur ne génère plus de HTML.
Le front Angular (Single Page Application) est servi une fois en fichiers statiques puis échange uniquement des données JSON avec l'API Spring Boot ; le serveur ne génère plus de HTML.
Les deux applications sont déployées et versionnées séparément. Conséquences : CORS (domaines différents), authentification par token, contrat d'API à maintenir (OpenAPI), gestion des erreurs côté client.
Navigateur ─► https://app.banque.sn (Angular, NGINX/CDN)
└─► https://api.banque.sn/api/v1/... (Spring Boot, JSON)Sérialisation JSON (Jackson ↔ TypeScript)Spring transforme les objets Java en JSON (Jackson) ; Angular reçoit ce JSON et le type avec une interface TypeScript. Les deux doivent correspondre champ par champ.
Spring transforme les objets Java en JSON (Jackson) ; Angular reçoit ce JSON et le type avec une interface TypeScript. Les deux doivent correspondre champ par champ.
TypeScript ne vérifie rien à l'exécution : si Java renomme nomComplet en nom, Angular affichera undefined sans erreur. D'où l'intérêt d'un contrat OpenAPI et de clients générés.
public record ClientDto(Long id, String nom, String email, LocalDate dateNaissance) {}export interface ClientDto { id: number; nom: string; email: string; dateNaissance: string; } // ISO "1990-05-12"Dates et fuseaux horaires dans le JSONÉchanger les dates en texte ISO-8601 : LocalDate → "2026-10-14", Instant → "2026-10-14T16:33:00Z" (UTC). Le front les convertit pour l'affichage dans le fuseau de l'utilisateur.
Échanger les dates en texte ISO-8601 : LocalDate → "2026-10-14", Instant → "2026-10-14T16:33:00Z" (UTC). Le front les convertit pour l'affichage dans le fuseau de l'utilisateur.
public record OperationDto(Instant horodatage, LocalDate dateValeur) {}
// {"horodatage":"2026-10-14T16:33:00Z","dateValeur":"2026-10-15"}const d = new Date(op.horodatage); // instant précis
{{ op.horodatage | date:'dd/MM/yyyy HH:mm' }}new Date("2026-10-15") est interprété en UTC minuit : selon le fuseau du navigateur, la date affichée peut reculer d'un jour. Garder les dates sans heure comme chaînes ou utiliser une bibliothèque de dates.Montants : BigDecimal ↔ JSONLes montants sont des BigDecimal côté Java ; en JSON, les envoyer comme chaîne ("15000.50") ou en entier d'unités mineures évite les erreurs d'arrondi des nombres flottants JavaScript.
Les montants sont des BigDecimal côté Java ; en JSON, les envoyer comme chaîne ("15000.50") ou en entier d'unités mineures évite les erreurs d'arrondi des nombres flottants JavaScript.
public record SoldeDto(@JsonFormat(shape = JsonFormat.Shape.STRING) BigDecimal solde, String devise) {}
// {"solde":"1250000.75","devise":"XOF"}0.1 + 0.2; // 0.30000000000000004 en JavaScript → ne jamais calculer de l'argent en number côté frontBearer token (en-tête Authorization)Le front envoie le jeton d'accès dans l'en-tête Authorization: Bearer <token> de chaque requête ; « bearer » signifie que quiconque possède le jeton est considéré comme authentifié.
Le front envoie le jeton d'accès dans l'en-tête Authorization: Bearer <token> de chaque requête ; « bearer » signifie que quiconque possède le jeton est considéré comme authentifié.
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).accessToken();
if (!token || !req.url.startsWith(environment.apiUrl)) return next(req); // jamais vers un domaine tiers
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};Requête preflight CORS (OPTIONS)Avant une requête « non simple » (JSON, en-tête Authorization, PUT/DELETE) vers un autre domaine, le navigateur envoie une requête OPTIONS pour demander l'autorisation ; si la réponse ne contient pas les bons en-têtes CORS, la vraie requête n'est jamais envoyée.
Avant une requête « non simple » (JSON, en-tête Authorization, PUT/DELETE) vers un autre domaine, le navigateur envoie une requête OPTIONS pour demander l'autorisation ; si la réponse ne contient pas les bons en-têtes CORS, la vraie requête n'est jamais envoyée.
OPTIONS /api/v1/virements
Origin: https://app.banque.sn
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 200
Access-Control-Allow-Origin: https://app.banque.sn
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-typehttp.cors(...)) pour qu'il soit traité avant l'authentification.Proxy de développement Angular (proxy.conf.json)En développement, ng serve peut relayer les appels /api vers Spring (localhost:8080) : le navigateur ne voit qu'une seule origine, donc pas de problème CORS en local.
En développement, ng serve peut relayer les appels /api vers Spring (localhost:8080) : le navigateur ne voit qu'une seule origine, donc pas de problème CORS en local.
{
"/api": { "target": "http://localhost:8080", "secure": false, "changeOrigin": true }
}ng serve --proxy-config proxy.conf.jsonOpenAPI / Swagger (springdoc)OpenAPI est un format standard qui décrit une API REST (endpoints, paramètres, schémas, codes de retour) ; springdoc le génère depuis le code Spring et fournit l'interface Swagger UI.
OpenAPI est un format standard qui décrit une API REST (endpoints, paramètres, schémas, codes de retour) ; springdoc le génère depuis le code Spring et fournit l'interface Swagger UI.
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>@Operation(summary = "Créer un client", responses = {
@ApiResponse(responseCode = "201", description = "Client créé"),
@ApiResponse(responseCode = "409", description = "Email déjà utilisé") })
@PostMapping
ResponseEntity<ClientDto> creer(@Valid @RequestBody CreateClientRequest req) { … }
// Swagger UI : /swagger-ui.html — spec : /v3/api-docsGénération du client Angular depuis OpenAPIÀ partir de la spec OpenAPI, un générateur produit les interfaces TypeScript et les services HttpClient : le front ne peut plus diverger du back sans erreur de compilation.
À partir de la spec OpenAPI, un générateur produit les interfaces TypeScript et les services HttpClient : le front ne peut plus diverger du back sans erreur de compilation.
npx @openapitools/openapi-generator-cli generate \
-i http://localhost:8080/v3/api-docs -g typescript-angular -o src/app/api2Erreurs, fichiers et temps réel6
Codes de statut HTTPNombre à 3 chiffres qui résume le résultat : 2xx succès, 3xx redirection, 4xx erreur du client, 5xx erreur du serveur.
Nombre à 3 chiffres qui résume le résultat : 2xx succès, 3xx redirection, 4xx erreur du client, 5xx erreur du serveur.
| Code | Signification | Exemple |
|---|---|---|
| 200 OK | Succès avec corps | GET d'un compte |
| 201 Created | Ressource créée (+ Location) | POST d'un virement |
| 204 No Content | Succès sans corps | DELETE |
| 400 Bad Request | Requête invalide | Validation @Valid échouée |
| 401 Unauthorized | Non authentifié | Token absent ou expiré |
| 403 Forbidden | Authentifié mais non autorisé | Client qui lit le compte d'un autre |
| 404 Not Found | Ressource inexistante | Compte introuvable |
| 409 Conflict | Conflit d'état | Email déjà utilisé, version obsolète |
| 422 Unprocessable Content | Règle métier violée | Solde insuffisant |
| 429 Too Many Requests | Limite de débit atteinte | Trop d'OTP demandés |
| 500 Internal Server Error | Bug serveur | Exception non gérée |
| 503 Service Unavailable | Indisponible temporairement | Maintenance, surcharge |
401 vs 403401 : « je ne sais pas qui tu es » (pas de token, token invalide ou expiré) → se reconnecter. 403 : « je sais qui tu es, mais tu n'as pas le droit » → message d'accès refusé.
401 : « je ne sais pas qui tu es » (pas de token, token invalide ou expiré) → se reconnecter. 403 : « je sais qui tu es, mais tu n'as pas le droit » → message d'accès refusé.
catchError((e: HttpErrorResponse) => {
if (e.status === 401) { auth.rafraichirOuReconnecter(); }
else if (e.status === 403) { toast.erreur("Vous n'avez pas les droits pour cette action"); }
return throwError(() => e);
})HttpErrorResponse et gestion d'erreurs côté AngularObjet reçu par Angular quand une requête échoue : status, message, et error (le corps renvoyé par Spring, par exemple un ProblemDetail).
Objet reçu par Angular quand une requête échoue : status, message, et error (le corps renvoyé par Spring, par exemple un ProblemDetail).
this.api.virer(demande).subscribe({
next: () => this.toast.succes('Virement effectué'),
error: (e: HttpErrorResponse) => {
const pb = e.error as { title?: string; detail?: string };
this.toast.erreur(pb?.detail ?? 'Erreur inattendue, réessayez plus tard');
},
});Upload de fichier (multipart)Envoi de fichier en multipart/form-data : Angular construit un FormData, Spring le reçoit en MultipartFile. Taille et type doivent être vérifiés côté serveur.
Envoi de fichier en multipart/form-data : Angular construit un FormData, Spring le reçoit en MultipartFile. Taille et type doivent être vérifiés côté serveur.
const fd = new FormData();
fd.append('fichier', file);
fd.append('type', 'PIECE_IDENTITE');
this.http.post('/api/v1/kyc/documents', fd, { reportProgress: true, observe: 'events' });@PostMapping(path = "/kyc/documents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
DocumentDto deposer(@RequestPart("fichier") MultipartFile fichier, @RequestPart("type") String type) {
if (!List.of("application/pdf", "image/jpeg", "image/png").contains(fichier.getContentType()))
throw new FormatNonAutoriseException();
return service.stocker(fichier, type);
}spring.servlet.multipart.max-file-size=5MBTéléchargement de fichier (Blob)Spring renvoie des octets avec Content-Type et Content-Disposition ; Angular les reçoit en Blob (responseType: 'blob') et propose l'enregistrement.
Spring renvoie des octets avec Content-Type et Content-Disposition ; Angular les reçoit en Blob (responseType: 'blob') et propose l'enregistrement.
@GetMapping("/releves/{mois}.pdf")
ResponseEntity<byte[]> releve(@PathVariable YearMonth mois) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=releve-" + mois + ".pdf")
.body(service.pdf(mois));
}this.http.get(`/api/v1/releves/${mois}.pdf`, { responseType: 'blob' }).subscribe(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `releve-${mois}.pdf`; a.click();
URL.revokeObjectURL(url);
});Temps réel : Server-Sent Events et WebSocketSSE : flux unidirectionnel serveur → navigateur sur HTTP (notifications, statut d'un paiement). WebSocket : canal bidirectionnel permanent (chat, cotations). Alternative simple : le polling.
SSE : flux unidirectionnel serveur → navigateur sur HTTP (notifications, statut d'un paiement). WebSocket : canal bidirectionnel permanent (chat, cotations). Alternative simple : le polling.
@GetMapping(path = "/paiements/{id}/statut", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter suivre(@PathVariable String id) { return suiviService.abonner(id); }const source = new EventSource(`/api/v1/paiements/${id}/statut`);
source.onmessage = e => this.statut.set(JSON.parse(e.data));EventSource ne permet pas d'ajouter un en-tête Authorization : il faut un cookie, un jeton à usage unique dans l'URL, ou une bibliothèque SSE basée sur fetch.📋 Synthèse express — Angular↔Spring
🔄 Flux en 8 étapes
| # | Couche | Rôle |
|---|---|---|
| 1 | Formulaire Angular (Reactive Form) | Saisie utilisateur + validation UX côté client |
| 2 | Composant → Service Angular | Appel HttpClient avec le DTO sérialisé en JSON |
| 3 | Intercepteur HTTP Angular | Ajout du header Authorization: Bearer <token> |
| 4 | Spring Security (filtre) | Authentification JWT + autorisation (rôles/scopes) |
| 5 | Controller Spring (@RestController) | Validation @Valid du DTO + mapping HTTP |
| 6 | Service Spring (@Service @Transactional) | Logique métier, règles, transaction |
| 7 | Repository JPA (@Repository) | Requêtes DB, persistance Entity |
| 8 | Réponse DTO → Angular | Code HTTP + JSON → Angular met à jour le template |
@Valid + @RestControllerAdvice).
📦 Exemple concret — Création d'un client
🔧 Points d'attention clés
Configurer @CrossOrigin ou une CorsConfigurationSource globale pour autoriser les requêtes depuis http://localhost:4200 en dev et le domaine de prod.
Les DTOs TypeScript et les records/DTO Java doivent correspondre champ par champ. Documenter avec OpenAPI/Swagger (@Operation, @Schema). Utiliser une spec OpenAPI partagée si possible.
Angular : intercepter les erreurs dans le service avec catchError. Gérer 401 (redirect login), 403 (accès refusé), 404, 409 (conflit), 500. Afficher des messages utilisateur adaptés.
Ne jamais stocker les secrets dans le bundle Angular. Guards Angular = UX seulement, pas sécurité réelle. La vraie sécurité est côté Spring Security. Environments Angular pour les URLs.
1Fondamentaux de la messagerie4
Message brokerIntermédiaire qui reçoit des messages de producteurs, les stocke et les distribue à des consommateurs ; il découple les services dans le temps (l'un peut être arrêté) et en charge (il absorbe les pics).
Intermédiaire qui reçoit des messages de producteurs, les stocke et les distribue à des consommateurs ; il découple les services dans le temps (l'un peut être arrêté) et en charge (il absorbe les pics).
Exemples : Apache Kafka (journal d'événements distribué), RabbitMQ (routage de messages AMQP), AWS SQS/SNS (service managé).
Service Virement ──publie──► [Broker] ──distribue──► Service Notification
└────────────► Service Anti-fraudePoint-à-point (queue) vs Publish/SubscribePoint-à-point : chaque message est traité par un seul consommateur (répartition de travail). Pub/Sub : chaque message est reçu par tous les abonnés intéressés (diffusion d'événements).
Point-à-point : chaque message est traité par un seul consommateur (répartition de travail). Pub/Sub : chaque message est reçu par tous les abonnés intéressés (diffusion d'événements).
Avec Kafka, les deux se font avec le même topic : consommateurs du même groupe → répartition ; groupes différents → chacun reçoit tout.
Événement, commande et messageUn événement annonce un fait passé (« VirementEffectué ») sans savoir qui l'écoute ; une commande demande une action à un destinataire précis (« EnvoyerSms ») ; les deux voyagent sous forme de messages.
Un événement annonce un fait passé (« VirementEffectué ») sans savoir qui l'écoute ; une commande demande une action à un destinataire précis (« EnvoyerSms ») ; les deux voyagent sous forme de messages.
public record VirementEffectue(UUID virementId, String compte, BigDecimal montant, Instant date) {} // événement
public record EnvoyerSms(String telephone, String texte) {} // commandeBackpressure (contre-pression)Capacité d'un consommateur à signaler qu'il est saturé pour que le producteur ralentisse, au lieu d'accumuler des messages en mémoire jusqu'à la panne.
Capacité d'un consommateur à signaler qu'il est saturé pour que le producteur ralentisse, au lieu d'accumuler des messages en mémoire jusqu'à la panne.
Avec Kafka, la contre-pression est naturelle : le consommateur *tire* (poll) les messages à son rythme et le retard se mesure par le Consumer lag. Avec RabbitMQ, on la règle par le Prefetch.
2Apache Kafka23
Apache KafkaPlateforme distribuée de streaming d'événements : un journal (log) append-only, partitionné et répliqué, où les messages sont conservés selon une durée de rétention et peuvent être relus.
Plateforme distribuée de streaming d'événements : un journal (log) append-only, partitionné et répliqué, où les messages sont conservés selon une durée de rétention et peuvent être relus.
Différence clé avec une file classique : lire un message ne le supprime pas. Chaque groupe de consommateurs garde sa position (offset), on peut donc rejouer l'historique ou ajouter un nouveau consommateur qui relit tout.
TopicCatégorie nommée de messages (ex. virements.effectues) dans laquelle les producteurs écrivent et les consommateurs lisent ; physiquement découpé en partitions.
Catégorie nommée de messages (ex. virements.effectues) dans laquelle les producteurs écrivent et les consommateurs lisent ; physiquement découpé en partitions.
kafka-topics.sh --create --topic virements.effectues --partitions 12 --replication-factor 3 \
--bootstrap-server kafka:9092PartitionSous-ensemble ordonné et immuable d'un topic ; unité de parallélisme et d'ordre. L'ordre des messages n'est garanti qu'à l'intérieur d'une partition.
Sous-ensemble ordonné et immuable d'un topic ; unité de parallélisme et d'ordre. L'ordre des messages n'est garanti qu'à l'intérieur d'une partition.
Plus de partitions = plus de consommateurs possibles en parallèle dans un groupe. On peut en ajouter mais pas en retirer, et l'ajout change la répartition des clés.
Topic virements (3 partitions)
P0: [m0][m3][m6] →
P1: [m1][m4][m7] →
P2: [m2][m5][m8] →OffsetNuméro séquentiel d'un message dans sa partition ; le consommateur enregistre (commit) l'offset du dernier message traité pour reprendre au bon endroit après un redémarrage.
Numéro séquentiel d'un message dans sa partition ; le consommateur enregistre (commit) l'offset du dernier message traité pour reprendre au bon endroit après un redémarrage.
Broker et cluster KafkaUn broker est un serveur Kafka qui stocke des partitions et répond aux clients ; un cluster regroupe plusieurs brokers pour la capacité et la tolérance aux pannes.
Un broker est un serveur Kafka qui stocke des partitions et répond aux clients ; un cluster regroupe plusieurs brokers pour la capacité et la tolérance aux pannes.
Réplication, leader et ISRChaque partition est copiée sur plusieurs brokers (replication factor, souvent 3) : un leader reçoit écritures et lectures, les followers se synchronisent ; l'ISR (In-Sync Replicas) est la liste des répliques à jour.
Chaque partition est copiée sur plusieurs brokers (replication factor, souvent 3) : un leader reçoit écritures et lectures, les followers se synchronisent ; l'ISR (In-Sync Replicas) est la liste des répliques à jour.
Si le broker leader tombe, une réplique de l'ISR devient leader. Avec acks=all et min.insync.replicas=2, un message acquitté existe sur au moins 2 brokers.
KRaft (fin de ZooKeeper)Mode de Kafka où les métadonnées du cluster sont gérées par Kafka lui-même via un protocole de consensus (Raft) ; seul mode disponible depuis Kafka 4.0, ZooKeeper n'est plus nécessaire.
Mode de Kafka où les métadonnées du cluster sont gérées par Kafka lui-même via un protocole de consensus (Raft) ; seul mode disponible depuis Kafka 4.0, ZooKeeper n'est plus nécessaire.
ProducerClient qui publie des messages dans un topic ; il choisit la partition (via la clé), regroupe les messages par lots et attend les acquittements selon acks.
Client qui publie des messages dans un topic ; il choisit la partition (via la clé), regroupe les messages par lots et attend les acquittements selon acks.
@Service
public class VirementPublisher {
private final KafkaTemplate<String, VirementEffectue> kafka;
public VirementPublisher(KafkaTemplate<String, VirementEffectue> kafka) { this.kafka = kafka; }
public void publier(VirementEffectue evt) {
kafka.send("virements.effectues", evt.compte(), evt) // clé = compte → même partition
.whenComplete((res, ex) -> { if (ex != null) log.error("Publication échouée", ex); });
}
}Clé de message et ordreLa clé (ex. numéro de compte) détermine la partition par hachage : tous les messages d'une même clé vont dans la même partition et sont donc traités dans l'ordre.
La clé (ex. numéro de compte) détermine la partition par hachage : tous les messages d'une même clé vont dans la même partition et sont donc traités dans l'ordre.
acks (acquittements du producteur)Niveau de confirmation attendu : acks=0 (aucune, rapide mais pertes possibles), acks=1 (le leader a écrit), acks=all (toutes les répliques de l'ISR ont écrit : le plus sûr, défaut depuis Kafka 3.0).
Niveau de confirmation attendu : acks=0 (aucune, rapide mais pertes possibles), acks=1 (le leader a écrit), acks=all (toutes les répliques de l'ISR ont écrit : le plus sûr, défaut depuis Kafka 3.0).
spring.kafka.producer.acks=all
spring.kafka.producer.properties.min.insync.replicas=2 # côté topic/broker en pratiqueProducteur idempotentAvec enable.idempotence=true (défaut depuis Kafka 3.0), le broker élimine les doublons créés par les relances du producteur grâce à un identifiant de producteur et des numéros de séquence.
Avec enable.idempotence=true (défaut depuis Kafka 3.0), le broker élimine les doublons créés par les relances du producteur grâce à un identifiant de producteur et des numéros de séquence.
Cela évite les doublons dus au réseau entre producteur et broker, mais pas les doublons métier (même événement publié deux fois par l'application) ni ceux côté consommateur.
ConsumerClient qui lit les messages d'un ou plusieurs topics en boucle (poll), les traite, puis enregistre son offset.
Client qui lit les messages d'un ou plusieurs topics en boucle (poll), les traite, puis enregistre son offset.
@KafkaListener(topics = "virements.effectues", groupId = "notification-service")
public void onVirement(VirementEffectue evt) {
notificationService.envoyer(evt);
}Consumer groupEnsemble de consommateurs partageant un group.id : chaque partition est attribuée à un seul membre du groupe, ce qui répartit la charge. Chaque groupe lit le topic indépendamment.
Ensemble de consommateurs partageant un group.id : chaque partition est attribuée à un seul membre du groupe, ce qui répartit la charge. Chaque groupe lit le topic indépendamment.
Topic 4 partitions, groupe "notif" avec 2 instances → 2 partitions chacune
groupe "notif" avec 6 instances → 4 actives, 2 inactives (pas de partition libre)
groupe "antifraude" → reçoit aussi TOUS les messages, indépendammentRebalancingRedistribution des partitions entre les membres d'un groupe quand un consommateur arrive, part ou ne répond plus ; la consommation est brièvement perturbée.
Redistribution des partitions entre les membres d'un groupe quand un consommateur arrive, part ou ne répond plus ; la consommation est brièvement perturbée.
Un traitement trop long (au-delà de max.poll.interval.ms) fait croire au coordinateur que le consommateur est mort → rebalance → le message est retraité par un autre : doublons. Le protocole de rebalance incrémental (Kafka 4) réduit ces interruptions.
Commit d'offset (automatique vs manuel)Enregistrer la position de lecture : automatiquement à intervalles (simple, mais risque de perte ou doublon) ou manuellement après traitement réussi (au moins une fois).
Enregistrer la position de lecture : automatiquement à intervalles (simple, mais risque de perte ou doublon) ou manuellement après traitement réussi (au moins une fois).
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.listener.ack-mode=record # Spring commit après chaque message traité sans exceptionauto-offset-reset (earliest / latest)Où commence un groupe qui n'a encore aucun offset enregistré : earliest = au début du topic (tout relire), latest = seulement les nouveaux messages.
Où commence un groupe qui n'a encore aucun offset enregistré : earliest = au début du topic (tout relire), latest = seulement les nouveaux messages.
spring.kafka.consumer.auto-offset-reset=earliestConsumer lagÉcart entre le dernier message écrit dans une partition et le dernier offset traité par le groupe : c'est le retard du consommateur, la métrique n°1 à surveiller.
Écart entre le dernier message écrit dans une partition et le dernier offset traité par le groupe : c'est le retard du consommateur, la métrique n°1 à surveiller.
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group notification-service
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# virements.effectues 0 15230 15890 660Rétention et log compactionLes messages sont conservés pendant une durée ou une taille donnée (retention.ms), puis supprimés ; la compaction garde seulement le dernier message de chaque clé (état courant).
Les messages sont conservés pendant une durée ou une taille donnée (retention.ms), puis supprimés ; la compaction garde seulement le dernier message de chaque clé (état courant).
kafka-configs.sh --alter --entity-type topics --entity-name clients.etat \
--add-config cleanup.policy=compact --bootstrap-server kafka:9092Transactions Kafka (exactly-once)Un producteur transactionnel écrit dans plusieurs partitions et commit les offsets consommés de façon atomique ; les consommateurs en read_committed ne voient que les transactions validées.
Un producteur transactionnel écrit dans plusieurs partitions et commit les offsets consommés de façon atomique ; les consommateurs en read_committed ne voient que les transactions validées.
spring.kafka.producer.transaction-id-prefix=virement-tx-
spring.kafka.consumer.isolation-level=read_committedSchema Registry et AvroLe Schema Registry stocke les schémas (Avro, Protobuf, JSON Schema) des messages et vérifie leur compatibilité à chaque évolution, pour qu'un producteur ne casse pas les consommateurs.
Le Schema Registry stocke les schémas (Avro, Protobuf, JSON Schema) des messages et vérifie leur compatibilité à chaque évolution, pour qu'un producteur ne casse pas les consommateurs.
{ "type": "record", "name": "VirementEffectue",
"fields": [ {"name": "id", "type": "string"},
{"name": "montant", "type": {"type": "bytes", "logicalType": "decimal", "precision": 15, "scale": 2}},
{"name": "canal", "type": ["null", "string"], "default": null} ] }KafkaTemplate et @KafkaListener (Spring Kafka)KafkaTemplate envoie des messages, @KafkaListener transforme une méthode en consommateur ; Spring Boot configure tout à partir des propriétés spring.kafka.*.
KafkaTemplate envoie des messages, @KafkaListener transforme une méthode en consommateur ; Spring Boot configure tout à partir des propriétés spring.kafka.*.
spring.kafka.bootstrap-servers=kafka:9092
spring.kafka.consumer.group-id=order-service
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.acks=all
spring.kafka.producer.properties.enable.idempotence=true
spring.kafka.listener.concurrency=3 # 3 threads consommateurs par instanceDead Letter Topic et retries non bloquantsUn message qui échoue après N tentatives est déplacé dans un topic « poubelle » (DLT) pour analyse, au lieu de bloquer la partition. @RetryableTopic ajoute des topics de retry avec délais croissants.
Un message qui échoue après N tentatives est déplacé dans un topic « poubelle » (DLT) pour analyse, au lieu de bloquer la partition. @RetryableTopic ajoute des topics de retry avec délais croissants.
@RetryableTopic(attempts = "4", backoff = @Backoff(delay = 1000, multiplier = 2),
exclude = ValidationException.class) // pas de retry pour une donnée invalide
@KafkaListener(topics = "paiements.recus")
public void traiter(PaiementRecu p) { service.crediter(p); }
@DltHandler
public void dlt(PaiementRecu p, @Header(KafkaHeaders.EXCEPTION_MESSAGE) String erreur) {
alerteService.signaler(p, erreur); // supervision + rejeu manuel
}Kafka StreamsBibliothèque Java de traitement de flux au-dessus de Kafka : filtrer, agréger, joindre des topics en continu (ex. total des retraits par carte sur 10 minutes pour la détection de fraude).
Bibliothèque Java de traitement de flux au-dessus de Kafka : filtrer, agréger, joindre des topics en continu (ex. total des retraits par carte sur 10 minutes pour la détection de fraude).
builder.stream("retraits", Consumed.with(Serdes.String(), retraitSerde))
.groupByKey()
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(10)))
.aggregate(() -> BigDecimal.ZERO, (carte, r, total) -> total.add(r.montant()), Materialized.with(...))
.toStream()
.filter((carte, total) -> total.compareTo(SEUIL) > 0)
.to("alertes.fraude");3Garanties de livraison6
Garanties de livraison (vue d'ensemble)Promesse d'un système de messagerie sur le nombre de fois qu'un message est traité : au plus une fois (pertes possibles), au moins une fois (doublons possibles) ou exactement une fois.
Promesse d'un système de messagerie sur le nombre de fois qu'un message est traité : au plus une fois (pertes possibles), au moins une fois (doublons possibles) ou exactement une fois.
Tout dépend du moment où l'on enregistre la progression (offset, ACK) par rapport au traitement : avant → at-most-once, après → at-least-once. En pratique : at-least-once + consommateur idempotent.
At-most-onceChaque message est livré au plus une fois : jamais de doublon, mais des pertes possibles (offset commité avant le traitement). Acceptable pour des métriques non critiques.
Chaque message est livré au plus une fois : jamais de doublon, mais des pertes possibles (offset commité avant le traitement). Acceptable pour des métriques non critiques.
At-least-onceChaque message est livré au moins une fois : jamais de perte, mais des doublons possibles (traitement fait, puis crash avant le commit). Garantie la plus courante ; impose des consommateurs idempotents.
Chaque message est livré au moins une fois : jamais de perte, mais des doublons possibles (traitement fait, puis crash avant le commit). Garantie la plus courante ; impose des consommateurs idempotents.
1. poll → message 42
2. traitement OK (SMS envoyé)
3. 💥 crash avant commit de l'offset
4. redémarrage → message 42 relu → SMS envoyé une 2e fois si le consommateur n'est pas idempotentExactly-onceChaque message produit exactement un effet. Possible de bout en bout dans Kafka (producteur idempotent + transactions) ; avec des systèmes externes, on l'obtient par at-least-once + idempotence.
Chaque message produit exactement un effet. Possible de bout en bout dans Kafka (producteur idempotent + transactions) ; avec des systèmes externes, on l'obtient par at-least-once + idempotence.
Consommateur idempotentConsommateur qui détecte et ignore les messages déjà traités (identifiant unique mémorisé en base dans la même transaction que l'effet), rendant les doublons inoffensifs.
Consommateur qui détecte et ignore les messages déjà traités (identifiant unique mémorisé en base dans la même transaction que l'effet), rendant les doublons inoffensifs.
@KafkaListener(topics = "paiements.recus")
@Transactional
public void traiter(PaiementRecu p) {
try { messagesTraites.save(new MessageTraite(p.id())); } // PK = id du message
catch (DataIntegrityViolationException doublon) { return; } // déjà traité
comptes.crediter(p.compte(), p.montant());
}Message poisonMessage qui fait échouer le consommateur à chaque tentative (format invalide, donnée incohérente) ; sans DLT, il bloque indéfiniment la partition.
Message qui fait échouer le consommateur à chaque tentative (format invalide, donnée incohérente) ; sans DLT, il bloque indéfiniment la partition.
4RabbitMQ12
RabbitMQ et AMQPBroker de messages basé sur le protocole AMQP : les producteurs publient vers un exchange, qui route les messages vers des queues selon des règles ; un message consommé et acquitté est supprimé.
Broker de messages basé sur le protocole AMQP : les producteurs publient vers un exchange, qui route les messages vers des queues selon des règles ; un message consommé et acquitté est supprimé.
Points forts : routage riche, priorités, TTL par message, faible latence pour des files de tâches. Kafka est préféré pour un gros débit, la relecture et la rétention longue.
ExchangePoint d'entrée des messages dans RabbitMQ : il ne stocke rien, il route chaque message vers zéro, une ou plusieurs queues selon son type et les bindings.
Point d'entrée des messages dans RabbitMQ : il ne stocke rien, il route chaque message vers zéro, une ou plusieurs queues selon son type et les bindings.
Queue (RabbitMQ)File qui stocke les messages jusqu'à ce qu'un consommateur les traite et les acquitte ; « durable » pour survivre à un redémarrage, idéalement de type « quorum » pour la réplication.
File qui stocke les messages jusqu'à ce qu'un consommateur les traite et les acquitte ; « durable » pour survivre à un redémarrage, idéalement de type « quorum » pour la réplication.
@Bean Queue notifications() { return QueueBuilder.durable("notifications").quorum().build(); }Binding et routing keyUn binding relie un exchange à une queue avec un motif ; la routing key est l'étiquette posée par le producteur sur le message, comparée à ce motif.
Un binding relie un exchange à une queue avec un motif ; la routing key est l'étiquette posée par le producteur sur le message, comparée à ce motif.
@Bean Binding b(Queue notifications, TopicExchange evenements) {
return BindingBuilder.bind(notifications).to(evenements).with("virement.*");
}
rabbitTemplate.convertAndSend("evenements", "virement.effectue", evt); // routing keyDirect exchangeRoute vers les queues dont la clé de binding est exactement égale à la routing key : du point-à-point ciblé.
Route vers les queues dont la clé de binding est exactement égale à la routing key : du point-à-point ciblé.
Fanout exchangeCopie chaque message dans toutes les queues liées, en ignorant la routing key : diffusion générale (broadcast).
Copie chaque message dans toutes les queues liées, en ignorant la routing key : diffusion générale (broadcast).
Topic exchangeRoute selon un motif sur la routing key découpée par des points : * remplace exactement un mot, # zéro ou plusieurs mots.
Route selon un motif sur la routing key découpée par des points : * remplace exactement un mot, # zéro ou plusieurs mots.
binding "virement.*" reçoit virement.effectue, virement.rejete
binding "*.rejete" reçoit virement.rejete, paiement.rejete
binding "agence.dakar.#" reçoit agence.dakar, agence.dakar.plateau.retraitHeaders exchangeRoute selon les en-têtes du message (ex. canal=USSD et pays=SN) au lieu de la routing key ; utile pour des règles multi-critères.
Route selon les en-têtes du message (ex. canal=USSD et pays=SN) au lieu de la routing key ; utile pour des règles multi-critères.
ACK, NACK et requeueLe consommateur confirme le traitement (ACK) pour que RabbitMQ supprime le message ; NACK signale un échec, avec ou sans remise en file (requeue).
Le consommateur confirme le traitement (ACK) pour que RabbitMQ supprime le message ; NACK signale un échec, avec ou sans remise en file (requeue).
Prefetch (QoS)Nombre maximum de messages non acquittés qu'un consommateur peut recevoir à la fois ; règle l'équilibrage de charge et la contre-pression.
Nombre maximum de messages non acquittés qu'un consommateur peut recevoir à la fois ; règle l'équilibrage de charge et la contre-pression.
spring.rabbitmq.listener.simple.prefetch=10Dead Letter Exchange (DLX)Exchange où RabbitMQ renvoie les messages rejetés, expirés (TTL) ou en surnombre ; on y branche une queue d'analyse et de rejeu.
Exchange où RabbitMQ renvoie les messages rejetés, expirés (TTL) ou en surnombre ; on y branche une queue d'analyse et de rejeu.
QueueBuilder.durable("paiements").deadLetterExchange("paiements.dlx").build();Kafka vs RabbitMQKafka : journal distribué, rétention, relecture, très gros débit, ordre par partition. RabbitMQ : routage flexible, files de tâches, acquittement par message, suppression après lecture.
Kafka : journal distribué, rétention, relecture, très gros débit, ordre par partition. RabbitMQ : routage flexible, files de tâches, acquittement par message, suppression après lecture.
| Critère | Kafka | RabbitMQ |
|---|---|---|
| Modèle | Journal (log) conservé | File, message supprimé après ACK |
| Relecture | ✅ (offsets) | ❌ |
| Débit | Très élevé | Élevé |
| Routage | Par topic/partition | Riche (direct, topic, headers) |
| Cas typique | Event streaming, CDC, audit | Tâches asynchrones, RPC, notifications |
5Services managés AWS3
Amazon SQS (Standard vs FIFO)File de messages entièrement gérée par AWS. Standard : débit quasi illimité, ordre non garanti, livraison au moins une fois. FIFO : ordre garanti et déduplication, débit limité.
File de messages entièrement gérée par AWS. Standard : débit quasi illimité, ordre non garanti, livraison au moins une fois. FIFO : ordre garanti et déduplication, débit limité.
@SqsListener("paiements-a-traiter") // Spring Cloud AWS
public void traiter(PaiementMessage m) { … }Visibility timeout (SQS)Délai pendant lequel un message lu est caché aux autres consommateurs ; s'il n'est pas supprimé avant la fin (traitement trop long ou échec), il redevient visible et sera retraité.
Délai pendant lequel un message lu est caché aux autres consommateurs ; s'il n'est pas supprimé avant la fin (traitement trop long ou échec), il redevient visible et sera retraité.
Amazon SNS (fan-out)Service pub/sub d'AWS : un message publié sur un topic SNS est poussé à tous les abonnés (files SQS, Lambda, HTTP, SMS). SNS + plusieurs SQS = diffusion fiable vers plusieurs services.
Service pub/sub d'AWS : un message publié sur un topic SNS est poussé à tous les abonnés (files SQS, Lambda, HTTP, SMS). SNS + plusieurs SQS = diffusion fiable vers plusieurs services.
📋 Synthèse express — Messaging
📨 Kafka — Concepts clés
Un topic est divisé en partitions. Chaque partition = 1 seul consumer par groupe. Parallélisme = nombre de partitions. Ordre garanti par partition.
- At-most-once — peut perdre des messages
- At-least-once — peut dupliquer (défaut)
- Exactly-once — idempotent producer + transactions
🐇 RabbitMQ — Types d'Exchange
| Exchange | Routage | Cas d'usage |
|---|---|---|
| Direct | Routing key exacte | Point-to-point |
| Fanout | Broadcast vers toutes les queues liées | Notifications globales |
| Topic | Pattern *.orange.* ou lazy.# | Pub/Sub filtré |
| Headers | Headers du message | Routage complexe |
1Identité & contrôle d'accès6
Authentification vs autorisationAuthentification : prouver qui on est (mot de passe, OTP, token). Autorisation : décider ce qu'on a le droit de faire une fois identifié (rôles, propriété de la ressource).
Authentification : prouver qui on est (mot de passe, OTP, token). Autorisation : décider ce qu'on a le droit de faire une fois identifié (rôles, propriété de la ressource).
En HTTP : un échec d'authentification donne 401, un échec d'autorisation donne 403. Les deux doivent être vérifiés côté serveur, jamais seulement dans Angular.
.authorizeHttpRequests(a -> a
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // autorisation par rôle
.anyRequest().authenticated()) // authentification requisePrincipe du moindre privilègeChaque utilisateur, service ou compte technique ne reçoit que les droits strictement nécessaires à sa tâche, et pas plus longtemps que nécessaire.
Chaque utilisateur, service ou compte technique ne reçoit que les droits strictement nécessaires à sa tâche, et pas plus longtemps que nécessaire.
Exemples : utilisateur de base de l'application sans droit DROP ; token de service limité au scope paiements:lecture ; conteneur exécuté en non-root ; accès production temporaire et tracé.
RBAC (contrôle d'accès par rôle)Les droits sont attachés à des rôles (CLIENT, CONSEILLER, ADMIN) et les utilisateurs reçoivent des rôles. Simple à administrer, mais insuffisant pour « ce client ne voit que SES comptes ».
Les droits sont attachés à des rôles (CLIENT, CONSEILLER, ADMIN) et les utilisateurs reçoivent des rôles. Simple à administrer, mais insuffisant pour « ce client ne voit que SES comptes ».
@PreAuthorize("hasRole('CONSEILLER')")
public List<ClientDto> portefeuille() { … }Contrôle d'accès par ressource (ABAC) et IDORVérifier non seulement le rôle mais aussi que la ressource demandée appartient à l'utilisateur. Sans ce contrôle : IDOR (Insecure Direct Object Reference), première faille du classement OWASP.
Vérifier non seulement le rôle mais aussi que la ressource demandée appartient à l'utilisateur. Sans ce contrôle : IDOR (Insecure Direct Object Reference), première faille du classement OWASP.
// ❌ IDOR : un client change l'id dans l'URL et lit le compte d'un autre
@GetMapping("/comptes/{id}") CompteDto get(@PathVariable Long id) { return service.get(id); }
// ✅ la requête est filtrée par le propriétaire authentifié
@GetMapping("/comptes/{id}")
CompteDto get(@PathVariable Long id, @AuthenticationPrincipal Jwt jwt) {
return service.getPourClient(id, jwt.getSubject()); // 404 si le compte n'est pas à lui
}MFA et OTPAuthentification multifacteur : combiner quelque chose que l'on sait (mot de passe), possède (téléphone, OTP, clé FIDO2) ou est (biométrie). Exigée pour les opérations sensibles (virement, ajout de bénéficiaire).
Authentification multifacteur : combiner quelque chose que l'on sait (mot de passe), possède (téléphone, OTP, clé FIDO2) ou est (biométrie). Exigée pour les opérations sensibles (virement, ajout de bénéficiaire).
Session (cookie) vs tokenSession : le serveur garde l'état et le navigateur envoie un identifiant de session en cookie (CSRF à gérer). Token : le client porte un jeton signé (JWT) à chaque requête ; le serveur reste sans état.
Session : le serveur garde l'état et le navigateur envoie un identifiant de session en cookie (CSRF à gérer). Token : le client porte un jeton signé (JWT) à chaque requête ; le serveur reste sans état.
| Session cookie | Token (JWT) | |
|---|---|---|
| État serveur | Oui (ou Redis) | Non |
| Envoi | Automatique (cookie) | En-tête Authorization |
| Risque principal | CSRF | Vol du token (XSS) |
| Révocation | Immédiate | Difficile avant expiration |
2OAuth2 & OpenID Connect13
OAuth2Protocole de délégation d'autorisation : une application obtient un jeton d'accès limité pour appeler une API au nom d'un utilisateur, sans jamais connaître son mot de passe.
Protocole de délégation d'autorisation : une application obtient un jeton d'accès limité pour appeler une API au nom d'un utilisateur, sans jamais connaître son mot de passe.
OAuth2 ne dit pas *qui* est l'utilisateur (c'est le rôle d'OpenID Connect) ; il dit *ce que l'application peut faire*. OAuth 2.1 consolide les bonnes pratiques : PKCE obligatoire, flux implicite et « password » supprimés.
Rôles OAuth2Resource Owner (l'utilisateur), Client (l'application Angular ou mobile), Authorization Server (Keycloak, qui émet les jetons), Resource Server (l'API Spring qui vérifie les jetons).
Resource Owner (l'utilisateur), Client (l'application Angular ou mobile), Authorization Server (Keycloak, qui émet les jetons), Resource Server (l'API Spring qui vérifie les jetons).
Utilisateur ─(se connecte)─► Keycloak (Authorization Server) ─(access token)─► Angular (Client)
Angular ─(Bearer token)─► API Spring (Resource Server) ─(vérifie signature via JWKS)OpenID Connect (OIDC)Couche d'identité au-dessus d'OAuth2 : en plus de l'access token, le serveur renvoie un ID token (JWT) qui décrit l'utilisateur authentifié, et expose un endpoint userinfo.
Couche d'identité au-dessus d'OAuth2 : en plus de l'access token, le serveur renvoie un ID token (JWT) qui décrit l'utilisateur authentifié, et expose un endpoint userinfo.
Le scope openid déclenche OIDC. L'ID token est destiné au client (afficher le nom, savoir qui est connecté) ; l'access token est destiné à l'API.
Authorization Code FlowFlux OAuth2 recommandé : le navigateur est redirigé vers le serveur d'autorisation, l'utilisateur s'y authentifie, puis le client reçoit un code à usage unique qu'il échange contre les jetons.
Flux OAuth2 recommandé : le navigateur est redirigé vers le serveur d'autorisation, l'utilisateur s'y authentifie, puis le client reçoit un code à usage unique qu'il échange contre les jetons.
1. GET /authorize?response_type=code&client_id=app&redirect_uri=…&scope=openid&state=xyz&code_challenge=…
2. Utilisateur saisit ses identifiants CHEZ Keycloak (jamais dans l'application)
3. Redirection vers redirect_uri?code=abc&state=xyz
4. POST /token code=abc + code_verifier → access_token, refresh_token, id_tokenstate protège contre la falsification de la redirection (CSRF du login) : il doit revenir identique.PKCE (Proof Key for Code Exchange)Extension du flux Authorization Code pour les clients publics (SPA, mobile) qui ne peuvent pas garder de secret : le client prouve qu'il est bien celui qui a demandé le code.
Extension du flux Authorization Code pour les clients publics (SPA, mobile) qui ne peuvent pas garder de secret : le client prouve qu'il est bien celui qui a demandé le code.
Sans PKCE, un attaquant qui intercepte le code (application malveillante, logs) pourrait l'échanger contre un token. Avec PKCE, il lui manque le code_verifier, resté en mémoire chez le client légitime. Obligatoire en OAuth 2.1, pour tous les clients.
code_verifier et code_challengeLe client génère un secret aléatoire (code_verifier), envoie son empreinte SHA-256 (code_challenge) au début du flux, puis révèle le verifier lors de l'échange ; le serveur vérifie que les deux correspondent.
Le client génère un secret aléatoire (code_verifier), envoie son empreinte SHA-256 (code_challenge) au début du flux, puis révèle le verifier lors de l'échange ; le serveur vérifie que les deux correspondent.
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); // 43 caractères
const challenge = base64url(new Uint8Array(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
// /authorize?...&code_challenge=<challenge>&code_challenge_method=S256
// /token ...&code_verifier=<verifier>Client Credentials FlowFlux pour la communication service à service, sans utilisateur : le service s'authentifie avec son client_id et son client_secret et obtient un token pour ses propres droits.
Flux pour la communication service à service, sans utilisateur : le service s'authentifie avec son client_id et son client_secret et obtient un token pour ses propres droits.
curl -X POST https://auth.banque.sn/realms/banque/protocol/openid-connect/token \
-d grant_type=client_credentials -d client_id=service-releves -d client_secret=$SECRETspring.security.oauth2.client.registration.keycloak:
client-id: service-releves
client-secret: ${KEYCLOAK_SECRET}
authorization-grant-type: client_credentialsScopePermission demandée par un client et accordée dans le token (ex. comptes:lecture, virements:ecriture) : elle limite ce que le jeton permet, indépendamment des rôles de l'utilisateur.
Permission demandée par un client et accordée dans le token (ex. comptes:lecture, virements:ecriture) : elle limite ce que le jeton permet, indépendamment des rôles de l'utilisateur.
.requestMatchers(HttpMethod.POST, "/api/v1/virements").hasAuthority("SCOPE_virements:ecriture")Access token, refresh token et ID tokenAccess token : courte durée (5-15 min), envoyé à l'API. Refresh token : plus long, sert uniquement à obtenir un nouvel access token auprès du serveur d'autorisation. ID token : décrit l'utilisateur, pour le client.
Access token : courte durée (5-15 min), envoyé à l'API. Refresh token : plus long, sert uniquement à obtenir un nouvel access token auprès du serveur d'autorisation. ID token : décrit l'utilisateur, pour le client.
Rotation des refresh tokensÀ chaque utilisation, le refresh token est remplacé par un nouveau et l'ancien invalidé ; si un ancien est réutilisé (vol probable), toute la session est révoquée.
À chaque utilisation, le refresh token est remplacé par un nouveau et l'ancien invalidé ; si un ancien est réutilisé (vol probable), toute la session est révoquée.
Flux implicite (déprécié) et OAuth 2.1L'ancien flux implicite renvoyait l'access token directement dans l'URL du navigateur (fuite par l'historique, les logs, le Referer). Il est abandonné au profit d'Authorization Code + PKCE.
L'ancien flux implicite renvoyait l'access token directement dans l'URL du navigateur (fuite par l'historique, les logs, le Referer). Il est abandonné au profit d'Authorization Code + PKCE.
Pattern BFF (Backend For Frontend)Un petit serveur côté front gère le flux OAuth2 et garde les tokens côté serveur ; le navigateur ne reçoit qu'un cookie de session HttpOnly. Pratique recommandée pour les SPA à forts enjeux de sécurité.
Un petit serveur côté front gère le flux OAuth2 et garde les tokens côté serveur ; le navigateur ne reçoit qu'un cookie de session HttpOnly. Pratique recommandée pour les SPA à forts enjeux de sécurité.
Avantage : aucun token accessible au JavaScript, donc un XSS ne peut pas le voler. Contrepartie : il faut gérer CSRF (cookie) et un composant de plus (Spring Cloud Gateway peut jouer ce rôle avec TokenRelay).
Stockage du token dans une SPAlocalStorage est lisible par tout script de la page (XSS = vol du token) ; la mémoire JavaScript est plus sûre mais perdue au rechargement ; le cookie HttpOnly (via BFF) est hors de portée du JavaScript.
localStorage est lisible par tout script de la page (XSS = vol du token) ; la mémoire JavaScript est plus sûre mais perdue au rechargement ; le cookie HttpOnly (via BFF) est hors de portée du JavaScript.
| Stockage | Résiste au XSS | Persistant | Remarque |
|---|---|---|---|
| localStorage | ❌ | ✅ | À éviter pour des tokens sensibles |
| Mémoire (variable) | ⚠️ partiel | ❌ | + refresh silencieux |
| Cookie HttpOnly (BFF) | ✅ | ✅ | Gérer CSRF et SameSite |
3JWT8
JWT (JSON Web Token)Jeton compact et signé, formé de trois parties encodées en Base64url séparées par des points : header.payload.signature. Le serveur le vérifie sans consulter de base.
Jeton compact et signé, formé de trois parties encodées en Base64url séparées par des points : header.payload.signature. Le serveur le vérifie sans consulter de base.
eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0.eyJzdWIiOiI0MiIsImV4cCI6MTc2MDQ0Mjc4MH0.kQ8…
└────────── header ───────────┘ └──────────── payload ───────────┘ └ signature ┘Structure d'un JWT : header, payload, signatureHeader : algorithme et identifiant de clé (alg, kid). Payload : les claims (qui, pour qui, jusqu'à quand, quels droits). Signature : garantit que header et payload n'ont pas été modifiés.
Header : algorithme et identifiant de clé (alg, kid). Payload : les claims (qui, pour qui, jusqu'à quand, quels droits). Signature : garantit que header et payload n'ont pas été modifiés.
// header
{ "alg": "RS256", "typ": "JWT", "kid": "k1" }
// payload
{ "iss": "https://auth.banque.sn/realms/banque", "sub": "42", "aud": "api-banque",
"exp": 1760442780, "iat": 1760441880, "jti": "a1b2c3", "scope": "comptes:lecture",
"realm_access": { "roles": ["CLIENT"] } }Claims standard (iss, sub, aud, exp, iat, jti)iss : émetteur ; sub : identifiant de l'utilisateur ; aud : destinataire prévu ; exp : expiration ; iat : date d'émission ; jti : identifiant unique du jeton (utile pour la révocation et l'anti-rejeu).
iss : émetteur ; sub : identifiant de l'utilisateur ; aud : destinataire prévu ; exp : expiration ; iat : date d'émission ; jti : identifiant unique du jeton (utile pour la révocation et l'anti-rejeu).
HS256 vs RS256HS256 : signature avec un secret partagé (qui peut vérifier peut aussi signer). RS256/ES256 : signature avec une clé privée, vérification avec la clé publique (les APIs ne peuvent pas forger de token).
HS256 : signature avec un secret partagé (qui peut vérifier peut aussi signer). RS256/ES256 : signature avec une clé privée, vérification avec la clé publique (les APIs ne peuvent pas forger de token).
En microservices, RS256 évite de distribuer un secret de signature à chaque service : l'API ne détient que la clé publique, publiée par Keycloak.
JWKS (JSON Web Key Set)Endpoint public qui publie les clés publiques du serveur d'autorisation ; l'API choisit la bonne clé grâce au kid du header, ce qui permet la rotation des clés sans redéploiement.
Endpoint public qui publie les clés publiques du serveur d'autorisation ; l'API choisit la bonne clé grâce au kid du header, ce qui permet la rotation des clés sans redéploiement.
GET https://auth.banque.sn/realms/banque/protocol/openid-connect/certs
{ "keys": [ { "kid": "k1", "kty": "RSA", "alg": "RS256", "n": "…", "e": "AQAB" } ] }Valider un JWT côté APIVérifier : la signature (clé du JWKS), l'algorithme attendu (refuser none), l'expiration (exp), l'émetteur (iss), l'audience (aud), puis extraire rôles et scopes.
Vérifier : la signature (clé du JWKS), l'algorithme attendu (refuser none), l'expiration (exp), l'émetteur (iss), l'audience (aud), puis extraire rôles et scopes.
@Bean JwtDecoder jwtDecoder() {
NimbusJwtDecoder d = JwtDecoders.fromIssuerLocation("https://auth.banque.sn/realms/banque");
d.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer("https://auth.banque.sn/realms/banque"),
new JwtClaimValidator<List<String>>("aud", aud -> aud != null && aud.contains("api-banque"))));
return d;
}Révocation d'un JWTUn JWT reste valide jusqu'à son expiration même si l'utilisateur se déconnecte. Parades : durée de vie courte + refresh token révocable, liste noire des jti (Redis), ou introspection auprès du serveur pour les opérations sensibles.
Un JWT reste valide jusqu'à son expiration même si l'utilisateur se déconnecte. Parades : durée de vie courte + refresh token révocable, liste noire des jti (Redis), ou introspection auprès du serveur pour les opérations sensibles.
Base64url n'est pas du chiffrementBase64url est un simple encodage réversible (texte transportable dans une URL) ; décoder un payload de JWT ne demande aucune clé.
Base64url est un simple encodage réversible (texte transportable dans une URL) ; décoder un payload de JWT ne demande aucune clé.
echo 'eyJzdWIiOiI0MiJ9' | base64 -d # {"sub":"42"}4Keycloak7
KeycloakServeur open source de gestion des identités et des accès (IAM) : authentification, SSO, OAuth2/OIDC, MFA, fédération LDAP/Active Directory, gestion des rôles et des clients.
Serveur open source de gestion des identités et des accès (IAM) : authentification, SSO, OAuth2/OIDC, MFA, fédération LDAP/Active Directory, gestion des rôles et des clients.
docker run -p 8081:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.0 start-devRealmEspace d'identité isolé dans Keycloak (utilisateurs, clients, rôles, politiques propres). Un realm par organisation ou environnement ; le realm master sert uniquement à administrer Keycloak.
Espace d'identité isolé dans Keycloak (utilisateurs, clients, rôles, politiques propres). Un realm par organisation ou environnement ; le realm master sert uniquement à administrer Keycloak.
Client Keycloak : public vs confidentielUn client est une application enregistrée dans Keycloak. Public : ne peut pas garder de secret (SPA, mobile) → PKCE. Confidentiel : application serveur qui possède un client_secret.
Un client est une application enregistrée dans Keycloak. Public : ne peut pas garder de secret (SPA, mobile) → PKCE. Confidentiel : application serveur qui possède un client_secret.
Rôles realm et rôles clientRôles realm : globaux au realm (realm_access.roles dans le token). Rôles client : propres à une application (resource_access.<client>.roles). Spring doit être configuré pour les lire.
Rôles realm : globaux au realm (realm_access.roles dans le token). Rôles client : propres à une application (resource_access.<client>.roles). Spring doit être configuré pour les lire.
@Bean JwtAuthenticationConverter jwtAuthConverter() {
var conv = new JwtAuthenticationConverter();
conv.setJwtGrantedAuthoritiesConverter(jwt -> {
Map<String, Object> realm = jwt.getClaim("realm_access");
List<String> roles = realm == null ? List.of() : (List<String>) realm.get("roles");
return roles.stream().map(r -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + r)).toList();
});
return conv;
}Groupes KeycloakEnsemble d'utilisateurs qui hérite de rôles et d'attributs communs (ex. groupe « Agence Plateau ») : on gère les droits par groupe plutôt qu'utilisateur par utilisateur.
Ensemble d'utilisateurs qui hérite de rôles et d'attributs communs (ex. groupe « Agence Plateau ») : on gère les droits par groupe plutôt qu'utilisateur par utilisateur.
Protocol mapperRègle Keycloak qui ajoute ou transforme un claim dans les tokens (rôles, groupes, attribut agence_id, audience), configurée par client ou via un client scope.
Règle Keycloak qui ajoute ou transforme un claim dans les tokens (rôles, groupes, attribut agence_id, audience), configurée par client ou via un client scope.
Fédération d'identités (LDAP, Identity Brokering)Keycloak peut s'appuyer sur un annuaire existant (LDAP/AD) pour les employés, ou déléguer la connexion à un autre fournisseur (Google, autre IdP OIDC/SAML).
Keycloak peut s'appuyer sur un annuaire existant (LDAP/AD) pour les employés, ou déléguer la connexion à un autre fournisseur (Google, autre IdP OIDC/SAML).
5Cryptographie & secrets9
Hachage vs chiffrementHachage : transformation à sens unique (on ne retrouve pas l'original), pour vérifier un mot de passe ou l'intégrité. Chiffrement : réversible avec une clé, pour protéger une donnée qu'on devra relire.
Hachage : transformation à sens unique (on ne retrouve pas l'original), pour vérifier un mot de passe ou l'intégrité. Chiffrement : réversible avec une clé, pour protéger une donnée qu'on devra relire.
Mot de passe → hachage lent et salé (BCrypt, Argon2). Numéro de pièce d'identité stocké → chiffrement (AES-GCM) avec une clé gérée dans un KMS/Vault.
Sel (salt)Valeur aléatoire unique ajoutée à chaque mot de passe avant hachage : deux mots de passe identiques donnent des empreintes différentes et les tables précalculées (rainbow tables) deviennent inutiles.
Valeur aléatoire unique ajoutée à chaque mot de passe avant hachage : deux mots de passe identiques donnent des empreintes différentes et les tables précalculées (rainbow tables) deviennent inutiles.
Argon2Algorithme de hachage de mots de passe moderne (vainqueur de la Password Hashing Competition), coûteux en mémoire et en temps, ce qui freine les attaques par GPU ; recommandé par l'OWASP avec BCrypt.
Algorithme de hachage de mots de passe moderne (vainqueur de la Password Hashing Competition), coûteux en mémoire et en temps, ce qui freine les attaques par GPU ; recommandé par l'OWASP avec BCrypt.
PasswordEncoder enc = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();HTTPS / TLSTLS chiffre et authentifie la connexion entre client et serveur (HTTPS = HTTP sur TLS) : confidentialité, intégrité et preuve d'identité du serveur via son certificat.
TLS chiffre et authentifie la connexion entre client et serveur (HTTPS = HTTP sur TLS) : confidentialité, intégrité et preuve d'identité du serveur via son certificat.
mTLS (TLS mutuel)Les deux parties présentent un certificat : le serveur authentifie aussi le client. Utilisé entre microservices (souvent via un service mesh) et avec les partenaires bancaires.
Les deux parties présentent un certificat : le serveur authentifie aussi le client. Utilisé entre microservices (souvent via un service mesh) et avec les partenaires bancaires.
HSTS et en-têtes de sécuritéEn-têtes HTTP qui durcissent le navigateur : HSTS (toujours HTTPS), Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options / frame-ancestors (anti-clickjacking), Referrer-Policy.
En-têtes HTTP qui durcissent le navigateur : HSTS (toujours HTTPS), Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options / frame-ancestors (anti-clickjacking), Referrer-Policy.
http.headers(h -> h
.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000).includeSubDomains(true))
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; frame-ancestors 'none'")));Gestion des secrets (Vault)Les secrets (mots de passe de base, clés API, clés de signature) vivent dans un coffre (HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets chiffrés), jamais dans le code ni dans Git.
Les secrets (mots de passe de base, clés API, clés de signature) vivent dans un coffre (HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets chiffrés), jamais dans le code ni dans Git.
Vault apporte en plus des secrets dynamiques (identifiants de base créés à la demande, expirant automatiquement), l'audit des accès et la rotation.
spring:
config:
import: "vault://" # Spring Cloud Vault : les secrets deviennent des propriétés
cloud.vault:
uri: https://vault.interne:8200
authentication: KUBERNETESChiffrement au repos et en transitEn transit : les données circulant sur le réseau sont chiffrées (TLS). Au repos : les données stockées (disques, sauvegardes, colonnes sensibles) le sont aussi, avec des clés gérées hors de la base.
En transit : les données circulant sur le réseau sont chiffrées (TLS). Au repos : les données stockées (disques, sauvegardes, colonnes sensibles) le sont aussi, avec des clés gérées hors de la base.
Rotation des secretsRemplacer régulièrement (et immédiatement en cas de fuite) mots de passe, clés et certificats, de préférence automatiquement, en supportant temporairement l'ancienne et la nouvelle valeur.
Remplacer régulièrement (et immédiatement en cas de fuite) mots de passe, clés et certificats, de préférence automatiquement, en supportant temporairement l'ancienne et la nouvelle valeur.
6Vulnérabilités & défenses12
OWASP Top 10Classement de référence des risques de sécurité web les plus critiques, publié par l'OWASP : contrôle d'accès défaillant, défaillances cryptographiques, injection, conception non sécurisée, mauvaise configuration, composants vulnérables…
Classement de référence des risques de sécurité web les plus critiques, publié par l'OWASP : contrôle d'accès défaillant, défaillances cryptographiques, injection, conception non sécurisée, mauvaise configuration, composants vulnérables…
Injection SQLInsérer du code SQL dans une entrée utilisateur pour modifier la requête exécutée (lire, modifier ou supprimer des données). Prévention : requêtes préparées / paramètres liés, jamais de concaténation.
Insérer du code SQL dans une entrée utilisateur pour modifier la requête exécutée (lire, modifier ou supprimer des données). Prévention : requêtes préparées / paramètres liés, jamais de concaténation.
// ❌ ?nom=' OR '1'='1 → renvoie tous les clients
em.createQuery("SELECT c FROM Client c WHERE c.nom = '" + nom + "'");
// ✅
em.createQuery("SELECT c FROM Client c WHERE c.nom = :nom").setParameter("nom", nom);ORDER BY + param) ne peuvent pas être paramétrés : les valider par liste blanche.XSS (Cross-Site Scripting)Faire exécuter du JavaScript malveillant dans le navigateur d'une victime via une donnée affichée sans encodage. Stocké (en base), réfléchi (dans l'URL) ou DOM-based (manipulation côté client).
Faire exécuter du JavaScript malveillant dans le navigateur d'une victime via une donnée affichée sans encodage. Stocké (en base), réfléchi (dans l'URL) ou DOM-based (manipulation côté client).
Conséquences : vol de token en localStorage, actions à la place de l'utilisateur. Angular échappe automatiquement les interpolations ; le danger vient de innerHTML avec bypassSecurityTrustHtml, de bibliothèques tierces et de la manipulation directe du DOM.
<p>{{ commentaire }}</p> <!-- ✅ échappé : <script> s'affiche en texte -->
<div [innerHTML]="commentaire"></div> <!-- ⚠️ assaini par Angular (scripts retirés) -->this.sanitizer.bypassSecurityTrustHtml(saisie); // ❌ désactive la protection : à proscrire sur une saisieEncodage des sorties et assainissement (sanitization)Encoder une donnée selon le contexte où elle est affichée (HTML, attribut, URL, JavaScript) pour qu'elle reste du texte ; assainir = retirer les parties dangereuses d'un HTML qu'on doit afficher.
Encoder une donnée selon le contexte où elle est affichée (HTML, attribut, URL, JavaScript) pour qu'elle reste du texte ; assainir = retirer les parties dangereuses d'un HTML qu'on doit afficher.
Content Security Policy (CSP)En-tête HTTP qui indique au navigateur d'où il a le droit de charger scripts, styles, images et vers où se connecter ; un script injecté depuis une source non autorisée est bloqué. Deuxième ligne de défense contre le XSS.
En-tête HTTP qui indique au navigateur d'où il a le droit de charger scripts, styles, images et vers où se connecter ; un script injecté depuis une source non autorisée est bloqué. Deuxième ligne de défense contre le XSS.
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' https://api.banque.sn;
img-src 'self' data:; frame-ancestors 'none'; object-src 'none'SSRF (Server-Side Request Forgery)Amener le serveur à appeler une URL choisie par l'attaquant (ex. un champ « URL de webhook ») pour atteindre des services internes ou les métadonnées du cloud (169.254.169.254).
Amener le serveur à appeler une URL choisie par l'attaquant (ex. un champ « URL de webhook ») pour atteindre des services internes ou les métadonnées du cloud (169.254.169.254).
InetAddressFilter pour aider.Brute force, rate limiting et CAPTCHALimiter les tentatives de connexion ou d'OTP par compte et par IP, verrouiller temporairement, ajouter un CAPTCHA après plusieurs échecs, pour bloquer les attaques par force brute et le credential stuffing.
Limiter les tentatives de connexion ou d'OTP par compte et par IP, verrouiller temporairement, ajouter un CAPTCHA après plusieurs échecs, pour bloquer les attaques par force brute et le credential stuffing.
Validation des entrées (liste blanche)Toute donnée entrante est suspecte : vérifier type, format, longueur et plage côté serveur, de préférence par liste blanche (ce qui est autorisé) plutôt que liste noire.
Toute donnée entrante est suspecte : vérifier type, format, longueur et plage côté serveur, de préférence par liste blanche (ce qui est autorisé) plutôt que liste noire.
public record BeneficiaireRequest(
@NotBlank @Size(max = 80) @Pattern(regexp = "[\\p{L} .'-]+") String nom,
@NotBlank @Pattern(regexp = "SN\\d{2}[A-Z0-9]{20,30}") String iban) {}Dépendances vulnérables (SCA)Analyse des bibliothèques tierces (Software Composition Analysis) pour détecter celles qui ont des vulnérabilités connues (CVE) ; intégrée au pipeline CI pour bloquer une dépendance critique.
Analyse des bibliothèques tierces (Software Composition Analysis) pour détecter celles qui ont des vulnérabilités connues (CVE) ; intégrée au pipeline CI pour bloquer une dépendance critique.
./mvnw org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7
npm audit --audit-level=highDétection de secrets dans GitOutils qui analysent les commits (pre-commit, CI) pour détecter mots de passe, clés API et tokens avant qu'ils n'atteignent le dépôt.
Outils qui analysent les commits (pre-commit, CI) pour détecter mots de passe, clés API et tokens avant qu'ils n'atteignent le dépôt.
Journal d'audit de sécuritéTrace horodatée et non modifiable des actions sensibles (connexion, virement, changement de bénéficiaire, accès admin) : qui, quoi, quand, d'où, résultat. Indispensable en banque pour la conformité et les enquêtes.
Trace horodatée et non modifiable des actions sensibles (connexion, virement, changement de bénéficiaire, accès admin) : qui, quoi, quand, d'où, résultat. Indispensable en banque pour la conformité et les enquêtes.
auditService.tracer(Audit.of("VIREMENT_INITIE", jwt.getSubject(), ip, Map.of("montant", montant, "dest", ibanMasque)));SN01 **** **** 1234).Défense en profondeur et secure by designConcevoir la sécurité dès le départ (modélisation des menaces, choix par défaut sûrs) et empiler plusieurs couches indépendantes (WAF, TLS, authentification, autorisation, validation, contraintes en base, journalisation) pour qu'une seule faille ne suffise pas.
Concevoir la sécurité dès le départ (modélisation des menaces, choix par défaut sûrs) et empiler plusieurs couches indépendantes (WAF, TLS, authentification, autorisation, validation, contraintes en base, journalisation) pour qu'une seule faille ne suffise pas.
📋 Synthèse express — Sécurité
🔐 OAuth2 — Flux PKCE (SPA/Mobile)
🎫 JWT — Structure & Bonnes pratiques
| Partie | Contenu |
|---|---|
| Header | {"alg":"RS256","typ":"JWT"} |
| Payload | Claims: iss, sub, exp, iat, roles, jti (unique ID) |
| Signature | HS256 (secret symétrique) ou RS256 (clé privée/publique) |
🔑 Keycloak — Concepts
Un realm = espace d'identité isolé. Multi-tenant = plusieurs realms. Realm master = admin uniquement.
Public client (SPA/mobile, PKCE) vs Confidential client (server-side, client_secret). Chaque application = 1 client Keycloak.
Ajoutent des claims au JWT (roles, groupes, attributs custom). Protocol mappers configurés par client ou realm.
🛡️ Checklist Sécurité — 12 points
| # | Point de contrôle | Notes |
|---|---|---|
| 1 | Authentification & autorisation côté serveur | Jamais uniquement côté client |
| 2 | Validation de toutes les entrées | @Valid, regex, longueur max |
| 3 | Encodage des sorties | Prévenir XSS en HTML/JS |
| 4 | Protection injection SQL | Requêtes préparées TOUJOURS |
| 5 | CORS strict et justifié | Pas de wildcard * en prod |
| 6 | CSRF selon contexte | Si cookies de session |
| 7 | Secrets hors du code | Vault, variables d'env sécurisées |
| 8 | HTTPS en production | TLS 1.2+ minimum |
| 9 | Mots de passe hachés BCrypt/Argon2 | Jamais MD5, SHA1, SHA256 |
| 10 | Contrôle d'accès par rôle/ressource | Principe moindre privilège |
| 11 | Limitation des tentatives | Rate limiting, CAPTCHA |
| 12 | Dépendances à jour | OWASP Dependency Check, Snyk |
⚔️ Vulnérabilités courantes
Injection de code SQL via les inputs. Exemple : '; DROP TABLE users; --. Prévention : requêtes préparées (setParameter), jamais de concaténation directe d'input utilisateur dans une requête.
Injection de scripts dans la page web. Stocké (en DB), Réfléchi (dans l'URL), DOM-based. Prévention : encoder les sorties HTML, Content Security Policy (CSP), Angular sanitize par défaut.
Ne jamais committer de secrets en Git (API keys, passwords, JWT secrets). Utiliser : HashiCorp Vault, AWS Secrets Manager, variables d'env du CI/CD. Scanner avec git-secrets ou trufflehog.
1Docker13
Conteneur vs machine virtuelleUn conteneur isole un processus et ses dépendances en partageant le noyau de l'OS hôte (léger, démarre en secondes) ; une machine virtuelle embarque un OS complet sur un hyperviseur (plus lourde, isolation plus forte).
Un conteneur isole un processus et ses dépendances en partageant le noyau de l'OS hôte (léger, démarre en secondes) ; une machine virtuelle embarque un OS complet sur un hyperviseur (plus lourde, isolation plus forte).
Le conteneur garantit que l'application tourne de la même façon sur le poste du développeur, en CI et en production : « ça marche chez moi » disparaît.
Image DockerModèle en lecture seule, composé de couches empilées, à partir duquel on lance des conteneurs ; identifiée par un nom, un tag et un digest (sha256).
Modèle en lecture seule, composé de couches empilées, à partir duquel on lance des conteneurs ; identifiée par un nom, un tag et un digest (sha256).
docker build -t registry.banque.sn/virements:1.4.2 .
docker run -d -p 8080:8080 --name virements registry.banque.sn/virements:1.4.2
docker imagesDockerfileFichier texte de recette pour construire une image : image de base (FROM), répertoire (WORKDIR), copie (COPY), commandes de build (RUN), port documenté (EXPOSE), commande de démarrage (ENTRYPOINT/CMD).
Fichier texte de recette pour construire une image : image de base (FROM), répertoire (WORKDIR), copie (COPY), commandes de build (RUN), port documenté (EXPOSE), commande de démarrage (ENTRYPOINT/CMD).
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]Couches et cache de buildChaque instruction du Dockerfile crée une couche mise en cache ; si une couche change, toutes les suivantes sont reconstruites. On place donc ce qui change rarement (dépendances) avant ce qui change souvent (code).
Chaque instruction du Dockerfile crée une couche mise en cache ; si une couche change, toutes les suivantes sont reconstruites. On place donc ce qui change rarement (dépendances) avant ce qui change souvent (code).
COPY pom.xml .
RUN ./mvnw dependency:go-offline # mis en cache tant que pom.xml ne change pas
COPY src ./src
RUN ./mvnw package -DskipTests # seule cette étape se refait quand le code changejava -Djarmode=tools -jar app.jar extract --layers) : seules les classes de l'application changent d'une version à l'autre.Multi-stage buildDockerfile en plusieurs étapes : une première image (JDK, Maven) compile, la dernière n'embarque que le résultat sur une image d'exécution légère. Image finale plus petite et plus sûre.
Dockerfile en plusieurs étapes : une première image (JDK, Maven) compile, la dernière n'embarque que le résultat sur une image d'exécution légère. Image finale plus petite et plus sûre.
# Étape 1 : build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests
# Étape 2 : exécution (sans JDK, sans Maven, sans sources)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Image de base : JDK vs JRE, alpine, distrolessPour exécuter, une image JRE suffit (pas de compilateur) ; alpine est très petite (musl libc), distroless ne contient même pas de shell : moins de taille, moins de vulnérabilités.
Pour exécuter, une image JRE suffit (pas de compilateur) ; alpine est très petite (musl libc), distroless ne contient même pas de shell : moins de taille, moins de vulnérabilités.
kubectl debug avec un conteneur éphémère.Utilisateur non-rootExécuter le processus avec un utilisateur sans privilèges (instruction USER) : si l'application est compromise, l'attaquant n'est pas root dans le conteneur.
Exécuter le processus avec un utilisateur sans privilèges (instruction USER) : si l'application est compromise, l'attaquant n'est pas root dans le conteneur.
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false.dockerignoreListe des fichiers exclus du contexte de build (target/, node_modules/, .git/, .env) : builds plus rapides et pas de secret embarqué par erreur dans l'image.
Liste des fichiers exclus du contexte de build (target/, node_modules/, .git/, .env) : builds plus rapides et pas de secret embarqué par erreur dans l'image.
.git
target/
node_modules/
*.envRegistry d'imagesDépôt où l'on publie et récupère les images (Docker Hub, GitLab Registry, Harbor, AWS ECR) ; le cluster Kubernetes y tire les images à déployer.
Dépôt où l'on publie et récupère les images (Docker Hub, GitLab Registry, Harbor, AWS ECR) ; le cluster Kubernetes y tire les images à déployer.
docker push registry.banque.sn/virements:1.4.2Tag d'image et digestLe tag est une étiquette lisible (1.4.2, sha du commit) ; le digest (sha256:…) identifie un contenu exact et immuable.
Le tag est une étiquette lisible (1.4.2, sha du commit) ; le digest (sha256:…) identifie un contenu exact et immuable.
:latest en production : on ne sait plus quelle version tourne et un rollback devient incertain. Taguer avec la version ou le hash du commit.Docker ComposeOutil qui décrit et lance plusieurs conteneurs ensemble (application, PostgreSQL, Kafka, Keycloak) dans un fichier YAML : idéal pour l'environnement de développement local.
Outil qui décrit et lance plusieurs conteneurs ensemble (application, PostgreSQL, Kafka, Keycloak) dans un fichier YAML : idéal pour l'environnement de développement local.
services:
postgres:
image: postgres:17-alpine
environment: { POSTGRES_DB: banque, POSTGRES_PASSWORD: dev }
ports: ["5432:5432"]
api:
build: .
depends_on: [postgres]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/banque
ports: ["8080:8080"]compose.yaml en dev avec spring-boot-docker-compose.VolumeStockage persistant monté dans un conteneur ; sans volume, les données écrites disparaissent quand le conteneur est supprimé.
Stockage persistant monté dans un conteneur ; sans volume, les données écrites disparaissent quand le conteneur est supprimé.
docker run -v pgdata:/var/lib/postgresql/data postgres:17-alpineBuildpacks et JibConstruire une image optimisée sans écrire de Dockerfile : Cloud Native Buildpacks (spring-boot:build-image) ou Jib (plugin Maven/Gradle, sans démon Docker).
Construire une image optimisée sans écrire de Dockerfile : Cloud Native Buildpacks (spring-boot:build-image) ou Jib (plugin Maven/Gradle, sans démon Docker).
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=registry.banque.sn/virements:1.4.22Kubernetes18
Kubernetes (k8s)Orchestrateur de conteneurs : il déploie, redémarre, met à l'échelle et répartit les conteneurs sur un cluster de machines, en convergeant en permanence vers l'état déclaré dans des fichiers YAML.
Orchestrateur de conteneurs : il déploie, redémarre, met à l'échelle et répartit les conteneurs sur un cluster de machines, en convergeant en permanence vers l'état déclaré dans des fichiers YAML.
On décrit l'état voulu (« 3 répliques de virements:1.4.2 »), Kubernetes s'occupe d'y arriver et d'y rester (auto-réparation).
Cluster, nœuds et control planeUn cluster = un control plane (API server, scheduler, etcd qui stocke l'état) + des nœuds de travail où le kubelet lance les pods.
Un cluster = un control plane (API server, scheduler, etcd qui stocke l'état) + des nœuds de travail où le kubelet lance les pods.
PodPlus petite unité déployable : un ou plusieurs conteneurs qui partagent réseau (même IP) et volumes. Un pod est éphémère : il peut être recréé ailleurs avec une autre IP.
Plus petite unité déployable : un ou plusieurs conteneurs qui partagent réseau (même IP) et volumes. Un pod est éphémère : il peut être recréé ailleurs avec une autre IP.
kubectl get pods -n banque
kubectl logs -f virements-7d9c8b6f4-x2kqp
kubectl describe pod virements-7d9c8b6f4-x2kqp # événements : OOMKilled, ImagePullBackOff…DeploymentObjet qui décrit l'application à exécuter (image, nombre de répliques, probes, ressources) et gère les mises à jour progressives et les rollbacks via des ReplicaSets.
Objet qui décrit l'application à exécuter (image, nombre de répliques, probes, ressources) et gère les mises à jour progressives et les rollbacks via des ReplicaSets.
apiVersion: apps/v1
kind: Deployment
metadata: { name: virements, namespace: banque }
spec:
replicas: 3
selector: { matchLabels: { app: virements } }
template:
metadata: { labels: { app: virements } }
spec:
containers:
- name: app
image: registry.banque.sn/virements:1.4.2
ports: [{ containerPort: 8080 }]
envFrom: [{ configMapRef: { name: virements-config } }, { secretRef: { name: virements-secrets } }]ReplicaSetGarantit qu'un nombre donné de pods identiques tourne en permanence ; géré automatiquement par le Deployment (un ReplicaSet par version).
Garantit qu'un nombre donné de pods identiques tourne en permanence ; géré automatiquement par le Deployment (un ReplicaSet par version).
Service KubernetesAdresse réseau stable (nom DNS + IP virtuelle) devant un ensemble de pods choisis par labels ; il répartit le trafic entre eux. Types : ClusterIP (interne), NodePort, LoadBalancer (exposé).
Adresse réseau stable (nom DNS + IP virtuelle) devant un ensemble de pods choisis par labels ; il répartit le trafic entre eux. Types : ClusterIP (interne), NodePort, LoadBalancer (exposé).
apiVersion: v1
kind: Service
metadata: { name: virements-service }
spec:
selector: { app: virements }
ports: [{ port: 80, targetPort: 8080 }]
# Les autres services appellent : http://virements-service.banque.svc.cluster.localIngressRègles de routage HTTP(S) depuis l'extérieur du cluster vers les Services (par domaine et chemin), avec terminaison TLS ; appliquées par un Ingress controller (NGINX, Traefik).
Règles de routage HTTP(S) depuis l'extérieur du cluster vers les Services (par domaine et chemin), avec terminaison TLS ; appliquées par un Ingress controller (NGINX, Traefik).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: api }
spec:
tls: [{ hosts: [api.banque.sn], secretName: api-tls }]
rules:
- host: api.banque.sn
http:
paths:
- path: /api/v1/virements
pathType: Prefix
backend: { service: { name: virements-service, port: { number: 80 } } }ConfigMapObjet qui stocke de la configuration non sensible (clé/valeur ou fichiers) injectée dans les pods en variables d'environnement ou fichiers montés.
Objet qui stocke de la configuration non sensible (clé/valeur ou fichiers) injectée dans les pods en variables d'environnement ou fichiers montés.
apiVersion: v1
kind: ConfigMap
metadata: { name: virements-config }
data:
SPRING_PROFILES_ACTIVE: prod
BANQUE_VIREMENT_PLAFOND_JOURNALIER: "5000000"Secret KubernetesObjet destiné aux données sensibles (mots de passe, clés), injecté comme une ConfigMap ; ses valeurs sont seulement encodées en Base64, il faut activer le chiffrement d'etcd et restreindre l'accès (RBAC).
Objet destiné aux données sensibles (mots de passe, clés), injecté comme une ConfigMap ; ses valeurs sont seulement encodées en Base64, il faut activer le chiffrement d'etcd et restreindre l'accès (RBAC).
NamespaceEspace logique qui cloisonne les ressources d'un cluster (par équipe ou environnement) : noms, quotas et droits RBAC propres.
Espace logique qui cloisonne les ressources d'un cluster (par équipe ou environnement) : noms, quotas et droits RBAC propres.
Requests et limits (ressources)requests : ressources réservées, utilisées par le scheduler pour placer le pod. limits : plafond ; au-delà de la mémoire limite, le conteneur est tué (OOMKilled), au-delà du CPU il est ralenti.
requests : ressources réservées, utilisées par le scheduler pour placer le pod. limits : plafond ; au-delà de la mémoire limite, le conteneur est tué (OOMKilled), au-delà du CPU il est ralenti.
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 1Gi }-XX:MaxRAMPercentage=75 afin que le heap tienne sous la limite mémoire du conteneur.HPA (Horizontal Pod Autoscaler)Ajuste automatiquement le nombre de répliques selon une métrique (CPU, mémoire, ou métrique métier comme le lag Kafka).
Ajuste automatiquement le nombre de répliques selon une métrique (CPU, mémoire, ou métrique métier comme le lag Kafka).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: virements }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: virements }
minReplicas: 2
maxReplicas: 10
metrics: [{ type: Resource, resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } } }]livenessProbeSonde qui vérifie que l'application est vivante ; en cas d'échecs répétés, le kubelet tue et redémarre le conteneur. Sert à sortir d'un état bloqué (deadlock).
Sonde qui vérifie que l'application est vivante ; en cas d'échecs répétés, le kubelet tue et redémarre le conteneur. Sert à sortir d'un état bloqué (deadlock).
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
failureThreshold: 3readinessProbeSonde qui indique si le pod peut recevoir du trafic ; en cas d'échec, il est retiré du Service (plus de requêtes) mais pas redémarré. Utile au démarrage, pendant un préchauffage ou une surcharge.
Sonde qui indique si le pod peut recevoir du trafic ; en cas d'échec, il est retiré du Service (plus de requêtes) mais pas redémarré. Utile au démarrage, pendant un préchauffage ou une surcharge.
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 5startupProbeSonde utilisée pendant le démarrage seulement : tant qu'elle n'a pas réussi, liveness et readiness ne sont pas évaluées. Évite de tuer une application lente à démarrer.
Sonde utilisée pendant le démarrage seulement : tant qu'elle n'a pas réussi, liveness et readiness ne sont pas évaluées. Évite de tuer une application lente à démarrer.
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
failureThreshold: 30
periodSeconds: 5 # jusqu'à 150 s pour démarrerArrêt gracieux (graceful shutdown)À l'arrêt d'un pod, Kubernetes envoie SIGTERM ; l'application cesse d'accepter des requêtes, termine celles en cours, puis s'arrête avant le SIGKILL (30 s par défaut).
À l'arrêt d'un pod, Kubernetes envoie SIGTERM ; l'application cesse d'accepter des requêtes, termine celles en cours, puis s'arrête avant le SIGKILL (30 s par défaut).
server.shutdown=graceful # défaut depuis Spring Boot 3.4
spring.lifecycle.timeout-per-shutdown-phase=20sHelmGestionnaire de paquets pour Kubernetes : un chart regroupe des templates YAML paramétrés par un fichier values.yaml, versionnés et installables en une commande.
Gestionnaire de paquets pour Kubernetes : un chart regroupe des templates YAML paramétrés par un fichier values.yaml, versionnés et installables en une commande.
helm upgrade --install virements ./charts/virements -n banque \
-f values-prod.yaml --set image.tag=1.4.2
helm rollback virements 12kubectl (commandes essentielles)Client en ligne de commande de Kubernetes pour inspecter et piloter le cluster.
Client en ligne de commande de Kubernetes pour inspecter et piloter le cluster.
kubectl get deploy,pods,svc -n banque
kubectl rollout status deploy/virements
kubectl rollout undo deploy/virements # rollback
kubectl top pods # consommation CPU/mémoire
kubectl exec -it virements-xxx -- sh
kubectl port-forward svc/virements-service 8080:803Stratégies de déploiement8
Rolling UpdateRemplacement progressif des pods de l'ancienne version par la nouvelle, quelques-uns à la fois ; stratégie par défaut de Kubernetes. Pendant la transition, les deux versions coexistent.
Remplacement progressif des pods de l'ancienne version par la nouvelle, quelques-uns à la fois ; stratégie par défaut de Kubernetes. Pendant la transition, les deux versions coexistent.
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } # jamais moins de capacité qu'avantBlue/GreenDeux environnements complets : Blue (actuel) et Green (nouvelle version). On teste Green, puis on bascule tout le trafic d'un coup ; rollback instantané en rebasculant. Coûte le double de ressources.
Deux environnements complets : Blue (actuel) et Green (nouvelle version). On teste Green, puis on bascule tout le trafic d'un coup ; rollback instantané en rebasculant. Coûte le double de ressources.
CanaryEnvoyer d'abord une petite partie du trafic (5 %) vers la nouvelle version, surveiller erreurs et latence, puis augmenter progressivement (25 %, 50 %, 100 %) ou annuler.
Envoyer d'abord une petite partie du trafic (5 %) vers la nouvelle version, surveiller erreurs et latence, puis augmenter progressivement (25 %, 50 %, 100 %) ou annuler.
Nécessite un routage fin du trafic (Istio, NGINX, Argo Rollouts) et de bonnes métriques pour décider automatiquement.
Feature flagInterrupteur de configuration qui active ou désactive une fonctionnalité sans redéployer ; découple le déploiement du code de la mise à disposition aux utilisateurs.
Interrupteur de configuration qui active ou désactive une fonctionnalité sans redéployer ; découple le déploiement du code de la mise à disposition aux utilisateurs.
if (features.estActive("virement-instantane", client)) { return virementInstantane(d); }
return virementClassique(d);RollbackRevenir rapidement à la version précédente quand un déploiement pose problème ; doit être préparé (images taguées, migrations compatibles) et testé.
Revenir rapidement à la version précédente quand un déploiement pose problème ; doit être préparé (images taguées, migrations compatibles) et testé.
Expand / Contract (migrations rétrocompatibles)Faire évoluer un schéma en plusieurs déploiements pour que l'ancienne et la nouvelle version fonctionnent ensemble : ajouter (expand), migrer, puis supprimer l'ancien (contract).
Faire évoluer un schéma en plusieurs déploiements pour que l'ancienne et la nouvelle version fonctionnent ensemble : ajouter (expand), migrer, puis supprimer l'ancien (contract).
v1 : colonne nom_complet
v2 : ajoute prenom + nom (nullable), écrit dans les trois colonnes ← expand
job de migration des données
v3 : ne lit plus nom_complet
v4 : DROP COLUMN nom_complet ← contractGitOpsL'état voulu de l'infrastructure et des déploiements est décrit dans Git ; un agent dans le cluster synchronise en continu le cluster sur ce dépôt. Déployer = merger, rollback = revert.
L'état voulu de l'infrastructure et des déploiements est décrit dans Git ; un agent dans le cluster synchronise en continu le cluster sur ce dépôt. Déployer = merger, rollback = revert.
Avantages : historique et audit de chaque changement, revue de code de l'infrastructure, pas d'accès direct à kubectl en production.
ArgoCDOutil GitOps pour Kubernetes : il compare en permanence les manifestes du dépôt Git à l'état du cluster, signale les écarts (drift) et les synchronise.
Outil GitOps pour Kubernetes : il compare en permanence les manifestes du dépôt Git à l'état du cluster, signale les écarts (drift) et les synchronise.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: { name: virements-prod }
spec:
source: { repoURL: https://gitlab.banque.sn/infra/deploiements.git, path: virements/prod, targetRevision: main }
destination: { server: https://kubernetes.default.svc, namespace: banque }
syncPolicy: { automated: { prune: true, selfHeal: true } }4CI/CD10
Intégration continue (CI)Chaque push déclenche automatiquement compilation, tests et analyses sur un serveur : les erreurs sont détectées en minutes, et la branche principale reste toujours dans un état livrable.
Chaque push déclenche automatiquement compilation, tests et analyses sur un serveur : les erreurs sont détectées en minutes, et la branche principale reste toujours dans un état livrable.
Livraison continue vs déploiement continuLivraison continue : chaque version validée est prête à être déployée, la mise en production reste une décision humaine (un clic). Déploiement continu : tout ce qui passe le pipeline part automatiquement en production.
Livraison continue : chaque version validée est prête à être déployée, la mise en production reste une décision humaine (un clic). Déploiement continu : tout ce qui passe le pipeline part automatiquement en production.
Pipeline et stagesSuite d'étapes automatisées (build → tests → qualité/sécurité → image → déploiement) ; si une étape échoue, la suite s'arrête.
Suite d'étapes automatisées (build → tests → qualité/sécurité → image → déploiement) ; si une étape échoue, la suite s'arrête.
# .gitlab-ci.yml
stages: [build, test, quality, package, deploy]
test:
stage: test
image: eclipse-temurin:21-jdk
script: ["./mvnw -B verify"]
artifacts: { reports: { junit: target/surefire-reports/*.xml } }
image:
stage: package
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
deploy-prod:
stage: deploy
when: manual # livraison continue : validation humaine
environment: production
script: ["helm upgrade --install virements ./chart --set image.tag=$CI_COMMIT_SHORT_SHA"]ArtefactProduit versionné d'un build (JAR, image Docker, bundle Angular) qu'on promeut d'environnement en environnement sans le reconstruire : ce qui a été testé est exactement ce qui est déployé.
Produit versionné d'un build (JAR, image Docker, bundle Angular) qu'on promeut d'environnement en environnement sans le reconstruire : ce qui a été testé est exactement ce qui est déployé.
SonarQube et quality gateSonarQube analyse le code sans l'exécuter (bugs, vulnérabilités, code smells, duplication, couverture) ; la quality gate est le seuil qui fait échouer le pipeline si le nouveau code ne respecte pas les critères.
SonarQube analyse le code sans l'exécuter (bugs, vulnérabilités, code smells, duplication, couverture) ; la quality gate est le seuil qui fait échouer le pipeline si le nouveau code ne respecte pas les critères.
./mvnw verify sonar:sonar -Dsonar.host.url=$SONAR_URL -Dsonar.token=$SONAR_TOKENScan d'image (Trivy)Outil qui détecte les vulnérabilités connues (CVE) des paquets de l'OS et des bibliothèques d'une image, ainsi que les mauvaises configurations ; bloque le pipeline au-delà d'un seuil de criticité.
Outil qui détecte les vulnérabilités connues (CVE) des paquets de l'OS et des bibliothèques d'une image, ainsi que les mauvaises configurations ; bloque le pipeline au-delà d'un seuil de criticité.
GitLab CI et GitHub ActionsMoteurs de CI/CD intégrés aux forges Git : pipelines décrits en YAML dans le dépôt, exécutés par des runners.
Moteurs de CI/CD intégrés aux forges Git : pipelines décrits en YAML dans le dépôt, exécutés par des runners.
# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 21, cache: maven }
- run: ./mvnw -B verifyVariables CI protégées et masquéesLes secrets du pipeline (tokens de registry, Sonar, kubeconfig) sont stockés comme variables chiffrées, masquées dans les logs et réservées aux branches protégées.
Les secrets du pipeline (tokens de registry, Sonar, kubeconfig) sont stockés comme variables chiffrées, masquées dans les logs et réservées aux branches protégées.
echo $SECRET ou un set -x dans un script peut quand même exposer un secret : vérifier les logs et limiter les droits des runners.Versionnage sémantique (SemVer)Format MAJEUR.MINEUR.CORRECTIF : MAJEUR = changement incompatible, MINEUR = ajout rétrocompatible, CORRECTIF = correction de bug. Ex. 1.4.2 → 1.5.0 pour une nouvelle fonctionnalité.
Format MAJEUR.MINEUR.CORRECTIF : MAJEUR = changement incompatible, MINEUR = ajout rétrocompatible, CORRECTIF = correction de bug. Ex. 1.4.2 → 1.5.0 pour une nouvelle fonctionnalité.
Infrastructure as Code (Terraform)Décrire l'infrastructure (réseaux, bases managées, clusters) dans des fichiers versionnés et l'appliquer par outil (Terraform, Pulumi), plutôt que de cliquer dans une console.
Décrire l'infrastructure (réseaux, bases managées, clusters) dans des fichiers versionnés et l'appliquer par outil (Terraform, Pulumi), plutôt que de cliquer dans une console.
resource "aws_db_instance" "banque" {
engine = "postgres"
engine_version = "17"
instance_class = "db.m6g.large"
allocated_storage = 100
storage_encrypted = true
}5Git10
git mergeIntègre une branche dans une autre ; si les historiques ont divergé, crée un commit de fusion à deux parents. L'historique réel est conservé (non linéaire).
Intègre une branche dans une autre ; si les historiques ont divergé, crée un commit de fusion à deux parents. L'historique réel est conservé (non linéaire).
git switch main
git merge --no-ff feature/virement-instantane # force un commit de merge expliciteFast-forwardQuand la branche cible n'a pas avancé depuis la création de la branche, Git déplace simplement son pointeur : pas de commit de fusion, historique linéaire.
Quand la branche cible n'a pas avancé depuis la création de la branche, Git déplace simplement son pointeur : pas de commit de fusion, historique linéaire.
git rebaseRejoue les commits d'une branche au-dessus d'une autre base : historique linéaire et propre, mais les commits sont réécrits (nouveaux identifiants).
Rejoue les commits d'une branche au-dessus d'une autre base : historique linéaire et propre, mais les commits sont réécrits (nouveaux identifiants).
git switch feature/kyc
git fetch origin
git rebase origin/main # résoudre les conflits commit par commit, puis git rebase --continue
git push --force-with-lease # la branche a été réécritepush --force vs --force-with-lease--force écrase la branche distante quoi qu'il arrive (peut effacer le travail d'un collègue) ; --force-with-lease refuse si la branche distante a changé depuis ta dernière récupération.
--force écrase la branche distante quoi qu'il arrive (peut effacer le travail d'un collègue) ; --force-with-lease refuse si la branche distante a changé depuis ta dernière récupération.
SquashRegrouper plusieurs commits en un seul (rebase interactif ou « squash merge » d'une pull request) pour garder un historique lisible : un commit = une fonctionnalité.
Regrouper plusieurs commits en un seul (rebase interactif ou « squash merge » d'une pull request) pour garder un historique lisible : un commit = une fonctionnalité.
Cherry-pickAppliquer un commit précis d'une branche sur une autre, typiquement pour reporter un correctif urgent sur une branche de release.
Appliquer un commit précis d'une branche sur une autre, typiquement pour reporter un correctif urgent sur une branche de release.
git switch release/1.4
git cherry-pick 3f2a9c1git revert vs git resetrevert crée un nouveau commit qui annule un commit existant (sûr sur une branche partagée) ; reset déplace la branche en arrière et réécrit l'historique (à réserver au local).
revert crée un nouveau commit qui annule un commit existant (sûr sur une branche partagée) ; reset déplace la branche en arrière et réécrit l'historique (à réserver au local).
git revert 3f2a9c1 # ✅ sur main en production
git reset --hard HEAD~1 # ⚠️ local uniquement : supprime le dernier commitPull request / merge request et code reviewDemande d'intégration d'une branche, accompagnée d'une revue par un pair et des vérifications automatiques (CI, Sonar) avant fusion ; principal outil de qualité et de partage de connaissances.
Demande d'intégration d'une branche, accompagnée d'une revue par un pair et des vérifications automatiques (CI, Sonar) avant fusion ; principal outil de qualité et de partage de connaissances.
Trunk-based development vs Git FlowTrunk-based : branches très courtes fusionnées au moins quotidiennement dans main, avec feature flags ; adapté au déploiement continu. Git Flow : branches develop, release, hotfix ; plus lourd, adapté aux livraisons planifiées.
Trunk-based : branches très courtes fusionnées au moins quotidiennement dans main, avec feature flags ; adapté au déploiement continu. Git Flow : branches develop, release, hotfix ; plus lourd, adapté aux livraisons planifiées.
Conventional CommitsConvention de messages de commit (feat:, fix:, refactor:, chore:…) qui rend l'historique lisible et permet de générer automatiquement changelog et numéro de version.
Convention de messages de commit (feat:, fix:, refactor:, chore:…) qui rend l'historique lisible et permet de générer automatiquement changelog et numéro de version.
feat(virement): ajoute le plafond journalier configurable
fix(kyc): corrige la validation du numéro CNI
refactor!: renomme l'API /clients en /customers ← "!" = changement incompatible (MAJEUR)📋 Synthèse express — DevOps
🐳 Docker — Multi-stage Build
☸️ Kubernetes — Probes
| Probe | Rôle | Échec |
|---|---|---|
| livenessProbe | L'application est-elle vivante ? | Restart du pod |
| readinessProbe | L'application peut-elle recevoir du trafic ? | Retiré du Service (pas de restart) |
| startupProbe | L'application a-t-elle démarré ? | Remplace liveness pendant le démarrage |
🚀 Stratégies de déploiement
Deux environnements identiques. Basculement instantané. Rollback immédiat. Coûteux en ressources (2x).
Déploiement progressif : 5% → 25% → 100%. Validation sur petit pourcentage. Risque limité. Needs traffic routing (Istio/NGINX).
Remplacement progressif pod par pod. Défaut Kubernetes. Risque de deux versions simultanées. Doit être backward-compatible.
Git = source de vérité. ArgoCD synchronise le cluster avec l'état Git. Tout changement = commit. Rollback = revert Git.
🌿 Git — Merge vs Rebase
| Critère | git merge | git rebase |
|---|---|---|
| Historique | Non-linéaire (commit de merge) | Linéaire (propre) |
| Conflits | Résolu en 1 fois | Résolu à chaque commit |
| Usage | Intégration de branches longues | Nettoyer avant PR/merge |
| Risque | Historique surchargé | Réécrire l'historique partagé ❌ |
| Commande | git merge feature | git rebase main |
🔄 Pipeline CI/CD — Étapes clés
Compilation et génération des artefacts (mvn package, npm run build).
Tests unitaires → intégration → E2E. Couverture Jacoco/Istanbul. Blocage si seuil non atteint.
SonarQube, ESLint, Checkstyle. Détection code smells et vulnérabilités.
Build image, scan (Trivy), push registry, déploiement Blue/Green ou Rolling.
1Stratégie de test13
Test unitaireTeste une seule unité de code (une classe, une méthode) isolée de ses dépendances (remplacées par des doublures) ; très rapide (millisecondes), sans Spring, sans base, sans réseau.
Teste une seule unité de code (une classe, une méthode) isolée de ses dépendances (remplacées par des doublures) ; très rapide (millisecondes), sans Spring, sans base, sans réseau.
class CalculFraisTest {
private final CalculFrais calcul = new CalculFrais();
@Test
void virement_interne_sans_frais() {
assertThat(calcul.frais(TypeVirement.INTERNE, new BigDecimal("100000"))).isEqualByComparingTo("0");
}
}Test d'intégrationVérifie que plusieurs composants fonctionnent ensemble avec de vraies infrastructures ou des équivalents fidèles (contexte Spring, base PostgreSQL en conteneur, sérialisation JSON, transactions).
Vérifie que plusieurs composants fonctionnent ensemble avec de vraies infrastructures ou des équivalents fidèles (contexte Spring, base PostgreSQL en conteneur, sérialisation JSON, transactions).
Il détecte ce que les tests unitaires ne voient pas : mapping JPA erroné, requête SQL invalide, configuration de sécurité, contrat JSON.
Test end-to-end (E2E)Simule un utilisateur réel à travers toute l'application (navigateur → Angular → API → base) ; le plus réaliste, mais lent, coûteux et plus fragile. À réserver aux parcours critiques (connexion, virement).
Simule un utilisateur réel à travers toute l'application (navigateur → Angular → API → base) ; le plus réaliste, mais lent, coûteux et plus fragile. À réserver aux parcours critiques (connexion, virement).
Pyramide des testsRépartition recommandée : beaucoup de tests unitaires (base, ~70 %), moins de tests d'intégration (~20 %), peu de tests E2E (sommet, ~10 %). Plus on monte, plus c'est lent et coûteux.
Répartition recommandée : beaucoup de tests unitaires (base, ~70 %), moins de tests d'intégration (~20 %), peu de tests E2E (sommet, ~10 %). Plus on monte, plus c'est lent et coûteux.
Arrange-Act-Assert / Given-When-ThenStructure d'un test en trois temps : préparer les données (Arrange/Given), exécuter l'action (Act/When), vérifier le résultat (Assert/Then). Un test = un comportement.
Structure d'un test en trois temps : préparer les données (Arrange/Given), exécuter l'action (Act/When), vérifier le résultat (Assert/Then). Un test = un comportement.
@Test
void debit_refuse_si_solde_insuffisant() {
// Given
Compte compte = new Compte(new BigDecimal("1000"));
// When
Throwable erreur = catchThrowable(() -> compte.debiter(new BigDecimal("5000")));
// Then
assertThat(erreur).isInstanceOf(SoldeInsuffisantException.class);
assertThat(compte.solde()).isEqualByComparingTo("1000");
}TDD (Test-Driven Development)Développement piloté par les tests : écrire d'abord un test qui échoue (Red), puis le minimum de code pour le faire passer (Green), puis améliorer le code sans casser les tests (Refactor). Cycles de quelques minutes.
Développement piloté par les tests : écrire d'abord un test qui échoue (Red), puis le minimum de code pour le faire passer (Green), puis améliorer le code sans casser les tests (Refactor). Cycles de quelques minutes.
Bénéfices : conception guidée par l'usage, couverture naturelle, filet de sécurité pour refactorer. L'étape Red prouve que le test détecte bien l'absence de la fonctionnalité.
// 1. RED : le test échoue (la méthode n'existe pas ou renvoie 0)
@Test void frais_mobile_money_1_pourcent_plafonne_a_5000() {
assertThat(calcul.fraisMobileMoney(new BigDecimal("800000"))).isEqualByComparingTo("5000");
}
// 2. GREEN : implémentation minimale
BigDecimal fraisMobileMoney(BigDecimal m) { return m.multiply(new BigDecimal("0.01")).min(new BigDecimal("5000")); }
// 3. REFACTOR : extraire les constantes TAUX et PLAFOND, relancer les testsBDD (Behavior-Driven Development)Approche où les comportements attendus sont décrits en langage naturel structuré (Given/When/Then), rédigés avec le métier, puis automatisés comme tests d'acceptation.
Approche où les comportements attendus sont décrits en langage naturel structuré (Given/When/Then), rédigés avec le métier, puis automatisés comme tests d'acceptation.
Gherkin et CucumberGherkin est la syntaxe des scénarios BDD (Feature, Scenario, Given, When, Then) ; Cucumber exécute ces scénarios en les reliant à des méthodes Java (step definitions).
Gherkin est la syntaxe des scénarios BDD (Feature, Scenario, Given, When, Then) ; Cucumber exécute ces scénarios en les reliant à des méthodes Java (step definitions).
Fonctionnalité: Virement entre comptes
Scénario: Virement refusé pour solde insuffisant
Étant donné un compte "SN01" avec un solde de 10000 XOF
Quand le client vire 25000 XOF vers "SN02"
Alors le virement est refusé avec le motif "SOLDE_INSUFFISANT"@Quand("le client vire {int} XOF vers {string}")
public void virer(int montant, String dest) { resultat = service.virer(source, dest, BigDecimal.valueOf(montant)); }Couverture de code (JaCoCo)Pourcentage du code exécuté par les tests (lignes, branches) mesuré par JaCoCo en Java (Istanbul côté JavaScript) ; indique ce qui n'est PAS testé, pas la qualité des tests.
Pourcentage du code exécuté par les tests (lignes, branches) mesuré par JaCoCo en Java (Istanbul côté JavaScript) ; indique ce qui n'est PAS testé, pas la qualité des tests.
<plugin>
<groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution><goals><goal>prepare-agent</goal></goals></execution>
<execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
</executions>
</plugin>Mutation testing (PIT)Outil qui modifie volontairement le code (inverse une condition, change un > en >=) et vérifie que les tests échouent ; un mutant qui « survit » révèle un test trop faible.
Outil qui modifie volontairement le code (inverse une condition, change un > en >=) et vérifie que les tests échouent ; un mutant qui « survit » révèle un test trop faible.
./mvnw org.pitest:pitest-maven:mutationCoverageTest flaky (instable)Test qui passe ou échoue de façon aléatoire sans changement de code (dépendance à l'heure, à l'ordre d'exécution, à des sleep, à des données partagées) ; il détruit la confiance dans la CI.
Test qui passe ou échoue de façon aléatoire sans changement de code (dépendance à l'heure, à l'ordre d'exécution, à des sleep, à des données partagées) ; il détruit la confiance dans la CI.
Clock), données isolées par test, attentes sur condition (Awaitility) au lieu de Thread.sleep.Test de contrat (Pact, Spring Cloud Contract)Vérifier automatiquement qu'un fournisseur d'API respecte ce que ses consommateurs attendent (format, champs, codes), sans démarrer tout le système ; évite de casser un autre microservice ou le front.
Vérifier automatiquement qu'un fournisseur d'API respecte ce que ses consommateurs attendent (format, champs, codes), sans démarrer tout le système ; évite de casser un autre microservice ou le front.
Tests de charge (Gatling, k6)Simuler de nombreux utilisateurs simultanés pour mesurer débit, latence (P95/P99) et point de rupture avant la production.
Simuler de nombreux utilisateurs simultanés pour mesurer débit, latence (P95/P99) et point de rupture avant la production.
// k6 : 200 utilisateurs virtuels pendant 5 minutes
import http from 'k6/http';
export const options = { vus: 200, duration: '5m', thresholds: { http_req_duration: ['p(95)<300'] } };
export default function () { http.get('https://staging.banque.sn/api/v1/comptes/42'); }2JUnit & assertions7
JUnit 5 / JUnit 6Framework de tests standard en Java (plateforme JUnit Jupiter). Spring Boot 4 utilise JUnit 6 (Java 17+), dans la continuité de l'API de JUnit 5.
Framework de tests standard en Java (plateforme JUnit Jupiter). Spring Boot 4 utilise JUnit 6 (Java 17+), dans la continuité de l'API de JUnit 5.
@DisplayName("Service de virement")
class VirementServiceTest {
@BeforeEach void setUp() { … }
@Test @DisplayName("refuse un montant négatif")
void montantNegatif() { … }
}@Test et @DisplayName@Test marque une méthode de test ; @DisplayName donne un libellé lisible affiché dans les rapports (« refuse un virement au-delà du plafond »).
@Test marque une méthode de test ; @DisplayName donne un libellé lisible affiché dans les rapports (« refuse un virement au-delà du plafond »).
@BeforeEach, @AfterEach, @BeforeAllMéthodes exécutées avant/après chaque test (préparation, nettoyage) ou une seule fois avant/après tous les tests de la classe (méthodes statiques).
Méthodes exécutées avant/après chaque test (préparation, nettoyage) ou une seule fois avant/après tous les tests de la classe (méthodes statiques).
@ParameterizedTestExécute le même test avec plusieurs jeux de données, chacun rapporté séparément.
Exécute le même test avec plusieurs jeux de données, chacun rapporté séparément.
@ParameterizedTest(name = "{0} XOF → frais {1}")
@CsvSource({ "1000, 10", "100000, 1000", "800000, 5000" })
void frais(BigDecimal montant, BigDecimal attendu) {
assertThat(calcul.fraisMobileMoney(montant)).isEqualByComparingTo(attendu);
}@NestedClasse de test interne qui regroupe des tests par contexte (« quand le compte est gelé… »), pour des rapports structurés et lisibles.
Classe de test interne qui regroupe des tests par contexte (« quand le compte est gelé… »), pour des rapports structurés et lisibles.
AssertJ (assertThat)Bibliothèque d'assertions fluides et lisibles, avec des messages d'échec précis ; incluse dans spring-boot-starter-test.
Bibliothèque d'assertions fluides et lisibles, avec des messages d'échec précis ; incluse dans spring-boot-starter-test.
assertThat(clients)
.hasSize(3)
.extracting(Client::ville)
.containsExactlyInAnyOrder("Dakar", "Thiès", "Saint-Louis");
assertThat(montant).isEqualByComparingTo("15000.00"); // BigDecimal : ignore l'échelleisEqualTo compare aussi l'échelle (15000 ≠ 15000.00) : utiliser isEqualByComparingTo.assertThatThrownByVérifie qu'un code lève bien une exception d'un type donné, avec le bon message.
Vérifie qu'un code lève bien une exception d'un type donné, avec le bon message.
assertThatThrownBy(() -> service.getOrder(99L))
.isInstanceOf(NotFoundException.class)
.hasMessage("Order not found: 99");3Doublures de test (mocks)6
Mock, Stub, Spy, FakeDoublures qui remplacent une dépendance en test. Stub : renvoie des réponses prédéfinies. Mock : vérifie aussi les interactions (appelé ? combien de fois ?). Spy : objet réel dont on surcharge une partie. Fake : implémentation simplifiée qui fonctionne (repository en mémoire).
Doublures qui remplacent une dépendance en test. Stub : renvoie des réponses prédéfinies. Mock : vérifie aussi les interactions (appelé ? combien de fois ?). Spy : objet réel dont on surcharge une partie. Fake : implémentation simplifiée qui fonctionne (repository en mémoire).
| Doublure | Renvoie des valeurs | Vérifie les appels | Logique réelle |
|---|---|---|---|
| Stub | ✅ | ❌ | ❌ |
| Mock | ✅ | ✅ | ❌ |
| Spy | partiel | ✅ | ✅ |
| Fake | ✅ | ❌ | simplifiée |
MockitoBibliothèque Java de création de mocks : on définit leurs réponses (when/given) et on vérifie leurs interactions (verify/then).
Bibliothèque Java de création de mocks : on définit leurs réponses (when/given) et on vérifie leurs interactions (verify/then).
@Mock, @InjectMocks et MockitoExtension@ExtendWith(MockitoExtension.class) active Mockito ; @Mock crée une doublure ; @InjectMocks instancie la classe testée en lui injectant les mocks (par constructeur).
@ExtendWith(MockitoExtension.class) active Mockito ; @Mock crée une doublure ; @InjectMocks instancie la classe testée en lui injectant les mocks (par constructeur).
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository repo;
@Mock PaymentGateway gateway;
@InjectMocks OrderService service; // new OrderService(repo, gateway)
}when/thenReturn et given/willReturn (BDDMockito)Programmer la réponse d'un mock : when(...).thenReturn(...) en style classique, given(...).willReturn(...) en style BDD, plus lisible dans un test Given/When/Then.
Programmer la réponse d'un mock : when(...).thenReturn(...) en style classique, given(...).willReturn(...) en style BDD, plus lisible dans un test Given/When/Then.
given(repo.findById(99L)).willReturn(Optional.empty());
given(gateway.charger(any(), any())).willThrow(new OperateurIndisponibleException());
when(tauxService.taux("EUR")).thenReturn(new BigDecimal("655.957"));verify et then().should()Vérifier qu'un mock a été appelé (ou non), avec quels arguments et combien de fois.
Vérifier qu'un mock a été appelé (ou non), avec quels arguments et combien de fois.
then(repo).should().findById(99L);
then(notificateur).should(never()).envoyer(any());
verify(repo, times(1)).save(argThat(c -> c.solde().signum() >= 0));ArgumentCaptorCapture l'argument passé à un mock pour l'inspecter en détail ensuite.
Capture l'argument passé à un mock pour l'inspecter en détail ensuite.
@Captor ArgumentCaptor<Audit> audit;
service.virer(…);
then(auditRepo).should().save(audit.capture());
assertThat(audit.getValue().type()).isEqualTo("VIREMENT_INITIE");4Tests Spring Boot13
@SpringBootTestDémarre le contexte Spring complet (tous les beans) pour un test d'intégration ; avec webEnvironment = RANDOM_PORT, lance aussi un vrai serveur HTTP.
Démarre le contexte Spring complet (tous les beans) pour un test d'intégration ; avec webEnvironment = RANDOM_PORT, lance aussi un vrai serveur HTTP.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class VirementApiIT { … }Test slices : @WebMvcTest, @DataJpaTestAnnotations qui ne chargent qu'une « tranche » de l'application : @WebMvcTest (contrôleurs, sécurité, JSON — services mockés), @DataJpaTest (repositories, JPA, base). Plus rapides que @SpringBootTest.
Annotations qui ne chargent qu'une « tranche » de l'application : @WebMvcTest (contrôleurs, sécurité, JSON — services mockés), @DataJpaTest (repositories, JPA, base). Plus rapides que @SpringBootTest.
@WebMvcTest(ClientController.class)
class ClientControllerTest {
@Autowired MockMvc mvc;
@MockitoBean ClientService service;
}
@DataJpaTest
class CompteRepositoryTest {
@Autowired CompteRepository repo;
@Test void trouve_par_iban() { … }
}MockMvcPermet d'exécuter des requêtes HTTP simulées sur les contrôleurs Spring MVC sans démarrer de serveur, et de vérifier statut, en-têtes et JSON.
Permet d'exécuter des requêtes HTTP simulées sur les contrôleurs Spring MVC sans démarrer de serveur, et de vérifier statut, en-têtes et JSON.
mockMvc.perform(post("/api/v1/clients")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"nom": "", "email": "pas-un-email"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors.email").exists());@MockitoBean (remplace @MockBean)Remplace un bean du contexte Spring par un mock Mockito dans un test Spring. @MockitoBean (Spring Framework 6.2) remplace @MockBean, déprécié depuis Spring Boot 3.4.
Remplace un bean du contexte Spring par un mock Mockito dans un test Spring. @MockitoBean (Spring Framework 6.2) remplace @MockBean, déprécié depuis Spring Boot 3.4.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean UserService userService; // avant : @MockBean
}jsonPathExpression qui cible une valeur dans une réponse JSON ($.name, $.items[0].id, $.length()) pour l'asserter dans MockMvc ou RestAssured.
Expression qui cible une valeur dans une réponse JSON ($.name, $.items[0].id, $.length()) pour l'asserter dans MockMvc ou RestAssured.
.andExpect(jsonPath("$.content.length()").value(20))
.andExpect(jsonPath("$.content[0].iban").value(startsWith("SN")))TestcontainersBibliothèque qui démarre de vrais services dans des conteneurs Docker jetables pendant les tests (PostgreSQL, Kafka, Redis, Keycloak) : on teste contre la vraie technologie et non une base H2 différente.
Bibliothèque qui démarre de vrais services dans des conteneurs Docker jetables pendant les tests (PostgreSQL, Kafka, Redis, Keycloak) : on teste contre la vraie technologie et non une base H2 différente.
@SpringBootTest
@Testcontainers
class CompteRepositoryIT {
@Container
@ServiceConnection // Boot configure la DataSource tout seul
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:17-alpine");
@Autowired CompteRepository repo;
@Test void sauvegarde_et_relit() { … }
}@Container et @Testcontainers@Testcontainers active l'extension JUnit ; @Container marque un conteneur dont elle gère le cycle de vie (statique = un conteneur partagé par toute la classe).
@Testcontainers active l'extension JUnit ; @Container marque un conteneur dont elle gère le cycle de vie (statique = un conteneur partagé par toute la classe).
@DynamicPropertySourceInjecte dans le contexte Spring des propriétés calculées à l'exécution (URL et port aléatoires d'un conteneur), avant le démarrage du contexte.
Injecte dans le contexte Spring des propriétés calculées à l'exécution (URL et port aléatoires d'un conteneur), avant le démarrage du contexte.
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
}@ServiceConnectionAnnotation (Spring Boot 3.1+) posée sur un conteneur Testcontainers : Boot en déduit automatiquement la configuration de connexion (DataSource, Kafka, Redis…), sans @DynamicPropertySource.
Annotation (Spring Boot 3.1+) posée sur un conteneur Testcontainers : Boot en déduit automatiquement la configuration de connexion (DataSource, Kafka, Redis…), sans @DynamicPropertySource.
RestAssuredBibliothèque de tests d'API HTTP au style Given/When/Then, utilisée contre une application réellement démarrée (RANDOM_PORT) ou un environnement de recette.
Bibliothèque de tests d'API HTTP au style Given/When/Then, utilisée contre une application réellement démarrée (RANDOM_PORT) ou un environnement de recette.
given().port(port).auth().oauth2(token).contentType(JSON)
.body(Map.of("ibanDest", "SN02…", "montant", 25000))
.when().post("/api/v1/virements")
.then().statusCode(201).body("statut", equalTo("EN_COURS"));RestTestClient (Spring Boot 4)Client de test HTTP fluide de Spring Boot 4, qui remplace TestRestTemplate (déprécié) et s'utilise aussi bien sur MockMvc que sur un serveur démarré.
Client de test HTTP fluide de Spring Boot 4, qui remplace TestRestTemplate (déprécié) et s'utilise aussi bien sur MockMvc que sur un serveur démarré.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureRestTestClient
class ApiIT {
@Autowired RestTestClient client;
@Test void sante() { client.get().uri("/actuator/health").exchange().expectStatus().isOk(); }
}WireMockFaux serveur HTTP programmable qui simule une API externe (opérateur mobile money, BCEAO) : réponses, lenteurs et erreurs, pour tester timeouts, retries et circuit breakers.
Faux serveur HTTP programmable qui simule une API externe (opérateur mobile money, BCEAO) : réponses, lenteurs et erreurs, pour tester timeouts, retries et circuit breakers.
stubFor(post("/v1/paiements").willReturn(aResponse().withStatus(503).withFixedDelay(4000)));Tester la sécurité (@WithMockUser, jwt())Simuler un utilisateur authentifié et ses rôles dans un test, pour vérifier que les règles d'accès renvoient bien 401/403 quand il le faut.
Simuler un utilisateur authentifié et ses rôles dans un test, pour vérifier que les règles d'accès renvoient bien 401/403 quand il le faut.
mockMvc.perform(get("/api/v1/admin/clients").with(jwt().authorities(new SimpleGrantedAuthority("ROLE_CLIENT"))))
.andExpect(status().isForbidden());
@Test @WithMockUser(roles = "ADMIN")
void admin_peut_lister() throws Exception { mockMvc.perform(get("/api/v1/admin/clients")).andExpect(status().isOk()); }5Tests Angular5
TestBedModule de test d'Angular qui configure un environnement d'injection et crée les composants à tester, avec leurs dépendances réelles ou simulées.
Module de test d'Angular qui configure un environnement d'injection et crée les composants à tester, avec leurs dépendances réelles ou simulées.
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ListeComptesComponent],
providers: [{ provide: CompteService, useValue: { lister: () => of(COMPTES) } }],
}).compileComponents();
});ComponentFixture et detectChangesLa fixture enveloppe le composant créé : accès à son instance et à son DOM ; detectChanges() (ou whenStable() en zoneless) met le rendu à jour avant les vérifications.
La fixture enveloppe le composant créé : accès à son instance et à son DOM ; detectChanges() (ou whenStable() en zoneless) met le rendu à jour avant les vérifications.
const fixture = TestBed.createComponent(ListeComptesComponent);
fixture.detectChanges();
const lignes = fixture.nativeElement.querySelectorAll('li');
expect(lignes.length).toBe(3);HttpTestingControllerContrôleur de test qui intercepte les requêtes HttpClient : on vérifie l'URL et la méthode appelées, puis on fournit la réponse simulée.
Contrôleur de test qui intercepte les requêtes HttpClient : on vérifie l'URL et la méthode appelées, puis on fournit la réponse simulée.
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
const http = TestBed.inject(HttpTestingController);
service.lister().subscribe(c => expect(c.length).toBe(2));
const req = http.expectOne('/api/v1/clients');
expect(req.request.method).toBe('GET');
req.flush([{ id: 1 }, { id: 2 }]);
http.verify(); // aucune requête inattendueJasmine/Karma, Jest et VitestOutils de tests JavaScript : Jasmine (assertions) exécuté par Karma dans un navigateur, historiquement par défaut ; Jest puis Vitest (défaut depuis Angular 21) sont plus rapides et s'exécutent en Node.
Outils de tests JavaScript : Jasmine (assertions) exécuté par Karma dans un navigateur, historiquement par défaut ; Jest puis Vitest (défaut depuis Angular 21) sont plus rapides et s'exécutent en Node.
Cypress et PlaywrightFrameworks de tests E2E qui pilotent un vrai navigateur ; Playwright (multi-navigateurs, rapide, attente automatique) est aujourd'hui le choix le plus courant, Selenium est l'ancien standard.
Frameworks de tests E2E qui pilotent un vrai navigateur ; Playwright (multi-navigateurs, rapide, attente automatique) est aujourd'hui le choix le plus courant, Selenium est l'ancien standard.
test('virement réussi', async ({ page }) => {
await page.goto('/virements/nouveau');
await page.getByLabel('IBAN bénéficiaire').fill('SN02…');
await page.getByLabel('Montant').fill('25000');
await page.getByRole('button', { name: 'Valider' }).click();
await expect(page.getByText('Virement en cours')).toBeVisible();
});📋 Synthèse express — Tests
🔺 Pyramide des tests
Rapides, isolés. Mockent les dépendances. JUnit 5 + Mockito. Testent 1 classe/méthode à la fois.
Testent plusieurs couches ensemble. @SpringBootTest, Testcontainers pour DB/Redis réels en Docker.
Simulent un vrai utilisateur. Playwright/Selenium. Lents, fragiles. Seulement les scénarios critiques.
🔴🟢🔵 Cycle TDD
🃏 JUnit 5 + Mockito
🐳 Testcontainers
🧪 MockMvc — Test Spring MVC
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean UserService userService; // remplace @MockBean (déprécié depuis Boot 3.4)
@Test
void getUser_shouldReturn200() throws Exception {
when(userService.findById(1L)).thenReturn(new UserDto(1L, "Alice"));
mockMvc.perform(get("/api/users/1")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
@Test
void getUser_notFound_shouldReturn404() throws Exception {
when(userService.findById(99L)).thenThrow(new ResourceNotFoundException("User not found"));
mockMvc.perform(get("/api/users/99"))
.andExpect(status().isNotFound());
}
}
🧰 Outils par niveau de test
| Niveau | Outils Java | Outils Angular | Vitesse | Couverture |
|---|---|---|---|---|
| Unitaires (70%) | JUnit 5, Mockito | Jasmine, Jest | ⚡ Très rapide | Logique métier |
| Intégration (20%) | @SpringBootTest, MockMvc | TestBed Angular | 🔄 Moyen | Couche service/API |
| E2E (10%) | RestAssured | Cypress, Playwright | 🐢 Lent | Parcours utilisateur |
1Fondamentaux3
Monitoring vs observabilitéLe monitoring surveille des indicateurs connus à l'avance (CPU, erreurs) et alerte ; l'observabilité est la capacité à comprendre n'importe quel comportement inattendu du système à partir des données qu'il émet, sans redéployer.
Le monitoring surveille des indicateurs connus à l'avance (CPU, erreurs) et alerte ; l'observabilité est la capacité à comprendre n'importe quel comportement inattendu du système à partir des données qu'il émet, sans redéployer.
Monitoring : « est-ce que ça marche ? ». Observabilité : « pourquoi ça ne marche pas, pour ce client, sur ce virement précis ? ».
Les trois piliers : métriques, logs, tracesMétriques : chiffres agrégés dans le temps (combien, à quelle vitesse). Logs : événements détaillés (que s'est-il passé). Traces : parcours d'une requête à travers les services (où le temps a été passé).
Métriques : chiffres agrégés dans le temps (combien, à quelle vitesse). Logs : événements détaillés (que s'est-il passé). Traces : parcours d'une requête à travers les services (où le temps a été passé).
| Pilier | Question | Outil type |
|---|---|---|
| Métriques | Le taux d'erreur augmente-t-il ? | Prometheus + Grafana |
| Logs | Quelle erreur exactement, pour quel virement ? | ELK, Loki |
| Traces | Quel service ralentit cette requête ? | Tempo, Jaeger, Zipkin |
traceId dans les logs, les traces et les exemplars des métriques.TélémétrieEnsemble des données (métriques, logs, traces, profils) émises automatiquement par une application pour la superviser à distance.
Ensemble des données (métriques, logs, traces, profils) émises automatiquement par une application pour la superviser à distance.
2Métriques12
Métrique (série temporelle)Valeur numérique mesurée à intervalles réguliers, identifiée par un nom et des labels (dimensions) ; peu coûteuse à stocker et idéale pour les tableaux de bord et alertes.
Valeur numérique mesurée à intervalles réguliers, identifiée par un nom et des labels (dimensions) ; peu coûteuse à stocker et idéale pour les tableaux de bord et alertes.
http_server_requests_seconds_count{uri="/api/v1/virements", method="POST", status="201"} 15342CounterMétrique qui ne fait qu'augmenter (ou repart à zéro au redémarrage) : nombre de requêtes, d'erreurs, de virements. On l'exploite par son taux de variation (rate).
Métrique qui ne fait qu'augmenter (ou repart à zéro au redémarrage) : nombre de requêtes, d'erreurs, de virements. On l'exploite par son taux de variation (rate).
Counter virements = Counter.builder("banque.virements")
.tag("canal", "mobile").register(registry);
virements.increment();GaugeMétrique qui monte et descend : valeur instantanée (connexions actives, taille d'une file, mémoire utilisée, lag Kafka).
Métrique qui monte et descend : valeur instantanée (connexions actives, taille d'une file, mémoire utilisée, lag Kafka).
Gauge.builder("banque.virements.en_attente", file, Queue::size).register(registry);HistogramRépartit les mesures (durées, tailles) dans des intervalles (buckets) ; permet de calculer des percentiles côté serveur et de les agréger entre instances.
Répartit les mesures (durées, tailles) dans des intervalles (buckets) ; permet de calculer des percentiles côté serveur et de les agréger entre instances.
management.metrics.distribution.percentiles-histogram.http.server.requests=trueSummaryCalcule les percentiles directement dans l'application ; précis pour une instance, mais ces percentiles ne peuvent pas être agrégés entre plusieurs pods (on préfère donc les histogrammes).
Calcule les percentiles directement dans l'application ; précis pour une instance, mais ces percentiles ne peuvent pas être agrégés entre plusieurs pods (on préfère donc les histogrammes).
Percentiles P50 / P95 / P99P95 = 95 % des requêtes sont plus rapides que cette valeur. La moyenne cache les cas lents ; le P99 montre ce que vivent les clients les moins bien servis.
P95 = 95 % des requêtes sont plus rapides que cette valeur. La moyenne cache les cas lents ; le P99 montre ce que vivent les clients les moins bien servis.
Moyenne 120 ms, P50 80 ms, P95 450 ms, P99 2,1 s
→ 1 requête sur 100 dépasse 2 s : à investiguer même si la moyenne semble bonneCardinalité des labelsNombre de combinaisons distinctes de labels d'une métrique ; un label à valeurs illimitées (id client, id virement) crée des millions de séries et fait exploser Prometheus.
Nombre de combinaisons distinctes de labels d'une métrique ; un label à valeurs illimitées (id client, id virement) crée des millions de séries et fait exploser Prometheus.
// ❌ une série par client
Counter.builder("virements").tag("clientId", id)
// ✅ dimensions bornées
Counter.builder("virements").tag("canal", "mobile").tag("statut", "OK")PrometheusBase de séries temporelles qui « scrape » (vient lire) régulièrement l'endpoint de métriques de chaque application (modèle pull), stocke les séries et évalue les règles d'alerte.
Base de séries temporelles qui « scrape » (vient lire) régulièrement l'endpoint de métriques de chaque application (modèle pull), stocke les séries et évalue les règles d'alerte.
scrape_configs:
- job_name: virements
scrape_interval: 15s
metrics_path: /actuator/prometheus
kubernetes_sd_configs: [{ role: pod }]PromQLLangage de requête de Prometheus pour calculer taux, ratios et percentiles à partir des séries.
Langage de requête de Prometheus pour calculer taux, ratios et percentiles à partir des séries.
# Requêtes par seconde (5 dernières minutes)
sum(rate(http_server_requests_seconds_count{application="virements"}[5m]))
# Taux d'erreurs 5xx
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
# Latence P95
histogram_quantile(0.95, sum by (le) (rate(http_server_requests_seconds_bucket[5m])))MicrometerFaçade de métriques (et d'observations) de Spring Boot, « le SLF4J des métriques » : on instrumente une fois, et l'export se fait vers Prometheus, Datadog, etc. Spring instrumente déjà HTTP, JDBC, Kafka, JVM.
Façade de métriques (et d'observations) de Spring Boot, « le SLF4J des métriques » : on instrumente une fois, et l'export se fait vers Prometheus, Datadog, etc. Spring instrumente déjà HTTP, JDBC, Kafka, JVM.
@Observed(name = "virement.execution") // métrique + span de trace automatiques
public void executer(Virement v) { … }
Timer.builder("operateur.appel").register(registry).record(() -> client.appeler());Endpoint /actuator/prometheusEndpoint exposé par Spring Boot Actuator (avec la dépendance micrometer-registry-prometheus) qui publie toutes les métriques au format texte lu par Prometheus.
Endpoint exposé par Spring Boot Actuator (avec la dépendance micrometer-registry-prometheus) qui publie toutes les métriques au format texte lu par Prometheus.
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
tags:
application: virementsGrafanaOutil de visualisation qui construit des tableaux de bord et des alertes à partir de Prometheus, Loki, Tempo, Elasticsearch… et permet de naviguer d'une métrique aux logs et traces correspondants.
Outil de visualisation qui construit des tableaux de bord et des alertes à partir de Prometheus, Loki, Tempo, Elasticsearch… et permet de naviguer d'une métrique aux logs et traces correspondants.
3Logs7
Log structuré (JSON)Chaque ligne de log est un objet JSON avec des champs (niveau, message, traceId, compte, durée) au lieu d'une phrase libre : recherche et filtrage fiables dans Kibana/Loki.
Chaque ligne de log est un objet JSON avec des champs (niveau, message, traceId, compte, durée) au lieu d'une phrase libre : recherche et filtrage fiables dans Kibana/Loki.
# Spring Boot 3.4+ : logs structurés intégrés
logging.structured.format.console=ecs{"@timestamp":"2026-10-14T16:33:00Z","log.level":"ERROR","message":"Paiement refusé",
"trace.id":"4bf92f3577b34da6","service.name":"virements","motif":"SOLDE_INSUFFISANT"}Niveaux de logTRACE < DEBUG < INFO < WARN < ERROR : on règle par package le niveau minimal affiché. ERROR = action requise, WARN = anomalie gérée, INFO = événements métier importants, DEBUG = diagnostic.
TRACE < DEBUG < INFO < WARN < ERROR : on règle par package le niveau minimal affiché. ERROR = action requise, WARN = anomalie gérée, INFO = événements métier importants, DEBUG = diagnostic.
logging.level.root=INFO
logging.level.sn.banque.virement=DEBUG
logging.level.org.hibernate.SQL=DEBUG # temporaire, pour voir les requêtesMDC et identifiant de corrélationLe MDC (Mapped Diagnostic Context) ajoute automatiquement des champs (traceId, utilisateur, idVirement) à toutes les lignes de log du thread courant ; un identifiant de corrélation relie toutes les lignes d'une même requête.
Le MDC (Mapped Diagnostic Context) ajoute automatiquement des champs (traceId, utilisateur, idVirement) à toutes les lignes de log du thread courant ; un identifiant de corrélation relie toutes les lignes d'une même requête.
MDC.put("virementId", v.id().toString());
try { log.info("Débit du compte source"); … }
finally { MDC.remove("virementId"); }
// Avec Micrometer Tracing, traceId et spanId sont ajoutés au MDC automatiquementPile ELK (Elasticsearch, Logstash, Kibana)Chaîne de gestion des logs : Logstash (ou Fluent Bit) collecte et transforme, Elasticsearch indexe et stocke, Kibana permet de rechercher et visualiser.
Chaîne de gestion des logs : Logstash (ou Fluent Bit) collecte et transforme, Elasticsearch indexe et stocke, Kibana permet de rechercher et visualiser.
Pods (stdout JSON) → Fluent Bit (DaemonSet) → Elasticsearch → KibanaFluentd / Fluent BitCollecteurs de logs : sur Kubernetes, un agent par nœud lit les sorties des conteneurs, enrichit avec les métadonnées (pod, namespace) et envoie vers Elasticsearch, Loki ou un SIEM.
Collecteurs de logs : sur Kubernetes, un agent par nœud lit les sorties des conteneurs, enrichit avec les métadonnées (pod, namespace) et envoie vers Elasticsearch, Loki ou un SIEM.
LokiSystème de logs de Grafana Labs qui indexe seulement les labels (pas le texte complet) : beaucoup moins coûteux qu'Elasticsearch, interrogé avec LogQL depuis Grafana.
Système de logs de Grafana Labs qui indexe seulement les labels (pas le texte complet) : beaucoup moins coûteux qu'Elasticsearch, interrogé avec LogQL depuis Grafana.
Données sensibles dans les logsLes logs sont largement accessibles et conservés longtemps : jamais de mot de passe, token, numéro de carte complet ou pièce d'identité en clair ; masquer les données personnelles.
Les logs sont largement accessibles et conservés longtemps : jamais de mot de passe, token, numéro de carte complet ou pièce d'identité en clair ; masquer les données personnelles.
log.info("Virement vers {}", Iban.masquer(iban)); // SN08 **** **** **** 45214Traces distribuées7
Trace distribuéeEnregistrement du parcours complet d'une requête à travers tous les services traversés, sous forme d'un arbre de spans chronométrés : on voit immédiatement quel service ou quelle requête SQL prend le temps.
Enregistrement du parcours complet d'une requête à travers tous les services traversés, sous forme d'un arbre de spans chronométrés : on voit immédiatement quel service ou quelle requête SQL prend le temps.
POST /virements 820 ms
├─ virements-service: valider 12 ms
├─ comptes-service: GET /comptes/42 35 ms
├─ PostgreSQL: UPDATE compte 18 ms
└─ operateur-api: POST /paiements 740 ms ← goulotSpanUnité de travail chronométrée dans une trace (un appel HTTP, une requête SQL, un message Kafka), avec début, durée, attributs, statut et lien vers son span parent.
Unité de travail chronométrée dans une trace (un appel HTTP, une requête SQL, un message Kafka), avec début, durée, attributs, statut et lien vers son span parent.
Trace ID et propagation (W3C traceparent)Identifiant unique d'une requête, transmis d'un service à l'autre dans les en-têtes HTTP (traceparent) et les en-têtes de messages Kafka, pour reconstituer la trace complète.
Identifiant unique d'une requête, transmis d'un service à l'autre dans les en-têtes HTTP (traceparent) et les en-têtes de messages Kafka, pour reconstituer la trace complète.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
└v┘└──────────── trace id ────────────┘└── span id ───┘└flags┘new ne propage rien.OpenTelemetry (OTel)Standard ouvert (CNCF) pour produire et transporter métriques, logs et traces de façon indépendante des fournisseurs ; protocole OTLP vers un collecteur, puis vers Tempo, Jaeger, Datadog…
Standard ouvert (CNCF) pour produire et transporter métriques, logs et traces de façon indépendante des fournisseurs ; protocole OTLP vers un collecteur, puis vers Tempo, Jaeger, Datadog…
# Spring Boot 4 : starter OpenTelemetry
management.otlp.tracing.endpoint=http://otel-collector:4318/v1/traces
management.tracing.sampling.probability=0.1Zipkin, Jaeger, TempoBackends de stockage et de visualisation des traces : Zipkin (historique, simple), Jaeger (CNCF), Grafana Tempo (stockage objet peu coûteux, intégré à Grafana).
Backends de stockage et de visualisation des traces : Zipkin (historique, simple), Jaeger (CNCF), Grafana Tempo (stockage objet peu coûteux, intégré à Grafana).
Micrometer TracingCouche de traçage de Spring Boot 3+ (successeur de Spring Cloud Sleuth) qui crée les spans pour HTTP, JDBC, Kafka et les exporte via OpenTelemetry ou Brave.
Couche de traçage de Spring Boot 3+ (successeur de Spring Cloud Sleuth) qui crée les spans pour HTTP, JDBC, Kafka et les exporte via OpenTelemetry ou Brave.
Échantillonnage (sampling)Ne conserver qu'une fraction des traces (ex. 10 %) pour maîtriser les coûts ; le « tail sampling » garde en priorité les traces lentes ou en erreur.
Ne conserver qu'une fraction des traces (ex. 10 %) pour maîtriser les coûts ; le « tail sampling » garde en priorité les traces lentes ou en erreur.
5Fiabilité (SRE)10
SLI (Service Level Indicator)Mesure chiffrée de la qualité de service vue par l'utilisateur : proportion de requêtes réussies, proportion de requêtes servies en moins de 300 ms, fraîcheur des données.
Mesure chiffrée de la qualité de service vue par l'utilisateur : proportion de requêtes réussies, proportion de requêtes servies en moins de 300 ms, fraîcheur des données.
SLI disponibilité = requêtes non-5xx / requêtes totales
SLI latence = requêtes < 300 ms / requêtes totalesSLO (Service Level Objective)Objectif interne fixé sur un SLI pour une période : « 99,9 % des virements aboutissent sur 30 jours glissants ». Il guide les priorités entre nouvelles fonctionnalités et fiabilité.
Objectif interne fixé sur un SLI pour une période : « 99,9 % des virements aboutissent sur 30 jours glissants ». Il guide les priorités entre nouvelles fonctionnalités et fiabilité.
SLA (Service Level Agreement)Engagement contractuel envers un client ou partenaire, assorti de pénalités ; toujours moins exigeant que le SLO interne pour garder une marge.
Engagement contractuel envers un client ou partenaire, assorti de pénalités ; toujours moins exigeant que le SLO interne pour garder une marge.
Error budget (budget d'erreur)Part d'échec tolérée par le SLO (100 % − SLO) : avec 99,9 % sur 30 jours, environ 43 minutes d'indisponibilité. Tant qu'il reste du budget, on livre ; s'il est épuisé, on priorise la fiabilité.
Part d'échec tolérée par le SLO (100 % − SLO) : avec 99,9 % sur 30 jours, environ 43 minutes d'indisponibilité. Tant qu'il reste du budget, on livre ; s'il est épuisé, on priorise la fiabilité.
Disponibilité et « nombre de 9 »Pourcentage de temps où le service répond correctement ; chaque « 9 » supplémentaire divise par dix l'indisponibilité tolérée.
Pourcentage de temps où le service répond correctement ; chaque « 9 » supplémentaire divise par dix l'indisponibilité tolérée.
| Disponibilité | Indisponibilité / mois | / an |
|---|---|---|
| 99 % | ~7 h 18 | ~3,65 jours |
| 99,9 % | ~43 min | ~8 h 45 |
| 99,95 % | ~22 min | ~4 h 22 |
| 99,99 % | ~4 min 23 | ~52 min |
Méthode USE (infrastructure)Pour chaque ressource (CPU, mémoire, disque, réseau, pool de connexions) : Utilization (taux d'occupation), Saturation (travail en attente), Errors (erreurs).
Pour chaque ressource (CPU, mémoire, disque, réseau, pool de connexions) : Utilization (taux d'occupation), Saturation (travail en attente), Errors (erreurs).
Méthode RED (services)Pour chaque service : Rate (requêtes par seconde), Errors (taux d'erreurs), Duration (latence, en percentiles). Base d'un tableau de bord de microservice.
Pour chaque service : Rate (requêtes par seconde), Errors (taux d'erreurs), Duration (latence, en percentiles). Base d'un tableau de bord de microservice.
Les quatre signaux d'or (Golden Signals)Recommandation de Google SRE : surveiller latence, trafic, erreurs et saturation ; proche de RED + saturation.
Recommandation de Google SRE : surveiller latence, trafic, erreurs et saturation ; proche de RED + saturation.
Alerting : alerter sur les symptômesAlerter sur ce que vit l'utilisateur (taux d'erreur, latence, consommation rapide du budget d'erreur) plutôt que sur des causes (CPU à 80 %) ; chaque alerte doit être actionnable et liée à un runbook.
Alerter sur ce que vit l'utilisateur (taux d'erreur, latence, consommation rapide du budget d'erreur) plutôt que sur des causes (CPU à 80 %) ; chaque alerte doit être actionnable et liée à un runbook.
- alert: TauxErreurVirementsEleve
expr: |
sum(rate(http_server_requests_seconds_count{application="virements",status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count{application="virements"}[5m])) > 0.02
for: 5m
labels: { severity: critical }
annotations:
summary: "Plus de 2 % d'erreurs 5xx sur les virements"
runbook: "https://wiki.banque.sn/runbooks/virements-erreurs"Latence de queue (tail latency)Les requêtes les plus lentes (P99, P99.9) ; dans une architecture où une page appelle 10 services, la probabilité de tomber sur au moins un appel lent devient forte.
Les requêtes les plus lentes (P99, P99.9) ; dans une architecture où une page appelle 10 services, la probabilité de tomber sur au moins un appel lent devient forte.
6Incidents6
Processus d'investigation d'un incidentDémarche structurée : 1) mesurer l'impact (qui, combien, depuis quand) ; 2) stabiliser (rollback, feature flag, scale) ; 3) corréler métriques → traces → logs ; 4) identifier ce qui a changé ; 5) corriger ; 6) communiquer ; 7) post-mortem.
Démarche structurée : 1) mesurer l'impact (qui, combien, depuis quand) ; 2) stabiliser (rollback, feature flag, scale) ; 3) corréler métriques → traces → logs ; 4) identifier ce qui a changé ; 5) corriger ; 6) communiquer ; 7) post-mortem.
RCA (Root Cause Analysis)Recherche de la cause profonde d'un incident (et pas seulement du symptôme), souvent avec la méthode des « 5 pourquoi ».
Recherche de la cause profonde d'un incident (et pas seulement du symptôme), souvent avec la méthode des « 5 pourquoi ».
Pourquoi les virements ont échoué ? → pool de connexions épuisé
Pourquoi ? → requêtes lentes qui gardent les connexions
Pourquoi ? → index supprimé par une migration
Pourquoi ? → migration non revue, pas de test de performance
Cause racine → absence de revue des migrations + pas de garde-fou en CIPost-mortem sans reproche (blameless)Document écrit après un incident : chronologie, impact, cause racine, ce qui a bien ou mal fonctionné, actions correctives avec responsables et échéances ; on analyse le système, pas les personnes.
Document écrit après un incident : chronologie, impact, cause racine, ce qui a bien ou mal fonctionné, actions correctives avec responsables et échéances ; on analyse le système, pas les personnes.
MTTD et MTTRMTTD (Mean Time To Detect) : temps moyen pour détecter un incident. MTTR (Mean Time To Recover/Restore) : temps moyen pour rétablir le service. On les réduit avec de bonnes alertes, des runbooks et des rollbacks rapides.
MTTD (Mean Time To Detect) : temps moyen pour détecter un incident. MTTR (Mean Time To Recover/Restore) : temps moyen pour rétablir le service. On les réduit avec de bonnes alertes, des runbooks et des rollbacks rapides.
RunbookProcédure écrite et testée pour diagnostiquer et traiter un type d'alerte (commandes, tableaux de bord à ouvrir, actions de mitigation, contacts d'escalade).
Procédure écrite et testée pour diagnostiquer et traiter un type d'alerte (commandes, tableaux de bord à ouvrir, actions de mitigation, contacts d'escalade).
Health checkEndpoint qui indique l'état de l'application et de ses dépendances (base, broker, disque) ; utilisé par les probes Kubernetes, les load balancers et la supervision.
Endpoint qui indique l'état de l'application et de ses dépendances (base, broker, disque) ; utilisé par les probes Kubernetes, les load balancers et la supervision.
GET /actuator/health
{ "status": "UP", "components": { "db": { "status": "UP" }, "kafka": { "status": "UP" }, "diskSpace": { "status": "UP" } } }📋 Synthèse express — Observabilité
📊 Les 3 piliers de l'observabilité
Données numériques agrégées dans le temps. Types : Counter, Gauge, Histogram, Summary. Scraping toutes les 15s.
Événements discrets avec contexte. Elasticsearch + Logstash/FluentD + Kibana. Structurés JSON recommandés.
Suivi d'une requête à travers les microservices. Trace ID propagé entre services. Permet d'identifier la latence par service.
📐 SLI / SLO / SLA
| Acronyme | Définition | Exemple |
|---|---|---|
| SLI | Service Level Indicator — la métrique mesurée | Taux de requêtes <200ms |
| SLO | Service Level Objective — l'objectif interne | 99.9% des requêtes <200ms |
| SLA | Service Level Agreement — l'engagement contractuel | 99.5% de disponibilité mensuelle |
📊 Méthodes Grafana
RED Method (services) : Rate (requêtes/sec) · Errors (taux d'erreurs) · Duration (latence P50/P95/P99)
1Réussir l'entretien3
Méthode STARStructure pour raconter une expérience : Situation (contexte), Tâche (ton objectif), Action (ce que TU as fait concrètement), Résultat (chiffré si possible) ; évite les réponses vagues.
Structure pour raconter une expérience : Situation (contexte), Tâche (ton objectif), Action (ce que TU as fait concrètement), Résultat (chiffré si possible) ; évite les réponses vagues.
S : L'API de consultation de solde dépassait 8 s aux heures de pointe (fin de mois, paie).
T : Ramener le P95 sous 500 ms sans changer d'infrastructure, en 2 semaines.
A : Profilage + traces → N+1 Hibernate sur les mouvements ; JOIN FETCH, index (compte_id, date),
cache Redis des plafonds, pagination keyset ; test de charge k6 avant/après.
R : P95 de 8 s à 320 ms, CPU base -60 %, zéro incident sur les 3 fins de mois suivantes.Pitch de présentation (3 minutes)Présentation orale courte et structurée : qui tu es, ton parcours en 2-3 étapes marquantes, ta spécialité, un résultat dont tu es fier, et pourquoi ce poste.
Présentation orale courte et structurée : qui tu es, ton parcours en 2-3 étapes marquantes, ta spécialité, un résultat dont tu es fier, et pourquoi ce poste.
1. Identité (15 s) : développeur fullstack / tech lead, X ans d'expérience
2. Parcours (60 s) : 2-3 expériences, chacune avec un résultat concret
3. Expertise (45 s) : Java/Spring, Angular, microservices, sécurité (Keycloak), IA (RAG)
4. Leadership (30 s) : mentorat, revues de code, décisions d'architecture
5. Motivation (30 s) : pourquoi ce poste, cette entreprise, ce secteurExercice de conception (system design)Exercice où l'on conçoit un système à l'oral (ex. « un service de virements instantanés ») : on évalue la démarche plus que la solution.
Exercice où l'on conçoit un système à l'oral (ex. « un service de virements instantanés ») : on évalue la démarche plus que la solution.
Démarche : 1) clarifier besoins et volumes ; 2) exigences non fonctionnelles (latence, disponibilité, sécurité, conformité) ; 3) API et modèle de données ; 4) architecture et flux ; 5) points de défaillance, cohérence, idempotence ; 6) observabilité et déploiement ; 7) compromis assumés.
2Domaine bancaire & fintech9
Core Banking SystemSystème central d'une banque qui tient les comptes, les soldes, les écritures, les produits (épargne, crédit) et la clôture journalière ; les canaux (mobile, web, agences) s'y connectent par API.
Système central d'une banque qui tient les comptes, les soldes, les écritures, les produits (épargne, crédit) et la clôture journalière ; les canaux (mobile, web, agences) s'y connectent par API.
Souvent un progiciel ancien et critique : on l'entoure d'APIs et de services modernes plutôt que de le modifier (pattern Strangler Fig, couche anti-corruption).
KYC (Know Your Customer)Obligation de vérifier l'identité d'un client et d'évaluer son profil de risque avant et pendant la relation : pièce d'identité, justificatif, selfie/biométrie, contrôle des listes de sanctions.
Obligation de vérifier l'identité d'un client et d'évaluer son profil de risque avant et pendant la relation : pièce d'identité, justificatif, selfie/biométrie, contrôle des listes de sanctions.
Côté technique : workflow de vérification (souvent BPMN), stockage chiffré des documents, traçabilité de chaque décision, niveaux de KYC qui débloquent des plafonds.
AML / LCB-FTLutte contre le blanchiment d'argent et le financement du terrorisme : surveillance des transactions (montants inhabituels, fractionnement, pays à risque), déclarations de soupçon, filtrage des listes de sanctions.
Lutte contre le blanchiment d'argent et le financement du terrorisme : surveillance des transactions (montants inhabituels, fractionnement, pays à risque), déclarations de soupçon, filtrage des listes de sanctions.
-- Exemple de règle : fractionnement (beaucoup de petits dépôts juste sous le seuil déclaratif)
SELECT client_id, COUNT(*), SUM(montant)
FROM depot
WHERE date_operation >= CURRENT_DATE - 7 AND montant BETWEEN 4500000 AND 4999999
GROUP BY client_id HAVING COUNT(*) >= 3;Réconciliation (rapprochement)Comparer deux sources censées concorder (écritures de la banque vs relevé d'un opérateur mobile money ou d'un partenaire) pour détecter écarts, doublons et opérations manquantes, puis les traiter.
Comparer deux sources censées concorder (écritures de la banque vs relevé d'un opérateur mobile money ou d'un partenaire) pour détecter écarts, doublons et opérations manquantes, puis les traiter.
Traitement batch quotidien typique : chargement des deux fichiers, appariement par référence, rapport des écarts, workflow de correction. Voir FULL OUTER JOIN.
Conformité réglementaireRespect des règles imposées au secteur : réglementation bancaire (BCEAO en zone UEMOA), protection des données personnelles (CDP au Sénégal), sécurité des données de carte (PCI DSS), obligations LCB-FT.
Respect des règles imposées au secteur : réglementation bancaire (BCEAO en zone UEMOA), protection des données personnelles (CDP au Sénégal), sécurité des données de carte (PCI DSS), obligations LCB-FT.
Impacts techniques : chiffrement, journal d'audit immuable, conservation et purge des données, contrôle des accès, hébergement et sous-traitance encadrés.
Comptabilité en partie doubleChaque opération génère au moins deux écritures de même montant, un débit et un crédit, dont la somme est toujours nulle ; c'est la base de toute tenue de comptes fiable.
Chaque opération génère au moins deux écritures de même montant, un débit et un crédit, dont la somme est toujours nulle ; c'est la base de toute tenue de comptes fiable.
-- Virement de 50 000 XOF : 2 écritures dans la même transaction
INSERT INTO ecriture(operation_id, compte, sens, montant) VALUES
(:op, 'SN01-CLIENT-A', 'DEBIT', 50000),
(:op, 'SN01-CLIENT-B', 'CREDIT', 50000);
-- Contrôle d'intégrité : pour chaque opération, somme des débits = somme des créditsArrondis monétaires (BigDecimal et RoundingMode)Les montants se calculent en BigDecimal avec une échelle et un mode d'arrondi explicites ; HALF_EVEN (arrondi bancaire) limite les biais cumulés sur de grands volumes.
Les montants se calculent en BigDecimal avec une échelle et un mode d'arrondi explicites ; HALF_EVEN (arrondi bancaire) limite les biais cumulés sur de grands volumes.
BigDecimal frais = montant.multiply(new BigDecimal("0.015")).setScale(0, RoundingMode.HALF_EVEN); // XOF : pas de centimes
new BigDecimal(0.1); // ❌ 0.1000000000000000055511151231257827…
new BigDecimal("0.1"); // ✅ valeur exacteMobile moneyPortefeuilles électroniques liés à un numéro de téléphone (Orange Money, Wave, Free Money) très utilisés en Afrique de l'Ouest ; une banque s'y interface par API pour les dépôts, retraits et transferts wallet ↔ compte.
Portefeuilles électroniques liés à un numéro de téléphone (Orange Money, Wave, Free Money) très utilisés en Afrique de l'Ouest ; une banque s'y interface par API pour les dépôts, retraits et transferts wallet ↔ compte.
Enjeux techniques : callbacks asynchrones (webhooks) à sécuriser, idempotence, statuts intermédiaires, réconciliation quotidienne, gestion des timeouts des opérateurs.
ISO 20022Norme internationale de messages financiers (XML structuré : pacs.008 pour un virement, camt.053 pour un relevé) qui remplace progressivement les anciens formats SWIFT MT.
Norme internationale de messages financiers (XML structuré : pacs.008 pour un virement, camt.053 pour un relevé) qui remplace progressivement les anciens formats SWIFT MT.
3Principes de conception11
DRY, KISS, YAGNIDRY (Don't Repeat Yourself) : une connaissance à un seul endroit. KISS (Keep It Simple) : la solution la plus simple qui marche. YAGNI (You Aren't Gonna Need It) : ne pas construire ce qui n'est pas demandé.
DRY (Don't Repeat Yourself) : une connaissance à un seul endroit. KISS (Keep It Simple) : la solution la plus simple qui marche. YAGNI (You Aren't Gonna Need It) : ne pas construire ce qui n'est pas demandé.
Design patternSolution éprouvée et nommée à un problème de conception récurrent (catalogue du « Gang of Four ») ; donne un vocabulaire commun à l'équipe.
Solution éprouvée et nommée à un problème de conception récurrent (catalogue du « Gang of Four ») ; donne un vocabulaire commun à l'équipe.
SingletonUne seule instance d'une classe dans l'application. En Spring, les beans sont singletons par défaut : on n'écrit plus le pattern à la main (constructeur privé + getInstance).
Une seule instance d'une classe dans l'application. En Spring, les beans sont singletons par défaut : on n'écrit plus le pattern à la main (constructeur privé + getInstance).
static) est un état global difficile à tester ; laisser Spring gérer le cycle de vie.FactoryDéléguer la création d'objets à une méthode ou une classe dédiée, qui choisit l'implémentation selon le contexte ; l'appelant ne fait plus de new sur des classes concrètes.
Déléguer la création d'objets à une méthode ou une classe dédiée, qui choisit l'implémentation selon le contexte ; l'appelant ne fait plus de new sur des classes concrètes.
@Component
class PaymentGatewayFactory {
private final Map<String, PaymentGateway> gateways; // Spring injecte toutes les implémentations
PaymentGatewayFactory(List<PaymentGateway> liste) {
this.gateways = liste.stream().collect(Collectors.toMap(PaymentGateway::code, g -> g));
}
PaymentGateway pour(String operateur) {
return Optional.ofNullable(gateways.get(operateur)).orElseThrow(() -> new OperateurInconnuException(operateur));
}
}BuilderConstruire pas à pas un objet complexe avec des méthodes nommées, au lieu d'un constructeur à 10 paramètres ; utilisé partout dans Spring (RestClient.builder(), ResponseEntity.ok()...).
Construire pas à pas un objet complexe avec des méthodes nommées, au lieu d'un constructeur à 10 paramètres ; utilisé partout dans Spring (RestClient.builder(), ResponseEntity.ok()...).
Releve r = Releve.builder()
.compte("SN01…")
.periode(YearMonth.of(2026, 9))
.format(Format.PDF)
.avecDetailFrais(true)
.build();StrategyEncapsuler des algorithmes interchangeables derrière une même interface et choisir lequel utiliser à l'exécution (calcul de frais par type d'opération, scoring par produit).
Encapsuler des algorithmes interchangeables derrière une même interface et choisir lequel utiliser à l'exécution (calcul de frais par type d'opération, scoring par produit).
interface CalculFrais { BigDecimal calculer(Operation op); }
class FraisVirementInterne implements CalculFrais { … }
class FraisMobileMoney implements CalculFrais { … }
// le service reçoit la stratégie adaptée, sans if/else en cascadeObserverUn sujet notifie automatiquement ses abonnés quand son état change ; base des événements Spring, de RxJS et du pub/sub.
Un sujet notifie automatiquement ses abonnés quand son état change ; base des événements Spring, de RxJS et du pub/sub.
public record CompteOuvert(Long compteId) {}
publisher.publishEvent(new CompteOuvert(id)); // émetteur
@EventListener
void envoyerKitBienvenue(CompteOuvert e) { … } // abonné découplé
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void indexer(CompteOuvert e) { … } // seulement si la transaction a été validéeTemplate MethodUne classe abstraite fixe les étapes d'un algorithme et laisse les sous-classes implémenter certaines étapes (voir Classe abstraite). JdbcTemplate et RestTemplate en sont des variantes par callback.
Une classe abstraite fixe les étapes d'un algorithme et laisse les sous-classes implémenter certaines étapes (voir Classe abstraite). JdbcTemplate et RestTemplate en sont des variantes par callback.
DecoratorEnvelopper un objet pour lui ajouter un comportement (cache, log, retry) sans modifier sa classe, en respectant la même interface ; les proxies Spring en sont une application.
Envelopper un objet pour lui ajouter un comportement (cache, log, retry) sans modifier sa classe, en respectant la même interface ; les proxies Spring en sont une application.
ADR (Architecture Decision Record)Court document versionné avec le code qui trace une décision d'architecture : contexte, options étudiées, décision, conséquences. Évite de rejouer les mêmes débats et aide les nouveaux arrivants.
Court document versionné avec le code qui trace une décision d'architecture : contexte, options étudiées, décision, conséquences. Évite de rejouer les mêmes débats et aide les nouveaux arrivants.
ADR-007 : Kafka pour les événements de virement
Statut : Accepté (2026-03-12)
Contexte : 6 services doivent réagir aux virements ; besoin de relecture et d'audit.
Options : RabbitMQ, Kafka, appels REST synchrones.
Décision : Kafka (rétention 7 j, partition par compte), Outbox côté producteur.
Conséquences : exploitation d'un cluster Kafka, consommateurs idempotents obligatoires.Dette techniqueCoût futur créé par des raccourcis (code dupliqué, absence de tests, dépendances obsolètes) : comme une dette financière, elle génère des « intérêts » (lenteur, bugs) tant qu'on ne la rembourse pas.
Coût futur créé par des raccourcis (code dupliqué, absence de tests, dépendances obsolètes) : comme une dette financière, elle génère des « intérêts » (lenteur, bugs) tant qu'on ne la rembourse pas.
4Leadership technique5
MentoratAccompagner la montée en compétence des développeurs : objectifs individuels, revues de code pédagogiques, pair programming, partage de lectures, autonomie progressive.
Accompagner la montée en compétence des développeurs : objectifs individuels, revues de code pédagogiques, pair programming, partage de lectures, autonomie progressive.
Pair programmingDeux développeurs sur le même code : un « pilote » écrit, un « navigateur » relit et réfléchit à la suite, en alternant. Efficace sur les sujets complexes et pour former.
Deux développeurs sur le même code : un « pilote » écrit, un « navigateur » relit et réfléchit à la suite, en alternant. Efficace sur les sujets complexes et pour former.
Definition of DoneListe partagée des critères qu'une tâche doit remplir pour être considérée terminée : code revu, tests verts, couverture, documentation, déployé en recette, critères d'acceptation validés.
Liste partagée des critères qu'une tâche doit remplir pour être considérée terminée : code revu, tests verts, couverture, documentation, déployé en recette, critères d'acceptation validés.
Rituels agiles (Scrum)Cadre itératif : sprint (1 à 4 semaines), planning, daily (point quotidien de synchronisation), revue (démo au métier), rétrospective (amélioration du fonctionnement de l'équipe).
Cadre itératif : sprint (1 à 4 semaines), planning, daily (point quotidien de synchronisation), revue (démo au métier), rétrospective (amélioration du fonctionnement de l'équipe).
Gérer un désaccord technique (POC)Argumenter avec des faits (mesures, impacts métier, risques), écouter les contre-arguments, et si le désaccord persiste, trancher par un POC court et mesurable ; documenter la décision (ADR) et s'engager collectivement (« disagree and commit »).
Argumenter avec des faits (mesures, impacts métier, risques), écouter les contre-arguments, et si le désaccord persiste, trancher par un POC court et mesurable ; documenter la décision (ADR) et s'engager collectivement (« disagree and commit »).
📋 Synthèse express — Mémo
⚡ Questions pièges fréquentes
== : comparaison de référence. equals() : comparaison de valeur. Toujours implémenter hashCode() si on override equals().
REQUIRED (défaut) = rejoint la transaction existante. REQUIRES_NEW = nouvelle transaction. Rollback par défaut sur RuntimeException uniquement.
String est immutable → concaténation dans une boucle = N objets créés. StringBuilder est mutable → O(n) vs O(n²). StringBuffer = synchronized.
200 OK · 201 Created · 204 No Content · 400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found · 409 Conflict · 500 Internal Error
🎯 Présentation STAR — Template
"Sur le projet X, j'ai dû [Situation]. Ma mission était [Task]. J'ai [Actions concrètes]. Résultat : [Métriques chiffrées]."
📋 Checklist avant entretien
- Java 21/25 nouveautés
- Spring Boot 3.5/4.0
- Architecture microservices
- SOLID + Design Patterns
- API CRUD complète
- Diagnostiquer N+1
- Circuit Breaker setup
- JWT OAuth2 flow
- Présentation STAR préparée
- Questions à poser (roadmap, stack, équipe)
- Finance/banking: Core Banking, KYC, risk
- Leadership : mentorat, code review
🏦 Contexte Finance/Banking (Baobab)
❓ Questions à poser au recruteur
- Quelle taille du système ? Nb de microservices ?
- Monolith → micro migration en cours ?
- Spring Boot 3.x ou encore 2.x ?
- Taille de l'équipe tech ?
- Rituels agiles (sprints de combien) ?
- Code review obligatoire ?
- Dette technique principale ?
💼 Exercices pratiques types
Concevoir un endpoint POST /api/virements avec validation (@Valid), vérification du solde, transaction ACID, audit trail, réponse 201 avec idempotency-key. Mentionner BigDecimal.
Détecter via les logs Hibernate (show_sql), corriger avec @EntityGraph ou JOIN FETCH, valider avec les métriques de requêtes. Expliquer la différence LAZY/EAGER.
Configurer states CLOSED/OPEN/HALF-OPEN, seuils d'erreur, timeout, fallback. Montrer l'annotation @CircuitBreaker(name="payment", fallbackMethod="fallback").
1. Client → /login → JWT signé. 2. Client envoie Bearer token. 3. Spring Security valide signature. 4. Claims extraits → SecurityContext. Mentionner refresh token et expiration.
EXPLAIN ANALYZE → identifier seq scan → ajouter index composite → vérifier statistiques → VACUUM ANALYZE → mesurer amélioration. Mentionner index partiel pour tables volumineuses.
🗣️ Questions comportementales — Réponses STAR
S : Performance dégradée en production (temps de réponse 8s). T : Réduire à moins de 500ms. A : Profiling → requête N+1 identifiée → JOIN FETCH + cache Redis. R : 95% de réduction, 0 incident en 3 mois.
J'expose mes arguments avec des données (benchmarks, impact métier). J'écoute les contre-arguments. Si toujours en désaccord, je propose un POC pour décider objectivement. Je documente la décision finale.
Code reviews bienveillantes avec explications (pas juste "à corriger"). Sessions de pair-programming sur les sujets complexes. Documentation des décisions architecturales (ADR). Encourager les junior à poser des questions.
Choisir un pattern trop complexe (CQRS) pour un petit projet. Résultat : délais + frustration équipe. Leçon : toujours choisir la solution la plus simple qui répond au besoin (KISS/YAGNI).
✅ Checklist complète avant entretien
| Quand | Action | Fait ? |
|---|---|---|
| J-7 | Réviser Java 21/25 (Virtual Threads, Records, Sealed Classes) | ☐ |
| J-7 | Réviser Spring Boot 3.5 → 4.x (Jackson 3, versioning d'API, JSpecify, virtual threads) | ☐ |
| J-5 | Pratiquer conception API REST + sécurité JWT | ☐ |
| J-5 | Revoir Angular 17 → 22 (Signals, @defer, zoneless, Signal Forms) | ☐ |
| J-3 | Préparer 3 réponses STAR (performance, conflit, leadership) | ☐ |
| J-3 | Réviser SOLID + Design Patterns (Factory, Strategy, Observer) | ☐ |
| J-2 | Lire les docs Baobab / fintech / Core Banking | ☐ |
| J-1 | Faire les exercices A à E de cette fiche | ☐ |
| J-1 | Préparer 5 questions à poser au recruteur | ☐ |
| J-1 | Réviser microservices : Circuit Breaker, Kafka, Idempotence | ☐ |
| Jour J | Présentation 3 min : parcours + spécialité + valeur ajoutée | ☐ |
| Jour J | Environnement de code prêt (IDE, Java 21 installé) | ☐ |