Padrões de Codificação Java
Padrões para código Java (17+) legível e sustentável em serviços Spring Boot e Quarkus.
Quando Usar
- Ao escrever ou revisar código Java em projetos Spring Boot ou Quarkus
- Ao aplicar convenções de nomenclatura, imutabilidade ou tratamento de exceções
- Ao trabalhar com records, sealed classes ou pattern matching (Java 17+)
- Ao revisar o uso de Optional, streams ou genéricos
- Ao estruturar pacotes e layout de projeto
- [QUARKUS]: Ao trabalhar com escopos CDI, entidades Panache ou pipelines reativos
Como Funciona
Detecção do Framework
Antes de aplicar os padrões, determine o framework a partir do arquivo de build:
- Arquivo de build contém
quarkus→ aplicar convenções [QUARKUS] - Arquivo de build contém
spring-boot→ aplicar convenções [SPRING] - Nenhum detectado → aplicar apenas convenções compartilhadas
Princípios Essenciais
- Prefira clareza a engenhosidade
- Imutável por padrão; minimize estado mutável compartilhado
- Falhe rápido com exceções significativas
- Nomenclatura e estrutura de pacotes consistentes
- [QUARKUS]: Prefira processamento em tempo de build a tempo de execução; evite reflexão em tempo de execução sempre que possível
Exemplos
As seções abaixo mostram exemplos concretos para Spring Boot, Quarkus e Java compartilhado para nomenclatura, imutabilidade, injeção de dependência, código reativo, exceções, layout de projeto, logging, configuração e testes.
Nomenclatura
// PASS: Classes/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS: Métodos/campos: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS: Constantes: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASS: [QUARKUS] Recursos JAX-RS nomeados como *Resource, não *Controller
public class MarketResource {}
// PASS: [SPRING] Controladores REST nomeados como *Controller
public class MarketController {}
Imutabilidade
// PASS: Prefira records e campos final
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// apenas getters, sem setters
}
// PASS: [QUARKUS] Entidades Panache active-record usam campos públicos (convenção Quarkus)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache gera acessores em tempo de build; campos públicos são idiomáticos aqui
}
// PASS: [QUARKUS] Entidades Panache MongoDB
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Uso de Optional
// PASS: Retorne Optional de métodos find*
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASS: Use map/flatMap em vez de get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Mercado não encontrado"));
Boas Práticas com Streams
// PASS: Use streams para transformações, mantenha pipelines curtos
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FAIL: Evite streams aninhados complexos; prefira loops para clareza
Injeção de Dependência
// PASS: [SPRING] Injeção por construtor (preferida em relação a @Autowired em campos)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] Injeção por construtor
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] Injeção de campo package-private (aceitável no Quarkus — evita problemas de proxy)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL: [SPRING] Injeção de campo com @Autowired
@Autowired
private MarketRepository marketRepository; // use injeção por construtor
// FAIL: [QUARKUS] @Singleton quando interceptação ou inicialização preguiçosa é necessária
@Singleton // não-proxiable — use @ApplicationScoped
public class MarketService {}
Padrões Reativos [QUARKUS]
// PASS: Retorne Uni/Multi de endpoints reativos
@GET
@Path("/{slug}")
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
return Market.find("slug", slug)
.<Market>firstResult()
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
}
// PASS: Composição de pipeline não bloqueante
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FAIL: Chamada bloqueante dentro de um pipeline Uni/Multi
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOQUEANTE — quebra o event loop
return Uni.createFrom().item(m);
}
// FAIL: Assinar mais de uma vez um Uni compartilhado
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // assinatura dupla — use Uni.memoize()
Exceções
- Use exceções não verificadas para erros de domínio; encapsule exceções técnicas com contexto
- Crie exceções específicas do domínio (ex.:
MarketNotFoundException) - Evite
catch (Exception ex)amplo, a menos que esteja relançando ou registrando centralmente
throw new MarketNotFoundException(slug);
Tratamento Centralizado de Exceções
// [SPRING]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MarketNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
}
}
// [QUARKUS] Opção A: ExceptionMapper
@Provider
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
@Override
public Response toResponse(MarketNotFoundException ex) {
return Response.status(404).entity(ErrorResponse.from(ex)).build();
}
}
// [QUARKUS] Opção B: @ServerExceptionMapper (RESTEasy Reactive)
@ServerExceptionMapper
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
}
Genéricos e Segurança de Tipos
- Evite tipos brutos; declare parâmetros genéricos
- Prefira genéricos limitados para utilitários reutilizáveis
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
Estrutura de Projeto
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (espelha main)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, beans @ConfigProperty, Producers
resource/ # Recursos JAX-RS (não "controller")
service/
repository/ # Implementações PanacheRepository (se não estiver usando active record)
domain/ # Entidades JPA/Panache, entidades MongoDB
dto/
util/
mapper/ # Mapeadores MapStruct (se usado)
src/main/resources/
application.properties # Convenção Quarkus (YAML suportado com quarkus-config-yaml)
import.sql # Import automático do Hibernate para dev/test
src/test/java/... (espelha main)
Formatação e Estilo
- Use 2 ou 4 espaços consistentemente (padrão do projeto)
- Um tipo público de nível superior por arquivo
- Mantenha métodos curtos e focados; extraia auxiliares
- Ordene membros: constantes, campos, construtores, métodos públicos, protegidos, privados
Code Smells a Evitar
- Listas longas de parâmetros → use DTO/builders
- Aninhamento profundo → retornos antecipados
- Números mágicos → constantes nomeadas
- Estado mutável estático → prefira injeção de dependência
- Blocos catch silenciosos → registre e aja ou relance
- [QUARKUS]:
@Singletononde@ApplicationScopedé pretendido — quebra proxy e interceptação - [QUARKUS]: Misturar
quarkus-resteasy-reactiveequarkus-resteasy(clássico) — escolha uma pilha - [QUARKUS]: Panache active-record + padrão repositório no mesmo contexto limitado — escolha um
Logging
// [SPRING] SLF4J
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch_market slug={}", slug);
log.error("failed_fetch_market slug={}", slug, ex);
// [QUARKUS] JBoss Logging (padrão, custo zero em tempo de build)
private static final Logger log = Logger.getLogger(MarketService.class);
log.infof("fetch_market slug=%s", slug);
log.errorf(ex, "failed_fetch_market slug=%s", slug);
// [QUARKUS] Alternativa: logging simplificado com @Inject
@Inject
Logger log; // injetado via CDI, escopo da classe declarante
Tratamento de Nulos
- Aceite
@Nullableapenas quando inevitável; caso contrário, use@NonNull - Use Bean Validation (
@NotNull,@NotBlank) em entradas - [QUARKUS]: Aplique
@Validem@BeanParam,@RestForme parâmetros de corpo de requisição
Configuração
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping (type-safe, validado em tempo de build)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] Valores simples com @ConfigProperty
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
Expectativas de Teste
Compartilhado
- JUnit 5 + AssertJ para asserções fluidas
- Mockito para mocks; evite mocks parciais sempre que possível
- Prefira testes determinísticos; sem sleeps ocultos
[SPRING]
@WebMvcTestpara slices de controller,@DataJpaTestpara slices de repositório@SpringBootTestreservado para testes de integração completos@MockBeanpara substituir beans no contexto Spring
[QUARKUS]
- JUnit 5 puro + Mockito para testes unitários (sem
@QuarkusTest) @QuarkusTestreservado para testes de integração CDI@InjectMockpara substituir beans CDI em testes de integração- Dev Services para banco de dados/Kafka/Redis — evite configuração manual do Testcontainers quando Dev Services forem suficientes
@QuarkusTestResourcepara ciclo de vida personalizado de serviços externos
// [SPRING] Teste de controller
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] Teste de integração
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] Teste unitário (sem CDI, sem @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
Lembre-se: Mantenha o código intencional, tipado e observável. Otimize para capacidade de manutenção em vez de micro-otimizações, a menos que comprovadamente necessárias.