Pular para o conteúdo principal

Capítulo 4: Design REST & Design Patterns

Objetivo

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áticaPor quê
DTOs ao invés de entidadesNunca expor entidades JPA na API
@ValidValidar as entradas no lado do servidor
201 Created para POSTCódigos HTTP corretos
404 Not Found se ausenteRetornar o status correto
Paginação em listasNunca retornar todos os dados de uma vez
Records Java para DTOsImutáveis, concisos, geram automaticamente equals/hashCode/toString
Frase para entrevista

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

RegraExemplo
Nomes no plural/products e não /product
Verbos HTTP = açõesGET = 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 papelADMIN 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écnicaPor quê
Classe finalImpede herança
Campos finalImutabilidade
Construtor privateForç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
Frase para entrevista

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

"Prefiro a abordagem enum — é a mais simples, thread-safe por padrão e lida com serialização. Joshua Bloch recomenda no Effective Java."

Nota Spring Boot

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 ObserverCom Observer
OrderService conhece EmailService, InventoryService, AnalyticsService...OrderService apenas publica um evento, só isso
Adicionar um listener = modificar OrderServiceAdicionar um listener = criar uma classe, zero modificação
Forte acoplamentoTotal desacoplamento
Frase para entrevista

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

PatternProblema resolvidoExemplo Spring
StrategyVariar um algoritmoFeeCalculator com Map
BuilderConstruir objetos complexosHttpRequest.Builder
SingletonInstância únicaBeans Spring (padrão)
ObserverNotificações desacopladas@EventListener
FactoryCriar objetos sem conhecer o tipoBeanFactory
Template MethodEsqueleto de algoritmoJdbcTemplate, RestTemplate
ProxyInterceptação transparente@Transactional, @Cacheable (proxies AOP)

⬅️ Capítulo 3: Algoritmos | Capítulo 5: Debug & Code Review →