Chapitre 4: Conception REST & Design Patterns
Ces exercices couvrent la conception d'API REST et l'implémentation de design patterns — deux catégories de questions très fréquentes en entrevue backend Java/Spring Boot.
Exercice 1 — Concevoir un endpoint REST complet
"Concevez et implémentez un endpoint pour créer et récupérer des utilisateurs."
Solution
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserRequest req) {
UserDTO user = userService.create(req);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<Page<UserDTO>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(userService.findAll(PageRequest.of(page, size)));
}
}
Les DTOs (Data Transfer Objects)
public record CreateUserRequest(
@NotBlank String name,
@Email String email
) {}
public record UserDTO(Long id, String name, String email) {}
Points clés à mentionner en entrevue
| Bonne pratique | Pourquoi |
|---|---|
| DTOs au lieu d'entités | Ne jamais exposer les entités JPA dans l'API |
| @Valid | Valider les entrées côté serveur |
| 201 Created pour POST | Codes HTTP corrects |
| 404 Not Found si absent | Retourner le bon statut |
| Pagination sur les listes | Ne jamais retourner toutes les données d'un coup |
| Records Java pour les DTOs | Immutables, concis, auto-génèrent equals/hashCode/toString |
"I use DTOs to avoid exposing entities, @Valid for input validation, proper HTTP status codes (201 Created, 404 Not Found), and pagination for list endpoints. I use Java Records for immutable DTOs."
Exercice 2 — Concevoir une API e-commerce
"Concevez les endpoints REST pour un système de e-commerce."
Solution
Products:
GET /api/products → Liste paginée des produits
GET /api/products/{id} → Détail d'un produit
POST /api/products → Créer un produit (ADMIN)
PUT /api/products/{id} → Modifier un produit (ADMIN)
DELETE /api/products/{id} → Supprimer un produit (ADMIN)
Orders:
POST /api/orders → Passer une commande
GET /api/orders → Mes commandes (utilisateur connecté)
GET /api/orders/{id} → Détail d'une commande
PATCH /api/orders/{id}/status → Modifier le statut (ADMIN)
Auth:
POST /api/auth/register → Inscription
POST /api/auth/login → Connexion (retourne JWT)
POST /api/auth/refresh → Rafraîchir le token
Cart:
GET /api/cart → Mon panier
POST /api/cart/items → Ajouter un article au panier
PUT /api/cart/items/{id} → Modifier la quantité
DELETE /api/cart/items/{id} → Retirer du panier
Bonnes pratiques REST mentionnées
| Règle | Exemple |
|---|---|
| Noms au pluriel | /products pas /product |
| Verbes HTTP = actions | GET = lire, POST = créer, PUT = remplacer, PATCH = modifier partiellement, DELETE = supprimer |
| Pas de verbes dans l'URL | /api/orders pas /api/getOrders |
| Ressources imbriquées | /api/cart/items (items appartiennent au cart) |
| Sécurité par rôle | ADMIN pour les opérations sensibles |
Exercice 3 — Implémenter le Builder Pattern
"Créez une classe HttpRequest immuable avec le Builder Pattern."
Solution
public final class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder builder) {
this.url = Objects.requireNonNull(builder.url, "URL required");
this.method = builder.method;
this.headers = Map.copyOf(builder.headers); // copie défensive
this.body = builder.body;
}
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public static class Builder {
private String url;
private String method = "GET";
private final Map<String, String> headers = new HashMap<>();
private String body;
public Builder url(String url) { this.url = url; return this; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
Utilisation
HttpRequest req = new HttpRequest.Builder()
.url("https://api.sonar.com/issues")
.method("POST")
.header("Authorization", "Bearer token123")
.header("Content-Type", "application/json")
.body("{\"severity\": \"CRITICAL\"}")
.build();
Points clés
| Technique | Pourquoi |
|---|---|
Classe final | Empêche l'héritage |
Champs final | Immutabilité |
Constructeur private | Forcer l'utilisation du Builder |
Map.copyOf() | Copie défensive — l'objet ne peut pas être modifié après création |
Objects.requireNonNull() | Validation des champs obligatoires |
Fluent API (return this) | Chaînage des appels |
"Immutable object, defensive copies for collections, required fields validated in constructor. The fluent API makes the call site self-documenting."
Exercice 4 — Implémenter le Singleton Pattern (Thread-Safe)
"Implémentez un Singleton thread-safe en Java."
Solution moderne (Enum Singleton)
public enum DatabaseConnection {
INSTANCE;
private final Connection connection;
DatabaseConnection() {
this.connection = createConnection();
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// Logique de connexion
return DriverManager.getConnection("jdbc:postgresql://localhost/mydb");
}
}
// Utilisation
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Solution classique (Double-Checked Locking)
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private final Connection connection;
private DatabaseConnection() {
this.connection = createConnection();
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}
"I prefer the enum approach — it's the simplest, thread-safe by default, and handles serialization. Joshua Bloch recommends it in Effective Java."
En Spring Boot, le Singleton Pattern est rarement implémenté manuellement. Les beans Spring sont singleton par défaut dans le conteneur IoC. Si l'intervieweur pose la question, mentionnez que vous utilisez @Service ou @Component qui sont singleton par défaut.
Exercice 5 — Observer Pattern avec Spring Events
"Implémentez un système de notification découplé."
Solution avec Spring Events
// L'événement
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
public Order getOrder() { return order; }
}
// Le publisher (Service qui crée la commande)
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
// Les listeners (réagissent à l'événement)
@Component
class EmailNotificationListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendConfirmation(event.getOrder());
}
}
@Component
class InventoryListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
inventoryService.decrementStock(event.getOrder().getItems());
}
}
@Component
class AnalyticsListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
analyticsService.trackOrder(event.getOrder());
}
}
Avantages
| Sans Observer | Avec Observer |
|---|---|
| OrderService connaît EmailService, InventoryService, AnalyticsService... | OrderService publie un événement, c'est tout |
| Ajouter un listener = modifier OrderService | Ajouter un listener = créer une classe, zéro modification |
| Couplage fort | Découplage total |
"I use Spring Events for the Observer Pattern. The OrderService just publishes an event — it doesn't know who listens. Adding a new reaction is just creating a new @EventListener class."
Résumé des Design Patterns
| Pattern | Problème résolu | Exemple Spring |
|---|---|---|
| Strategy | Varier un algorithme | FeeCalculator avec Map |
| Builder | Construire des objets complexes | HttpRequest.Builder |
| Singleton | Instance unique | Beans Spring (par défaut) |
| Observer | Notifications découplées | @EventListener |
| Factory | Créer des objets sans connaître le type | BeanFactory |
| Template Method | Squelette d'algorithme | JdbcTemplate, RestTemplate |
| Proxy | Intercepion transparente | @Transactional, @Cacheable (proxies AOP) |
⬅️ Chapitre 3: Algorithmes | Chapitre 5: Débogage & Revue de code →