Capítulo 4: Design REST & Design Patterns
Estes exercícios cobrem o design de APIs REST e a implementação de design patterns — duas categorias de perguntas muito frequentes em entrevistas para backend Java/Spring Boot.
Exercício 1 — Projetar um endpoint REST completo
"Projete e implemente um endpoint para criar e recuperar usuários."
Solução
@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)));
}
}
Os DTOs (Data Transfer Objects)
public record CreateUserRequest(
@NotBlank String name,
@Email String email
) {}
public record UserDTO(Long id, String name, String email) {}
Pontos-chave para mencionar na entrevista
| Boa prática | Por quê |
|---|---|
| DTOs ao invés de entidades | Nunca expor entidades JPA na API |
| @Valid | Validar as entradas no lado do servidor |
| 201 Created para POST | Códigos HTTP corretos |
| 404 Not Found se ausente | Retornar o status correto |
| Paginação em listas | Nunca retornar todos os dados de uma vez |
| Records Java para DTOs | Imutáveis, concisos, geram automaticamente equals/hashCode/toString |
"Uso DTOs para evitar expor entidades, @Valid para validação de entrada, códigos de status HTTP corretos (201 Created, 404 Not Found) e paginação para endpoints de lista. Uso Java Records para DTOs imutáveis."
Exercício 2 — Projetar uma API de e-commerce
"Projete os endpoints REST para um sistema de e-commerce."
Solução
Products:
GET /api/products → Lista paginada de produtos
GET /api/products/{id} → Detalhe de um produto
POST /api/products → Criar um produto (ADMIN)
PUT /api/products/{id} → Alterar um produto (ADMIN)
DELETE /api/products/{id} → Remover um produto (ADMIN)
Orders:
POST /api/orders → Realizar um pedido
GET /api/orders → Meus pedidos (usuário autenticado)
GET /api/orders/{id} → Detalhe de um pedido
PATCH /api/orders/{id}/status → Alterar status (ADMIN)
Auth:
POST /api/auth/register → Cadastro
POST /api/auth/login → Login (retorna JWT)
POST /api/auth/refresh → Atualizar token
Cart:
GET /api/cart → Meu carrinho
POST /api/cart/items → Adicionar item ao carrinho
PUT /api/cart/items/{id} → Alterar quantidade
DELETE /api/cart/items/{id} → Remover do carrinho
Boas práticas REST mencionadas
| Regra | Exemplo |
|---|---|
| Nomes no plural | /products e não /product |
| Verbos HTTP = ações | GET = ler, POST = criar, PUT = substituir, PATCH = alterar parcialmente, DELETE = remover |
| Sem verbos na URL | /api/orders e não /api/getOrders |
| Recursos aninhados | /api/cart/items (itens pertencem ao carrinho) |
| Segurança por papel | ADMIN para operações sensíveis |
Exercício 3 — Implementar o Builder Pattern
"Crie uma classe HttpRequest imutável com o Builder Pattern."
Solução
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); // cópia 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();
Pontos-chave
| Técnica | Por quê |
|---|---|
Classe final | Impede herança |
Campos final | Imutabilidade |
Construtor private | Força o uso do Builder |
Map.copyOf() | Cópia defensiva — o objeto não pode ser modificado após a criação |
Objects.requireNonNull() | Validação de campos obrigatórios |
Fluent API (return this) | Encadeamento de chamadas |
"Objeto imutável, cópias defensivas para coleções, campos obrigatórios validados no construtor. A Fluent API torna o uso autoexplicativo."
Exercício 4 — Implementar o Singleton Pattern (Thread-Safe)
"Implemente um Singleton thread-safe em Java."
Solução 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 conexão
return DriverManager.getConnection("jdbc:postgresql://localhost/mydb");
}
}
// Uso
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Solução clássica (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;
}
}
"Prefiro a abordagem enum — é a mais simples, thread-safe por padrão e lida com serialização. Joshua Bloch recomenda no Effective Java."
No Spring Boot, o Singleton Pattern raramente é implementado manualmente. Os beans Spring são singleton por padrão no container IoC. Se o entrevistador perguntar, mencione que você usa @Service ou @Component, que já são singleton por padrão.
Exercício 5 — Observer Pattern com Spring Events
"Implemente um sistema de notificação desacoplado."
Solução com Spring Events
// O evento
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
public Order getOrder() { return order; }
}
// O publisher (Service que cria o 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;
}
}
// Os listeners (reagem ao 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());
}
}
Vantagens
| Sem Observer | Com Observer |
|---|---|
| OrderService conhece EmailService, InventoryService, AnalyticsService... | OrderService apenas publica um evento, só isso |
| Adicionar um listener = modificar OrderService | Adicionar um listener = criar uma classe, zero modificação |
| Forte acoplamento | Total desacoplamento |
"Uso Spring Events para o Observer Pattern. O OrderService apenas publica um evento — não sabe quem está ouvindo. Adicionar uma nova reação é só criar uma nova classe @EventListener."
Resumo dos Design Patterns
| Pattern | Problema resolvido | Exemplo Spring |
|---|---|---|
| Strategy | Variar um algoritmo | FeeCalculator com Map |
| Builder | Construir objetos complexos | HttpRequest.Builder |
| Singleton | Instância única | Beans Spring (padrão) |
| Observer | Notificações desacopladas | @EventListener |
| Factory | Criar objetos sem conhecer o tipo | BeanFactory |
| Template Method | Esqueleto de algoritmo | JdbcTemplate, RestTemplate |
| Proxy | Interceptação transparente | @Transactional, @Cacheable (proxies AOP) |
⬅️ Capítulo 3: Algoritmos | Capítulo 5: Debug & Code Review →