JAVA SENIOR
MASTERY GUIDE
De Junior/Mid à Senior — JPA • DDD • Patterns • Architecture
Edition Professionnelle 2024
CHAPITRE 1 — JPA & Hibernate : Maîtrise
Profonde
Ce chapitre couvre les mécanismes internes de JPA et Hibernate. Comprendre ces fondations
est ce qui sépare un dev qui 'fait marcher' JPA d'un dev qui maîtrise réellement la couche de
persistance.
1.1 — Le Cycle de Vie d'une Entité
Hibernate maintient un PersistenceContext qui trace chaque entité selon quatre états. Ignorer
ces états est la cause numéro 1 des bugs JPA silencieux.
État Description Comportement Hibernate
TRANSIENT Créé avec new, jamais persisté Totalement ignoré
MANAGED Attaché au PersistenceContext Dirty checking — modifications
actif auto-flushées
DETACHED Session fermée ou evict() Modifications ignorées
appelé silencieusement
REMOVED [Link]() appelé DELETE généré au prochain
flush
// ── Transitions d'état ─────────────────────────────────────────
Product p = new Product("iPhone", price); // TRANSIENT
[Link](p); // TRANSIENT → MANAGED
[Link](p); // MANAGED → DETACHED
// session fermée → tous les objets deviennent DETACHED
// ⚠️ merge() retourne une NOUVELLE instance MANAGED
// l'objet passé reste DETACHED — toujours utiliser le retour
Product managed = [Link](p); // DETACHED → MANAGED (nouvelle
instance)
[Link](managed); // MANAGED → REMOVED
// → DELETE exécuté au flush suivant
⚠️ Piège : merge() ne modifie PAS l'objet passé en paramètre. Il retourne une nouvelle
instance managed. Toujours travailler avec l'objet retourné.
1.2 — Dirty Checking : Hibernate Détecte les
Changements
Hibernate prend un snapshot de chaque entité managed au chargement. Au flush, il compare et
génère les UPDATE nécessaires. Comprendre ça évite les save() inutiles et explique les
UPDATE inattendus.
✅ BON CODE
// ✅ PAS besoin de save() — dirty checking automatique
@Transactional
public void updatePrice(Long id, BigDecimal newPrice) {
Product product = [Link](id).orElseThrow();
// product est MANAGED — Hibernate surveille chaque champ
[Link]([Link](newPrice, [Link]));
// Au commit : Hibernate détecte le changement → UPDATE automatique
// [Link](product) est INUTILE ici
}
1.3 — Le Problème N+1 : Le Bug Silencieux
Le N+1 est le problème de performance JPA le plus répandu. Il ne se voit pas en
développement et explose en production sous charge. Règle absolue : jamais de query dans
une boucle.
❌ MAUVAIS CODE
// ❌ N+1 — génère 1 + N queries (1 pour la liste + 1 par order)
List<Order> orders = [Link](); // 1 query
for (Order order : orders) {
// ← UNE query par order pour charger le customer (LAZY)
[Link]([Link]().getName());
}
// 1000 orders = 1001 queries → timeout, connexions épuisées
✅ BON CODE
// ✅ JOIN FETCH — tout en une seule query
@Query("SELECT o FROM Order o JOIN FETCH [Link] WHERE [Link] = :status")
List<Order> findAllWithCustomer(@Param("status") OrderStatus status);
// ✅ EntityGraph — plus flexible, configurable à l'appel
@EntityGraph(attributePaths = {"customer", "items", "[Link]"})
List<Order> findByStatus(OrderStatus status);
// ✅ @BatchSize — pour les collections volumineuses
@BatchSize(size = 50) // charge 50 entités par query au lieu de 1
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
🔍 Détection : Active [Link]-sql=true et hibernate.generate_statistics=true. Si tu
vois la même requête répétée en boucle dans les logs → N+1 identifié à corriger.
1.4 — LAZY vs EAGER : La Règle Absolue
❌ MAUVAIS CODE
// ❌ EAGER sur une collection = catastrophe de performance
@OneToMany(fetch = [Link]) // charge TOUS les items à chaque
findAll()
private List<OrderItem> items; // 1000 orders = 1000 × N items chargés
inutilement
// ❌ EAGER sur @ManyToOne = jointure systématique même si inutile
@ManyToOne(fetch = [Link])
private Category category;
✅ BON CODE
// ✅ LAZY partout — JOIN FETCH explicite uniquement quand nécessaire
@ManyToOne(fetch = [Link])
private Category category;
@OneToMany(mappedBy = "product", fetch = [Link])
private Set<Tag> tags;
// Chargement ciblé dans la query qui en a besoin
@Query("SELECT p FROM Product p JOIN FETCH [Link] WHERE [Link] = :id")
Optional<Product> findWithCategory(@Param("id") Long id);
📐 LAZY par défaut sur TOUTES les relations. JOIN FETCH uniquement dans
les queries explicites qui en ont besoin.
1.5 — @Transactional : Les 3 Pièges Critiques
Piège 1 — Self-invocation (proxy Spring bypassé)
@Transactional fonctionne via un proxy AOP. Appeler une méthode @Transactional depuis la
même classe court-circuite le proxy → transaction inexistante.
❌ MAUVAIS CODE
// ❌ Self-invocation — @Transactional sur createOrder ignorée
@Service
public class OrderService {
public void processAll(List<Long> ids) {
[Link](id -> createOrder(id)); // appel interne → bypasse le proxy
AOP
}
@Transactional // ← IGNORÉE — pas de proxy impliqué
public void createOrder(Long id) { /* croit être transactionnel... ne l'est
pas */ }
}
✅ BON CODE
// ✅ Extraire dans un service dédié → proxy actif
@Service @RequiredArgsConstructor
public class OrderService {
private final OrderCreationService creationService; // injection du service
dédié
public void processAll(List<Long> ids) {
[Link](id -> [Link](id)); // proxy actif ✅
}
}
@Service
public class OrderCreationService {
@Transactional // ✅ transaction effective via proxy
public void createOrder(Long id) { /* ... */ }
}
Piège 2 — @Async + @Transactional sur la même méthode
@Transactional stocke la transaction dans un ThreadLocal du thread courant. @Async exécute
sur un autre thread → ThreadLocal différent → pas de transaction.
❌ MAUVAIS CODE
// ❌ BUG CRITIQUE — @Async lance un nouveau thread
// ce thread n'a PAS la transaction du ThreadLocal du thread HTTP
@Async
@Transactional(rollbackFor = [Link]) // ← inefficace sur ce thread
public CompletableFuture<Product> create(CreateProductDto dto) {
// Croit être transactionnel... ne l'est pas
// Les exceptions sont capturées dans le CompletableFuture — silencieuses
}
✅ BON CODE
// ✅ La transaction est DANS le code async, gérée par un service synchrone
@Async
public CompletableFuture<Product> create(CreateProductDto dto) {
Product saved = [Link](dto); // méthode
@Transactional séparée
return [Link](saved);
}
@Transactional // sur méthode SYNCHRONE — transaction dans le bon thread
public Product doCreate(CreateProductDto dto) { /* ... */ }
Piège 3 — Checked exceptions sans rollbackFor
❌ MAUVAIS CODE
// ❌ checked exception → PAS de rollback par défaut Spring
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) throws
InsufficientFundsException {
[Link](from, amount); // executé
// InsufficientFundsException → le débit EST committé sans le crédit !
[Link](to, amount);
}
✅ BON CODE
// ✅ rollbackFor explicite pour les checked exceptions
@Transactional(rollbackFor = [Link])
public void transfer(Long from, Long to, BigDecimal amount) throws
InsufficientFundsException {
[Link](from, amount);
[Link](to, amount); // rollback si InsufficientFundsException
✅
}
1.6 — equals() / hashCode() sur les Entités
❌ MAUVAIS CODE
// ❌ @Data sur une entité — equals/hashCode basé sur l'id
@Data @Entity
public class Product {
@Id @GeneratedValue
private Long id; // null avant persist !
}
// Le bug :
Set<Product> set = new HashSet<>();
Product p = new Product();
[Link](p); // hashCode calculé avec id = null
[Link](p); // id = 42 assigné par la BDD
[Link](p); // ← FALSE ! hashCode a changé → objet perdu dans le
Set
✅ BON CODE
// ✅ equals/hashCode sur un identifiant métier STABLE (pas l'id technique)
@Entity @Getter
@NoArgsConstructor(access = [Link])
public class Product {
@Id @GeneratedValue
private Long id;
@Column(unique = true, nullable = false)
private String sku; // identifiant métier stable avant et après persist
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Product other)) return false;
return sku != null && [Link]([Link]);
}
@Override
public int hashCode() { return getClass().hashCode(); } // stable même avec
id null
}
CHAPITRE 2 — DDD Pragmatique : Modèle
Riche
Le DDD n'est pas une architecture, c'est une façon de penser. L'objectif central : les règles
métier vivent dans le domaine, pas dans les services. Le code parle le même langage que le
métier.
2.1 — Modèle Anémique vs Modèle Riche
❌ MAUVAIS CODE
// ❌ MODÈLE ANÉMIQUE — entité = sac de données, logique dans le service
@Entity public class Order {
private String status; // 'CREATED', 'CONFIRMED'... magic strings
private BigDecimal total;
// getters + setters publics sur TOUT
}
@Service public class OrderService {
public void confirm(Long id) {
Order o = [Link](id).orElseThrow();
if (![Link]().equals("CREATED")) // règle métier dans le service
throw new BusinessException("...");
[Link]("CONFIRMED"); // mutation directe depuis n'importe
où
[Link]([Link]()); // éparpillée dans tous les
services
}
}
✅ BON CODE
// ✅ MODÈLE RICHE — l'entité se défend elle-même
@Entity @Getter
@NoArgsConstructor(access = [Link])
public class Order {
@Id @GeneratedValue private Long id;
@Enumerated([Link])
private OrderStatus status; // enum, pas de magic string
private LocalDateTime confirmedAt;
@OneToMany(mappedBy = "order", cascade = [Link])
private List<OrderItem> items = new ArrayList<>();
// Factory method — seul point d'entrée valide
public static Order create(ShippingAddress address) {
[Link](address, "Adresse obligatoire");
Order o = new Order();
[Link] = [Link];
[Link] = address;
return o;
}
// Comportement métier avec invariants protégés
public void confirm() {
if ([Link]()) throw new BusinessException("Commande vide");
if (status != [Link]) throw new BusinessException("Déjà
traité: " + status);
[Link] = [Link];
[Link] = [Link]();
}
public void addItem(Product product, int quantity) {
if (status != [Link]) throw new BusinessException("Non
modifiable: " + status);
if (quantity <= 0) throw new BusinessException("Quantité invalide");
[Link](new OrderItem(this, product, quantity));
}
}
2.2 — Value Objects : Remplacer les Primitives
Un Value Object est défini par sa valeur, pas son identité. Immuable, sans id. Il remplace les
primitives ambiguës et encapsule leur validation.
❌ MAUVAIS CODE
// ❌ Primitive Obsession — perte du sens métier
public class Product {
private BigDecimal price; // négatif autorisé ? quelle devise ?
private String currency; // couplé à price mais séparé
private String email; // validée ? normalized ?
private int weight; // grammes ? kg ? lbs ?
}
✅ BON CODE
// ✅ Value Objects — sens, validation et comportement encapsulés
@Embeddable
public final class Money {
private BigDecimal amount;
@Enumerated([Link])
private Currency currency;
protected Money() {} // pour JPA
public static Money of(BigDecimal amount, Currency currency) {
if ([Link]([Link]) < 0)
throw new BusinessException("Montant négatif interdit");
[Link](currency, "Devise obligatoire");
Money m = new Money(); [Link] = amount; [Link] = currency;
return m;
}
public Money add(Money other) {
if ()
throw new BusinessException("Devises incompatibles: " + currency + "
vs " + [Link]);
return [Link]([Link]([Link]), currency);
}
public boolean isGreaterThan(Money other) { return
[Link]([Link]) > 0; }
// equals/hashCode par valeur — obligatoire pour un Value Object
}
2.3 — Builder + Aggregate : Validation Fail Fast
L'objectif : valider les dépendances externes AVANT de construire l'objet. Inutile d'instancier un
Aggregate si une dépendance est déjà invalide. Ordre strict : primitives → règles locales → I/O
externe → règles croisées.
✅ BON CODE
public static class Builder {
private final String name; // obligatoires → dans le constructeur du
Builder
private final Money price; // le compilateur FORCE leur fourniture
private final Long categoryId;
private String description; // optionnel → méthode fluente
private Builder(String name, Money price, Long categoryId) {
[Link] = name; [Link] = price; [Link] = categoryId;
}
public Builder description(String d) { [Link] = d; return this; }
// Dépendances injectées au build() — pas dans le constructeur
public Product build(CategoryRepository categoryRepo, SkuValidator
skuValidator) {
// NIVEAU 1 — Validation primitive SANS I/O (gratuit, immédiat)
[Link](name, "name requis");
if ([Link]()) throw new BusinessException("name vide");
[Link](price, "price requis");
// NIVEAU 2 — Unicité / existence (I/O léger)
if ([Link](sku))
throw new BusinessException("SKU déjà utilisé: " + sku);
// NIVEAU 3 — Dépendances avec règles croisées (I/O + logique)
Category category = [Link](categoryId)
.orElseThrow(() -> new BusinessException("Category introuvable: " +
categoryId));
if ([Link]() && sku == null)
throw new BusinessException("Catégorie '" + [Link]() + "'
nécessite un SKU");
// CONSTRUCTION — seulement si tout est valide
return new Product(this, category);
}
}
// Utilisation — impossible d'oublier les champs obligatoires
Product p = [Link]("iPhone 15", [Link]([Link](999), EUR),
categoryId)
.description("Dernier modèle")
.build(categoryRepository, skuValidator);
2.4 — Domain Events : Découpler les Effets de Bord
Les Domain Events permettent de séparer la logique principale des effets secondaires (emails,
stock, audit). L'entité publie un événement après commit. Les handlers réagissent
indépendamment.
✅ BON CODE
// Event record — immuable
public record OrderConfirmed(Long orderId, String customerEmail, LocalDateTime
at) {}
// Publication dans l'Aggregate
@Entity
public class Order {
@Transient // non persisté en BDD
private final List<Object> domainEvents = new ArrayList<>();
public void confirm() {
// logique métier...
[Link] = [Link];
// event publié APRÈS la mutation — état cohérent
[Link](new OrderConfirmed(id, [Link](),
[Link]()));
}
}
// Handler AFTER_COMMIT — réagit seulement si la transaction a réussi
@Component @RequiredArgsConstructor
public class OrderConfirmedHandler {
private final EmailService emailService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(OrderConfirmed event) {
[Link]([Link]());
// Si l'email échoue → l'order reste CONFIRMÉ (transaction principale
déjà commitée)
}
}
CHAPITRE 3 — DTOs : 1 Use Case = 1 DTO
Le DTO universel est l'anti-pattern le plus répandu. Il génère des bugs silencieux, rend la
validation impossible et crée un contrat ambigu. La règle est simple : un use case = un DTO
dédié.
3.1 — Architecture des DTOs
src/product/dto/
├── inputs/ ← ce qu'on REÇOIT
│ ├── [Link] ← use case création
│ ├── [Link] ← use case modification (PATCH)
│ ├── [Link] ← use case publication
│ └── [Link] ← use case changement de prix
├── outputs/ ← ce qu'on RETOURNE
│ ├── [Link] ← liste (données minimales)
│ ├── [Link] ← page détail (données complètes)
│ └── [Link] ← back-office (données sensibles)
└── shared/ ← fragments réutilisables (jamais utilisés
seuls)
├── [Link]
└── [Link]
3.2 — Inputs : Contrats Clairs par Use Case
✅ BON CODE
// ✅ CreateProductDto — seuls les champs nécessaires à la création
@Getter @NoArgsConstructor
public class CreateProductDto {
@NotBlank(message = "Le nom est obligatoire")
@Size(max = 255)
private String name;
@NotNull(message = "Le prix est obligatoire")
@DecimalMin(value = "0.01", message = "Le prix doit être > 0")
private BigDecimal price;
@NotNull(message = "La catégorie est obligatoire")
private Long categoryId;
private String description; // optionnel
// PAS de id → assigné par la BDD
// PAS de status → imposé par l'Aggregate
// PAS de createdAt → @CreationTimestamp
}
// ✅ PublishProductDto — use case dédié, intention claire
@Getter @NoArgsConstructor
public class PublishProductDto {
@FutureOrPresent
private LocalDate publishAt; // null = publication immédiate
private String publishNote;
}
3.3 — Patchable<T> : Résoudre l'Ambiguïté PATCH
Sans Patchable, il est impossible de distinguer 'champ non envoyé' de 'champ volontairement
mis à null'. C'est la cause de nombreux bugs silencieux dans les endpoints PATCH.
✅ BON CODE
// Le wrapper Patchable<T>
public final class Patchable<T> {
private final T value;
private final boolean provided;
private Patchable(T value, boolean provided) { [Link] = value;
[Link] = provided; }
public static <T> Patchable<T> of(T value) { return new Patchable<>(value,
true); }
public static <T> Patchable<T> absent() { return new Patchable<>(null,
false); }
public boolean isProvided() { return provided; }
public T getValue() { return value; }
}
// UpdateProductDto — PATCH sémantique
@Getter @NoArgsConstructor
public class UpdateProductDto {
private Patchable<String> name = [Link]();
private Patchable<BigDecimal> price = [Link]();
private Patchable<String> description = [Link]();
// categoryId absent → on ne change jamais la catégorie via update
}
// Dans le service — logique cristalline, aucune ambiguïté
@Transactional
public ProductView update(Long id, UpdateProductDto dto) {
Product product = [Link](id).orElseThrow();
if ([Link]().isProvided())
[Link]([Link]().getValue());
if ([Link]().isProvided())
[Link]([Link]([Link]().getValue(), EUR));
if ([Link]().isProvided())
[Link]([Link]().getValue());
return [Link]([Link](product));
}
3.4 — Mapper : Réutilisation Sans Coupler les DTOs
📐 Ne jamais hériter un DTO d'un autre. La réutilisation se fait dans le Mapper,
pas dans la hiérarchie des types.
✅ BON CODE
@Component
public class ProductMapper {
public ProductSummaryView toSummary(Product p) {
return [Link]()
.id([Link]()).name([Link]())
.price([Link]().getAmount())
.categoryName([Link]().getName())
.build();
}
public ProductDetailView toDetail(Product p) {
return [Link]()
// champs communs — réutilisation via code, pas héritage
.id([Link]()).name([Link]())
.price([Link]().getAmount())
.categoryName([Link]().getName())
// champs supplémentaires propres au détail
.description([Link]())
.tags([Link]().stream().map(Tag::getName).toList())
.createdAt([Link]())
.build();
}
}
CHAPITRE 4 — Design Patterns Senior
4.1 — Strategy Pattern : Remplacer les if/else
❌ MAUVAIS CODE
// ❌ if/else ingérable — ajouter un type = modifier cette méthode
public BigDecimal calculateDiscount(Order order, String type) {
if ([Link]("PERCENTAGE")) return [Link]().multiply(new
BigDecimal("0.10"));
else if ([Link]("FIXED")) return new BigDecimal("50.00");
else if ([Link]("SHIPPING")) return [Link]();
// Chaque nouveau type = modifier ce fichier → violation Open/Closed
return [Link];
}
✅ BON CODE
// ✅ Strategy Pattern — ouvert à l'extension, fermé à la modification
public interface DiscountStrategy {
BigDecimal calculate(Order order);
DiscountType getType();
}
@Component
public class PercentageDiscount implements DiscountStrategy {
@Override public BigDecimal calculate(Order order) {
return [Link]().multiply(new BigDecimal("0.10"));
}
@Override public DiscountType getType() { return [Link]; }
}
// Registry auto-construite par Spring
@Service
public class DiscountService {
private final Map<DiscountType, DiscountStrategy> strategies;
public DiscountService(List<DiscountStrategy> list) {
[Link] = [Link]()
.collect([Link](DiscountStrategy::getType, s -> s));
}
public BigDecimal apply(Order order, DiscountType type) {
return [Link]([Link](type))
.map(s -> [Link](order))
.orElseThrow(() -> new BusinessException("Stratégie inconnue: " +
type));
// Ajouter un type = créer une classe. RIEN d'autre à modifier.
}
}
4.2 — Specification Pattern : Requêtes Composables
✅ BON CODE
// ✅ Specifications réutilisables et composables
public class ProductSpecs {
public static Specification<Product> hasCategory(Long categoryId) {
return (root, query, cb) -> [Link]([Link]("category").get("id"),
categoryId);
}
public static Specification<Product> priceBetween(BigDecimal min, BigDecimal
max) {
return (root, query, cb) -> [Link]([Link]("price").get("amount"),
min, max);
}
public static Specification<Product> isActive() {
return (root, query, cb) -> [Link]([Link]("status"),
[Link]);
}
}
// Composition — lisible, testable indépendamment
Specification<Product> spec = [Link]()
.and([Link](categoryId))
.and([Link](min, max));
List<Product> products = [Link](spec);
4.3 — Dependency Inversion : Domaine Indépendant
✅ BON CODE
// ✅ Interface dans le DOMAINE — pas de dépendance sur l'infrastructure
public interface NotificationPort {
void notify(String recipient, String subject, String body);
}
// Implémentation dans l'INFRASTRUCTURE
@Component @RequiredArgsConstructor
public class EmailNotificationAdapter implements NotificationPort {
private final JavaMailSender mailSender;
@Override
public void notify(String recipient, String subject, String body) {
SimpleMailMessage msg = new SimpleMailMessage();
[Link](recipient); [Link](subject); [Link](body);
[Link](msg);
}
}
// Le service du domaine dépend uniquement de NotificationPort
// → facilement testable avec un mock
// → remplaçable par SMS, push, Slack sans changer le domaine
CHAPITRE 5 — Performance & Bonnes
Pratiques
5.1 — Batch Operations
✅ BON CODE
// [Link] — activer le batching Hibernate
// [Link].batch_size=50
// [Link].order_inserts=true
// [Link].order_updates=true
// ✅ saveAll() avec batch size → 10 000 inserts en ~200 queries groupées
@Transactional
public void importProducts(List<CreateProductDto> dtos) {
List<Product> products = [Link]()
.map(dto -> [Link](dto, resolveCategory(dto)))
.toList();
[Link](products); // Hibernate batche automatiquement par
groupes de 50
}
5.2 — Projections : Ne Charger que le Nécessaire
✅ BON CODE
// ✅ Interface Projection — SELECT uniquement les colonnes utiles
public interface ProductSummaryProjection {
Long getId();
String getName();
BigDecimal getPrice();
// Hibernate génère : SELECT id, name, price FROM product
// Pas de jointures inutiles, pas de lazy loading, perf maximale
}
public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductSummaryProjection> findAllProjectedBy();
// ✅ Constructor expression JPQL — encore plus explicite
@Query("SELECT new [Link]([Link], [Link],
[Link]) " +
"FROM Product p WHERE [Link] = :status")
List<ProductSummaryView> findSummariesByStatus(@Param("status")
ProductStatus status);
}
5.3 — @Transactional(readOnly = true) sur les Lectures
✅ BON CODE
// ✅ readOnly = true sur toutes les méthodes de lecture
// Hibernate désactive le dirty checking → performances améliorées
// Spring peut router vers un replica read-only si configuré
@Transactional(readOnly = true)
public List<ProductSummaryView> findAll(Pageable pageable) {
return [Link](pageable)
.getContent().stream()
.map(mapper::toSummary)
.toList();
}
@Transactional(readOnly = true)
public ProductDetailView findById(Long id) {
return [Link](id) // JOIN FETCH approprié
.map(mapper::toDetail)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
CHAPITRE 6 — Tests : Documentation
Vivante
6.1 — Tests Unitaires sur l'Aggregate
✅ BON CODE
@DisplayName("Order — règles métier")
class OrderTest {
@Test
@DisplayName("Une commande CONFIRMED ne peut pas recevoir de nouveaux
articles")
void shouldThrowWhenAddingItemToConfirmedOrder() {
// Given
Order order = [Link](anyAddress());
[Link](product("iPhone", money(999)), 1);
[Link]();
// When / Then
assertThatThrownBy(() -> [Link](product("AirPods", money(199)),
1))
.isInstanceOf([Link])
.hasMessageContaining("CONFIRMED");
}
@Test
@DisplayName("Impossible de confirmer une commande vide")
void shouldThrowWhenConfirmingEmptyOrder() {
Order order = [Link](anyAddress()); // aucun item
assertThatThrownBy(order::confirm)
.isInstanceOf([Link])
.hasMessageContaining("vide");
}
@Test
@DisplayName("Le total est calculé correctement")
void shouldCalculateTotalCorrectly() {
Order order = [Link](anyAddress());
[Link](product("iPhone", money(999)), 2); // 1998
[Link](product("AirPods", money(199)), 1); // 199
assertThat([Link]()).isEqualByComparingTo(new
BigDecimal("2197.00"));
}
}
6.2 — Tests de Contrat sur les DTOs
✅ BON CODE
class CreateProductDtoTest {
private final Validator validator =
[Link]().getValidator();
@Test @DisplayName("Valide si tous les champs obligatoires sont présents")
void shouldPassWithValidData() {
CreateProductDto dto = new CreateProductDto("iPhone", new
BigDecimal("999"), 1L);
assertThat([Link](dto)).isEmpty();
}
@Test @DisplayName("Échoue si le nom est blank")
void shouldFailWhenNameIsBlank() {
CreateProductDto dto = new CreateProductDto("", new BigDecimal("999"),
1L);
Set<ConstraintViolation<CreateProductDto>> v = [Link](dto);
assertThat(v).hasSize(1);
assertThat([Link]().next().getPropertyPath().toString()).isEqualTo("name");
}
@Test @DisplayName("Le mot de passe n'est jamais visible dans toString()")
void shouldNotExposePasswordInToString() {
CreateUserDto dto = new CreateUserDto("jean@[Link]",
"secretpassword123");
assertThat([Link]()).doesNotContain("secretpassword123");
}
}
6.3 — Tests d'Intégration JPA
✅ BON CODE
@DataJpaTest
@AutoConfigureTestDatabase(replace = [Link])
class ProductRepositoryTest {
@Autowired private ProductRepository productRepository;
@Test
@DisplayName("findAll ne retourne pas les produits supprimés")
void shouldExcludeDeletedProducts() {
Product active = createProduct("Active");
Product deleted = createProduct("Deleted");
[Link]();
[Link]([Link](active, deleted));
List<Product> results = [Link]();
assertThat(results).hasSize(1)
.extracting(Product::getName)
.containsExactly("Active");
}
@Test
@DisplayName("findWithCategory génère une seule query (pas de N+1)")
void shouldLoadWithCategoryInSingleQuery() {
Statistics stats = [Link]();
[Link](true); [Link]();
[Link](); // JOIN FETCH
assertThat([Link]()).isEqualTo(1);
}
}
CHAPITRE 7 — Checklist Senior : Avant
Chaque Pull Request
7.1 — Checklist JPA & Performance
✓ Vérification Risque si ignoré
□ Aucune query dans une boucle Timeout en production sous
(N+1) charge
□ Toutes les relations en LAZY Chargements inutiles → perf
par défaut dégradée
□ JOIN FETCH explicite dans les LazyInitializationException hors
queries concernées session
□ equals/hashCode sur Objets perdus dans Set/Map
identifiant métier stable après persist
□ Pas de @Data sur les entités equals/hashCode cassé sur id
JPA = null
□ @Transactional(readOnly=true Dirty checking inutile → perf
) sur les reads dégradée
□ batch_size configuré pour les N inserts séquentiels au lieu de
imports massifs batches
7.2 — Checklist Transactionnelle
✓ Vérification Risque si ignoré
□ @Transactional sur toutes les Données partiellement
méthodes d'écriture sauvées sans rollback
□ rollbackFor = [Link] Rollback ignoré → corruption
si checked exceptions de données
□ Pas de @Async + Transaction inexistante sur le
@Transactional sur la même mauvais thread
méthode
□ Events publiés avec Réaction à un événement qui
AFTER_COMMIT ne sera pas committé
□ Pas de self-invocation de Proxy bypassé → transaction
méthode @Transactional inexistante
7.3 — Checklist Design & Code Quality
✓ Vérification Risque si ignoré
□ 1 DTO par use case (Create, Contrat ambigu, validation
Update, View distinct) impossible
□ Logique métier dans l'entité, Invariants contournables, bugs
pas dans le service éparpillés
□ Constructeur protégé sur les Création dans un état invalide
entités possible
□ Validation Fail Fast (primitives Appels BDD inutiles pour des
avant I/O) données invalides
□ Tous les paramètres de Bug fonctionnel — compilateur
méthode sont utilisés ne le détecte pas
□ Nommage exprime l'intention Code illisible, maintenance
métier difficile
□ Enums plutôt que String pour Magic strings, non refactorable
les états
7.4 — Les 7 Questions à se Poser
// Avant chaque ligne de code, demande-toi :
// 1. Qui est responsable de cette règle métier ?
// → Service = orchestration | Entité = invariants
// 2. Combien de queries est-ce que ça génère ?
// → Boucle avec findById = N+1 à corriger immédiatement
// 3. Qu'est-ce qui se passe si ça échoue à mi-chemin ?
// → @Transactional présent ? rollbackFor correct ?
// 4. Peut-on construire cet objet dans un état invalide ?
// → Builder avec validation au build() nécessaire ?
// 5. Ce paramètre est-il vraiment utilisé partout ?
// → Si non → bug ou paramètre inutile (exemple : houseId ignoré)
// 6. 'null' signifie 'pas fourni' ou 'volontairement null' ?
// → Si ambigu → utiliser Patchable<T>
// 7. Dans 6 mois, un autre dev comprendra-t-il l'intention ?
// → Si non → renommer, extraire, commenter l'intention
CONCLUSION — La Roadmap vers le
Niveau Senior
Mindshift 1 — Penser en systèmes, pas en features
Un senior ne code pas une feature. Il réfléchit à comment elle s'intègre dans le système, quels
invariants elle touche, comment elle échoue, et comment elle évoluera dans 6 mois.
Mindshift 2 — Rendre l'erreur impossible, pas juste la détecter
Un junior met des if pour détecter les erreurs. Un senior conçoit ses objets pour qu'il soit
impossible de les créer dans un état invalide. La validation se déplace du service vers le type.
Mindshift 3 — Le code est pour les humains, pas les machines
La machine exécute n'importe quoi. Le code est lu par des humains. Nommage, structure,
découpage — tout doit exprimer l'intention métier, pas l'implémentation technique.
La Timeline Réaliste
Phase Durée Objectif Action concrète
Phase 1 0–2 mois Maîtrise JPA Activer les logs SQL,
traquer et corriger
chaque N+1 trouvé
Phase 2 2–4 mois DDD pragmatique Migrer 1 entité vers
Aggregate + Builder +
Value Objects
Phase 3 4–6 mois Design Patterns Remplacer les if/else
par Strategy, Builder
sur les entités
Phase 4 6–12 mois Architecture clean Implémenter un use
case complet avec
couches séparées
Continu — Mindset senior Checklist PR à
chaque commit,
relecture critique,
post-mortems
📐 La différence entre un dev qui stagne et un dev qui progresse : le second
comprend POURQUOI, pas juste COMMENT.
Bonne route vers le niveau Senior.