Padrões de Codificação Java
Padrões para código Java (17+) legível e de fácil manutenção em serviços Spring Boot e Quarkus.
Quando Usar
- Escrevendo ou revisando código Java em projetos Spring Boot ou Quarkus
- Aplicando convenções de nomenclatura, imutabilidade ou tratamento de exceções
- Trabalhando com records, sealed classes ou pattern matching (Java 17+)
- Revisando o uso de Optional, streams ou genéricos
- Estruturando pacotes e layout de projeto
- [QUARKUS]: Trabalhando com escopos CDI, entidades Panache ou pipelines reativos
Como Funciona
Detecção de 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 Básicos
- Prefira clareza a complexidade
- 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 em vez de runtime; evite reflexão em runtime 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 active-record Panache 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 MongoDB Panache
@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("Market not found"));
Boas Práticas de 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 vez de @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 por campo package-private (aceitável no Quarkus — evita problemas com proxies)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL: [SPRING] Injeção por campo com @Autowired
@Autowired
private MarketRepository marketRepository; // use injeção por construtor
// FAIL: [QUARKUS] @Singleton quando interceptação ou lazy init é necessário
@Singleton // não-proxyável — use @ApplicationScoped em vez disso
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 de domínio (ex.:
MarketNotFoundException) - Evite
catch (Exception ex)genérico 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 delimitados 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, @ConfigProperty beans, 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 # Auto-import 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 alto nível por arquivo
- Mantenha métodos curtos e focados; extraia helpers
- Ordem dos membros: constantes, campos, construtores, métodos públicos, protegidos, privados
Code Smells a Evitar
- Listas de parâmetros longas → 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]:
@Singletonquando@ApplicationScopedé o pretendido — quebra proxying e interceptação - [QUARKUS]: Misturar
quarkus-resteasy-reactiveequarkus-resteasy(clássico) — escolha uma pilha - [QUARKUS]: Usar Panache active-record e padrão repository no mesmo contexto delimitado — 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 para a classe declarante
Tratamento de Nulos
- Aceite
@Nullableapenas quando inevitável; caso contrário, use@NonNull - Use Bean Validation (
@NotNull,@NotBlank) nas entradas - [QUARKUS]: Aplique
@Validem@BeanParam,@RestForme parâmetros do corpo da 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 fluentes
- 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 repository@SpringBootTestreservado para testes de integração completos@MockBeanpara substituir beans no contexto Spring
[QUARKUS]
- JUnit 5 + Mockito simples 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 de 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 acima de micro-otimizações, a menos que comprovadamente necessário.