b. API
O primeiro passo desenha o contrato do catalog: quais recursos ele expõe, quais requisições aceita e o que responde. Em uma entrevista, este é o passo 3 do framework (API e modelo de dados); em um time de verdade, é aquilo contra o que os outros times programam, então merece cuidado antes de existir qualquer SQL.
1. Crie o projeto
Crie a pasta services/catalog no seu repositório com a estrutura padrão do Maven:
mkdir -p services/catalog/src/main/java/boxoffice/catalog
mkdir -p services/catalog/src/main/resources/db/migration
Depois acrescente o pom.xml:
| Dependência | Por quê |
|---|---|
spring-boot-starter-webmvc | Spring MVC + Tomcat embutido: uma thread por requisição em andamento (você vai contá-las no lab 2) |
spring-boot-starter-validation | anotações de Bean Validation em records e parâmetros |
spring-boot-starter-data-jpa | JPA/Hibernate, repositórios, transações, o pool de conexões HikariCP |
spring-boot-starter-actuator | /actuator/health (usado pelo Compose, pelo Nginx e pelo Kubernetes) e /actuator/metrics |
spring-boot-starter-flyway + flyway-database-postgresql | migrations versionadas do schema (no Spring Boot 4 cada tecnologia tem o seu starter e módulo de auto-configuração; só o flyway-core não é mais detectado) |
postgresql | driver JDBC |
springdoc-openapi-starter-webmvc-ui (3.x) | gera o documento OpenAPI a partir dos controllers, mais o Swagger UI (o springdoc 3 é a linha do Spring Boot 4) |
Prefere um gerador?
O start.spring.io produz o mesmo esqueleto (Maven, Java 21, as dependências acima). Escolha o Spring Boot 4.1.x e ajuste o group para boxoffice. Os labs foram escritos e testados com Spring Boot 4.1.1 e Spring Cloud 2025.1.3; uma versão de correção 4.1.x mais nova funciona do mesmo jeito.
O ponto de entrada é a linha de sempre:
package boxoffice.catalog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CatalogApplication {
public static void main(String[] args) {
SpringApplication.run(CatalogApplication.class, args);
}
}
2. Desenhe os recursos
Dois recursos, cada um um substantivo com uma URL de coleção e uma URL de item, todos sob o prefixo do serviço /catalog (veja as convenções do lab 0):
classDiagram
class Venue {
Long id
String name
String city
int capacity
}
class Event {
Long id
String title
Category category
OffsetDateTime startsAt
BigDecimal priceFrom
}
class Category {
<<enumeration>>
CONCERT
SPORTS
FESTIVAL
THEATER
}
Event "*" --> "1" Venue : acontece em
Event --> Category | Decisão | Escolha | Por quê |
|---|---|---|
| Identificadores | BIGINT gerado pelo banco | simples, compacto, fácil de sortear no k6 (1..200000) |
| Coleções | paginadas (page, size ≤ 50) | um GET /events sem limite sobre 200.000 linhas seria uma queda autoinfligida |
| Listagem padrão | próximos eventos, os mais próximos primeiro | é o que a página inicial mostra; eventos passados não estão à venda |
| Dinheiro | BigDecimal / NUMERIC(10,2) | nunca double para preços |
| Tempo | OffsetDateTime / TIMESTAMPTZ, ISO-8601 no JSON | eventos acontecem em fusos horários diferentes |
| Erros | RFC 9457 application/problem+json | um formato de erro legível por máquina para todo serviço |
| Criação | 201 Created + Location: /catalog/events/{id} | o cliente descobre onde o novo recurso vive |
3. Records de requisição e resposta
A API nunca devolve entidades JPA. Records são imutáveis, não têm anotações de persistência e deixam o formato do JSON explícito — quando a tabela muda, a API não muda por acidente.
package boxoffice.catalog;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
/** What a client sends to create or update an event. */
public record EventIn(
@NotBlank @Size(max = 200) String title,
@NotNull Category category,
@NotNull Long venueId,
@NotNull OffsetDateTime startsAt,
@NotNull @PositiveOrZero BigDecimal priceFrom) {
}
package boxoffice.catalog;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
/** What the API returns: never the JPA entity itself. */
public record EventOut(
Long id,
String title,
Category category,
OffsetDateTime startsAt,
BigDecimal priceFrom,
VenueOut venue) {
static EventOut of(Event e) {
return new EventOut(e.getId(), e.getTitle(), e.getCategory(), e.getStartsAt(),
e.getPriceFrom(), VenueOut.of(e.getVenue()));
}
}
package boxoffice.catalog;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
public record VenueIn(
@NotBlank @Size(max = 200) String name,
@NotBlank @Size(max = 100) String city,
@Positive int capacity) {
}
package boxoffice.catalog;
import java.util.List;
import org.springframework.data.domain.Page;
/** A stable JSON shape for paged results (Spring's Page is not meant to be serialized). */
public record PageOut<T>(List<T> items, int page, int size, long totalItems, int totalPages) {
static <T> PageOut<T> of(Page<T> p) {
return new PageOut<>(p.getContent(), p.getNumber(), p.getSize(), p.getTotalElements(), p.getTotalPages());
}
}
Todos ficam em src/main/java/boxoffice/catalog/. As fábricas estáticas EventOut.of(Event) e VenueOut.of(Venue) são o único lugar em que uma entidade vira DTO.
4. Controllers
Controllers só traduzem HTTP em chamadas de método: validam a entrada, chamam o serviço e escolhem o código de status. Nada de SQL, nada de regra de negócio.
Como a validação funciona aqui:
@Valid @RequestBody EventIn inverifica as anotações do record (@NotBlank,@NotNull,@PositiveOrZero) e responde 400 antes de o método rodar.@Min(0)/@Max(50)empageesizeusam a validação de métodos embutida no Spring (Spring Framework 6.1+):?size=5000é rejeitado com 400 em vez de carregar 5.000 linhas.- Com
spring.mvc.problemdetails.enabled: true(próximo passo), os dois casos voltam comoapplication/problem+json.
O que um cliente vê
Na entrevista
Ao esboçar a API, diga três coisas em voz alta: os recursos e verbos (GET /events?city=&page=), a estratégia de paginação (offset agora; keyset — WHERE starts_at > :last — quando os usuários rolam fundo, porque OFFSET 100000 ainda lê 100.000 linhas) e o modelo de erros. Entrevistadores prestam atenção em se você limita toda coleção.
Armadilhas
- Devolver a entidade: um
venuelazy serializado fora de uma transação lançaLazyInitializationExceptionou, pior, uma coluna nova vaza para os clientes (um hash de senha, um preço de custo). - Listas sem limite: todo endpoint de coleção precisa de um tamanho máximo de página.
200para tudo: códigos de status fazem parte do contrato; proxies, caches e checks do k6 dependem deles.
Os controllers dependem do CatalogService, que você escreve no próximo passo junto com as entidades e o banco: 2. Persistência.