Estándares de Codificación Java
Estándares para código Java (17+) legible y mantenible en servicios Spring Boot y Quarkus.
Cuándo Usarlo
- Al escribir o revisar código Java en proyectos Spring Boot o Quarkus
- Al aplicar convenciones de nomenclatura, inmutabilidad o manejo de excepciones
- Al trabajar con records, clases selladas o pattern matching (Java 17+)
- Al revisar el uso de Optional, streams o genéricos
- Al estructurar paquetes y la disposición del proyecto
- [QUARKUS]: Al trabajar con ámbitos CDI, entidades Panache o pipelines reactivos
Cómo Funciona
Detección del Framework
Antes de aplicar los estándares, determina el framework a partir del archivo de construcción:
- El archivo de construcción contiene
quarkus→ aplicar convenciones [QUARKUS] - El archivo de construcción contiene
spring-boot→ aplicar convenciones [SPRING] - Ninguno detectado → aplicar solo convenciones compartidas
Principios Fundamentales
- Preferir claridad sobre ingenio
- Inmutable por defecto; minimizar el estado mutable compartido
- Fallar rápido con excepciones significativas
- Nomenclatura y estructura de paquetes consistentes
- [QUARKUS]: Favorecer el procesamiento en tiempo de compilación sobre el de ejecución; evitar la reflexión en tiempo de ejecución cuando sea posible
Ejemplos
Las secciones a continuación muestran ejemplos concretos para Spring Boot, Quarkus y Java compartido sobre nomenclatura, inmutabilidad, inyección de dependencias, código reactivo, excepciones, disposición del proyecto, registro, configuración y pruebas.
Nomenclatura
// PASA: Clases/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASA: Métodos/campos: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASA: Constantes: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASA: [QUARKUS] Recursos JAX-RS nombrados como *Resource, no *Controller
public class MarketResource {}
// PASA: [SPRING] Controladores REST nombrados como *Controller
public class MarketController {}
Inmutabilidad
// PASA: Favorecer records y campos final
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// solo getters, sin setters
}
// PASA: [QUARKUS] Las entidades Panache de active-record usan campos públicos (convención de Quarkus)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache genera accesores en tiempo de compilación; los campos públicos son idiomáticos aquí
}
// PASA: [QUARKUS] Entidades Panache MongoDB
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Uso de Optional
// PASA: Devolver Optional desde métodos find*
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASA: Usar map/flatMap en lugar de get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Mercado no encontrado"));
Mejores Prácticas con Streams
// PASA: Usar streams para transformaciones, mantener pipelines cortos
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FALLA: Evitar streams anidados complejos; preferir bucles para mayor claridad
Inyección de Dependencias
// PASA: [SPRING] Inyección por constructor (preferida sobre @Autowired en campos)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASA: [QUARKUS] Inyección por constructor
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASA: [QUARKUS] Inyección de campo package-private (aceptable en Quarkus — evita problemas de proxy)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FALLA: [SPRING] Inyección de campo con @Autowired
@Autowired
private MarketRepository marketRepository; // usar inyección por constructor
// FALLA: [QUARKUS] @Singleton cuando se necesita intercepción o inicialización diferida
@Singleton // no proxyable — usar @ApplicationScoped en su lugar
public class MarketService {}
Patrones Reactivos [QUARKUS]
// PASA: Devolver Uni/Multi desde endpoints reactivos
@GET
@Path("/{slug}")
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
return Market.find("slug", slug)
.<Market>firstResult()
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
}
// PASA: Composición de pipeline no bloqueante
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FALLA: Llamada bloqueante dentro de un pipeline Uni/Multi
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOQUEANTE — rompe el bucle de eventos
return Uni.createFrom().item(m);
}
// FALLA: Suscribirse más de una vez a un Uni compartido
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // doble suscripción — usar Uni.memoize()
Excepciones
- Usar excepciones no verificadas para errores de dominio; envolver excepciones técnicas con contexto
- Crear excepciones específicas del dominio (ej.,
MarketNotFoundException) - Evitar
catch (Exception ex)amplio a menos que se relance/registre centralizadamente
throw new MarketNotFoundException(slug);
Manejo Centralizado de Excepciones
// [SPRING]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MarketNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
}
}
// [QUARKUS] Opción 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] Opción B: @ServerExceptionMapper (RESTEasy Reactive)
@ServerExceptionMapper
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
}
Genéricos y Seguridad de Tipos
- Evitar tipos crudos; declarar parámetros genéricos
- Preferir genéricos acotados para utilidades reutilizables
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
Estructura del Proyecto
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (refleja main)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, @ConfigProperty beans, Producers
resource/ # Recursos JAX-RS (no "controller")
service/
repository/ # Implementaciones de PanacheRepository (si no se usa active record)
domain/ # Entidades JPA/Panache, entidades MongoDB
dto/
util/
mapper/ # Mapeadores MapStruct (si se usan)
src/main/resources/
application.properties # Convención de Quarkus (YAML soportado con quarkus-config-yaml)
import.sql # Autoimportación de Hibernate para dev/test
src/test/java/... (refleja main)
Formato y Estilo
- Usar 2 o 4 espacios de forma consistente (estándar del proyecto)
- Un tipo público de nivel superior por archivo
- Mantener métodos cortos y enfocados; extraer ayudantes
- Ordenar miembros: constantes, campos, constructores, métodos públicos, protegidos, privados
Malas Prácticas a Evitar
- Listas de parámetros largas → usar DTO/constructores
- Anidamiento profundo → retornos tempranos
- Números mágicos → constantes con nombre
- Estado mutable estático → preferir inyección de dependencias
- Bloques catch silenciosos → registrar y actuar o relanzar
- [QUARKUS]:
@Singletondonde se necesita@ApplicationScoped— rompe la creación de proxies y la intercepción - [QUARKUS]: Mezclar
quarkus-resteasy-reactiveyquarkus-resteasy(clásico) — elegir una pila - [QUARKUS]: Panache active-record + patrón repositorio en el mismo contexto acotado — elegir uno
Registro
// [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 (predeterminado, sin costo en tiempo de compilación)
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: registro simplificado con @Inject
@Inject
Logger log; // Inyectado vía CDI, con ámbito de la clase declarante
Manejo de Nulos
- Aceptar
@Nullablesolo cuando sea inevitable; de lo contrario usar@NonNull - Usar Bean Validation (
@NotNull,@NotBlank) en las entradas - [QUARKUS]: Aplicar
@Validen@BeanParam,@RestFormy parámetros del cuerpo de la solicitud
Configuración
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping (seguro en tipos, validado en tiempo de compilación)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] Valores simples con @ConfigProperty
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
Expectativas de Pruebas
Compartido
- JUnit 5 + AssertJ para aserciones fluidas
- Mockito para mocking; evitar mocks parciales cuando sea posible
- Favorecer pruebas deterministas; sin sleeps ocultos
[SPRING]
@WebMvcTestpara pruebas de controladores,@DataJpaTestpara pruebas de repositorios@SpringBootTestreservado para pruebas de integración completas@MockBeanpara reemplazar beans en el contexto de Spring
[QUARKUS]
- JUnit 5 simple + Mockito para pruebas unitarias (sin
@QuarkusTest) @QuarkusTestreservado para pruebas de integración con CDI@InjectMockpara reemplazar beans CDI en pruebas de integración- Dev Services para base de datos/Kafka/Redis — evitar configurar Testcontainers manualmente cuando Dev Services sea suficiente
@QuarkusTestResourcepara el ciclo de vida de servicios externos personalizados
// [SPRING] Prueba de controlador
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] Prueba de integración
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] Prueba unitaria (sin CDI, sin @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
Recuerda: Mantén el código intencional, tipado y observable. Optimiza para mantenibilidad sobre microoptimizaciones a menos que se demuestre necesario.