Saltar al contenido principal

Capítulo 4: Diseño REST y Design Patterns

Objetivo

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ácticaPor qué
DTOs en lugar de entidadesNunca exponer las entidades JPA en la API
@ValidValidar las entradas del lado del servidor
201 Created para POSTCódigos HTTP correctos
404 Not Found si no existeRetornar el estado correcto
Paginación en los listadosNunca retornar todos los datos de una vez
Records de Java para los DTOsInmutables, concisos, generan automáticamente equals/hashCode/toString
Frase de entrevista

"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​

ReglaEjemplo
Nombres en plural/products no /product
Verbos HTTP = accionesGET = 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 rolADMIN 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écnicaPor qué
Clase finalImpide la herencia
Campos finalInmutabilidad
Constructor privateObliga 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
Frase de entrevista

"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;
}
}
Frase de entrevista

"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."

Nota Spring Boot

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 ObserverCon Observer
OrderService conoce EmailService, InventoryService, AnalyticsService...OrderService solo publica un evento, nada más
Agregar un listener = modificar OrderServiceAgregar un listener = crear una clase, cero modificaciones
Acoplamiento fuerteDesacoplamiento total
Frase de entrevista

"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​

PatternProblema resueltoEjemplo Spring
StrategyVariar un algoritmoFeeCalculator con Map
BuilderConstruir objetos complejosHttpRequest.Builder
SingletonInstancia únicaBeans Spring (por defecto)
ObserverNotificaciones desacopladas@EventListener
FactoryCrear objetos sin conocer el tipoBeanFactory
Template MethodEsqueleto de algoritmoJdbcTemplate, RestTemplate
ProxyIntercepción transparente@Transactional, @Cacheable (proxies AOP)

⬅️ Capítulo 3: Algoritmos | Capítulo 5: Depuración y Revisión de código →