Capítulo 4: Diseño REST y Design Patterns
Estos ejercicios cubren el diseño de APIs REST y la implementación de design patterns — dos categorías de preguntas muy frecuentes en entrevistas backend Java/Spring Boot.
Ejercicio 1 — Diseñar un endpoint REST completo
"Diseñe e implemente un endpoint para crear y recuperar usuarios."
Solución
@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)));
}
}
Los DTOs (Data Transfer Objects)
public record CreateUserRequest(
@NotBlank String name,
@Email String email
) {}
public record UserDTO(Long id, String name, String email) {}
Puntos clave a mencionar en la entrevista
| Buena práctica | Por qué |
|---|---|
| DTOs en lugar de entidades | Nunca exponer las entidades JPA en la API |
| @Valid | Validar las entradas del lado del servidor |
| 201 Created para POST | Códigos HTTP correctos |
| 404 Not Found si no existe | Retornar el estado correcto |
| Paginación en los listados | Nunca retornar todos los datos de una vez |
| Records de Java para los DTOs | Inmutables, concisos, generan automáticamente equals/hashCode/toString |
"Uso DTOs para evitar exponer entidades, @Valid para la validación de entradas, códigos de estado HTTP apropiados (201 Created, 404 Not Found), y paginación para endpoints de listado. Utilizo Java Records para DTOs inmutables."
Ejercicio 2 — Diseñar una API de e-commerce
"Diseñe los endpoints REST para un sistema de e-commerce."
Solución
Products:
GET /api/products → Lista paginada de productos
GET /api/products/{id} → Detalle de un producto
POST /api/products → Crear un producto (ADMIN)
PUT /api/products/{id} → Modificar un producto (ADMIN)
DELETE /api/products/{id} → Eliminar un producto (ADMIN)
Orders:
POST /api/orders → Realizar un pedido
GET /api/orders → Mis pedidos (usuario autenticado)
GET /api/orders/{id} → Detalle de un pedido
PATCH /api/orders/{id}/status → Cambiar el estado (ADMIN)
Auth:
POST /api/auth/register → Registro
POST /api/auth/login → Inicio de sesión (retorna JWT)
POST /api/auth/refresh → Refrescar el token
Cart:
GET /api/cart → Mi carrito
POST /api/cart/items → Agregar un artículo al carrito
PUT /api/cart/items/{id} → Modificar la cantidad
DELETE /api/cart/items/{id} → Quitar del carrito
Buenas prácticas REST mencionadas
| Regla | Ejemplo |
|---|---|
| Nombres en plural | /products no /product |
| Verbos HTTP = acciones | GET = leer, POST = crear, PUT = reemplazar, PATCH = modificar parcialmente, DELETE = eliminar |
| Sin verbos en la URL | /api/orders no /api/getOrders |
| Recursos anidados | /api/cart/items (los items pertenecen al cart) |
| Seguridad por rol | ADMIN para operaciones sensibles |
Ejercicio 3 — Implementar el Builder Pattern
"Cree una clase HttpRequest inmutable con el Builder Pattern."
Solución
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); // copia defensiva
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); }
}
}
Uso
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();
Puntos clave
| Técnica | Por qué |
|---|---|
Clase final | Impide la herencia |
Campos final | Inmutabilidad |
Constructor private | Obliga el uso del Builder |
Map.copyOf() | Copia defensiva — el objeto no puede ser modificado después de su creación |
Objects.requireNonNull() | Validación de campos obligatorios |
Fluent API (return this) | Encadenamiento de llamadas |
"Objeto inmutable, copias defensivas para colecciones, campos obligatorios validados en el constructor. La Fluent API hace que el uso sea auto-documentado."
Ejercicio 4 — Implementar el Singleton Pattern (Thread-Safe)
"Implemente un Singleton thread-safe en Java."
Solución moderna (Enum Singleton)
public enum DatabaseConnection {
INSTANCE;
private final Connection connection;
DatabaseConnection() {
this.connection = createConnection();
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// Lógica de conexión
return DriverManager.getConnection("jdbc:postgresql://localhost/mydb");
}
}
// Uso
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Solución clásica (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;
}
}
"Prefiero el enfoque enum — es el más simple, thread-safe por defecto y maneja la serialización. Joshua Bloch lo recomienda en Effective Java."
En Spring Boot, el Singleton Pattern rara vez se implementa manualmente. Los beans de Spring son singleton por defecto en el contenedor IoC. Si el entrevistador hace la pregunta, mencione que utiliza @Service o @Component que son singleton por defecto.
Ejercicio 5 — Observer Pattern con Spring Events
"Implemente un sistema de notificaciones desacoplado."
Solución con Spring Events
// El evento
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
public Order getOrder() { return order; }
}
// El publisher (Servicio que crea el pedido)
@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;
}
}
// Los listeners (reaccionan al evento)
@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());
}
}
Ventajas
| Sin Observer | Con Observer |
|---|---|
| OrderService conoce EmailService, InventoryService, AnalyticsService... | OrderService solo publica un evento, nada más |
| Agregar un listener = modificar OrderService | Agregar un listener = crear una clase, cero modificaciones |
| Acoplamiento fuerte | Desacoplamiento total |
"Uso Spring Events para el Observer Pattern. El OrderService solo publica un evento — no sabe quién escucha. Agregar una nueva reacción es solo crear una nueva clase @EventListener."
Resumen de Design Patterns
| Pattern | Problema resuelto | Ejemplo Spring |
|---|---|---|
| Strategy | Variar un algoritmo | FeeCalculator con Map |
| Builder | Construir objetos complejos | HttpRequest.Builder |
| Singleton | Instancia única | Beans Spring (por defecto) |
| Observer | Notificaciones desacopladas | @EventListener |
| Factory | Crear objetos sin conocer el tipo | BeanFactory |
| Template Method | Esqueleto de algoritmo | JdbcTemplate, RestTemplate |
| Proxy | Intercepción transparente | @Transactional, @Cacheable (proxies AOP) |
⬅️ Capítulo 3: Algoritmos | Capítulo 5: Depuración y Revisión de código →