Java maven
✅ GitHub Copilot Naming Conventions (Consolidated)¶
Important Process Requirements¶
- ALWAYS present a detailed plan and wait for explicit approval before implementing any code changes
- Do not proceed with implementation until receiving confirmation from the user
- When presenting the plan, provide a step-by-step breakdown of all files to be created or modified
- Ask directly: "Do you approve this plan before I proceed with implementation?"
Naming Conventions¶
🔤 General Naming Patterns¶
| Element Type | Pattern / Style | Example(s) |
|---|---|---|
| Constants | UPPER_CASE_WITH_UNDERSCORES |
MAX_TIMEOUT, DEFAULT_PORT |
| Variables | camelCase |
totalAmount, userId |
| Static Variables | camelCase |
sharedInstance |
| Catch / Lambda Params | camelCase |
e, eventHandler |
| Method Names | camelCase |
getUserData(), isAvailable() |
| Class / Enum / Interface Names | PascalCase |
UserService, TransactionType |
| Interfaces | PascalCase (no I prefix) |
Validator, UserRepository |
| Package Names | lowercase, no underscores | com.example.project |
| File / Directory Names | snake_case (if needed) |
my_utils.java |
| Generic Type Parameters | Single uppercase letter (T, E, K, V, R) |
T, E, K, V, R |
🧠 Naming Best Practices¶
- Capitalize acronyms/abbreviations longer than two letters like regular words (e.g.,
HttpClient, notHTTPClient). - Avoid abbreviations unless more common than the full term (e.g.,
URLis fine,Usris not). - Use consistent terms across codebase (e.g., always use
customerinstead of alternatinguser,client, etc.). - For boolean variables and methods:
- Prefer positive forms:
isEnabled, notisDisabled - Use
isX()instead ofgetX() - Use noun phrases for non-boolean properties (e.g.,
retryLimit) - Use non-imperative verb phrases for booleans (e.g.,
isVisible) - Omit the verb in named boolean parameters when context is clear
- Prefer positive forms:
- Avoid method or field names that match the enclosing class name
- Avoid suspicious or incorrect method signature overrides like
equals()orhashCode()that violate contracts - Prefer descriptive nouns last in identifiers (e.g.,
userProfileManager, notmanagerUserProfile) - Make method and class names read like natural language when possible (e.g.,
UserService.getUserByEmail()) - Do not use reserved keywords like
record,var, orsealedas identifiers - Ensure the outer type name matches the filename (e.g., class
UserServicein fileUserService.java) - Use descriptive and intention-revealing names (e.g.,
retryLimitinstead ofrl,isVisibleinstead offlag; avoiduserDataObject— preferuserProfileoruserDetails) - Avoid noise words in identifiers (e.g., avoid
the,data,objectunless truly meaningful) - Use context in names when needed, but avoid overly long or repetitive identifiers (e.g.,
userRepositoryis better thanapplicationCoreUserDataAccessRepository) - Follow existing naming conventions for type parameters in your codebase when consistency outweighs strict style (e.g., continuing with
UserTif it’s a common pattern already in use)
These conventions help ensure clarity, maintainability, and a shared vocabulary across teams using Copilot.
✅ Code Style Instructions¶
📐 Format and Structure¶
- Always use braces for control structures (
if,while, etc.). - Use one statement per line.
- Ensure a consistent order of declarations (fields, constructors, methods).
- Method overloads must be grouped together.
defaultcases must be last inswitchstatements.- Enforce newline separation between classes, methods, and logical blocks.
- Use 2 spaces for indentation.
- Brace alignment: +2 spaces.
- Case blocks and array initializations: 2 spaces.
throwsclauses and line wrapping: 4 spaces.- Use spaces, not tabs (no tab characters).
- Do not wrap lines unnecessarily.
- Always end files with a newline.
- Maximum line length: 140 characters (except for package, import, and URLs).
- Use single space separators.
- No extra space before/after parentheses, commas, colons, or operators unless specified.
- Follow standard whitespace rules around operators, keywords, and braces.
🔠 Encoding¶
- Use UTF-8 encoding.
💡 Declaration¶
- Keep field declarations at the start of the class.
- Prefer
finalover mutable variables when possible. - Use constants for compile-time known values.
- Declare constants as
static final, and avoid reassignment of parameters or loop variables. - Avoid premature variable declarations if not needed immediately.
- Avoid empty blocks (e.g.,
catch,finally,if,switch). - Avoid protected fields in final classes.
- Do not use magic numbers except for -1, 0, and 1.
🧹 Code Cleanup¶
- Avoid unnecessary imports and redundant type qualifications.
- Remove unused local variables, private fields, private methods, parameters, or assignments.
- Remove unnecessary modifiers, casts, or return statements.
- Prefer the use of the diamond operator (
<>) and short array initializers where applicable.
🏷️ Typing and Annotations¶
- Annotate variables without initializers with explicit types.
- Annotate fields and top-level variables if type isn’t obvious.
- Annotate return and parameter types on methods.
- Specify generic type arguments when not inferable.
- Use
Objectonly if no type can be inferred, and document its usage. - Use inclusive start / exclusive end parameters for ranges.
📝 Documentation Standards¶
JavaDoc formatting and structure¶
- JavaDocs must start with a one-sentence summary in its own paragraph.
- Follow this order for block tags:
@param,@return,@throws,@deprecated. - Block tags must be properly indented and formatted.
- Write comments as full sentences.
- Use JavaDoc (
/** ... */) for public APIs; avoid block comments for documentation. - Place JavaDoc comments before metadata annotations.
JavaDoc requirements and scope¶
- JavaDoc may be omitted for trivial members (e.g., simple getters/setters or record components) when the documentation would be redundant.
- All public methods and interfaces must have JavaDoc.
- Only
@Overrideand@Testannotated methods may bypass JavaDoc requirements. - Document public APIs first; private ones only if necessary.
- Avoid empty or malformed JavaDocs.
Content and clarity¶
- Use
@linkto reference other identifiers in JavaDoc. - Clearly explain parameters, return values, and exceptions.
- Document why code exists or how to use it, not just what it does.
Tool-specific annotations¶
- Annotate public APIs using
@Operation,@ApiResponse,@Parameter, etc. - Keep OpenAPI specs synchronized with the actual implementation.
- Document async events using AsyncAPI-compatible annotations or YAML.
Special cases¶
- Include comments in main methods;
mainmust not be uncommented without explanation. - Include terminology explanations, links, and references in library-level documentation.
🌐 API Documentation Guidelines¶
Requirements¶
- Document all public endpoints using Swagger/OpenAPI.
- Include clear descriptions of each endpoint, its parameters, responses, and possible error codes.
- Use standard OpenAPI annotations such as
@Operation,@Parameter, and@ApiResponse. - Keep the OpenAPI specification updated as part of the CI/CD lifecycle.
Environment & Validation¶
- Expose Swagger UI in development environments or restrict access behind authentication in other environments.
- Validate the OpenAPI contract as part of the Definition of Done (DoD) for any API development or changes.
🧪 Testing Guidelines¶
Focus and Coverage¶
- Write unit Tests for business logic.
- Write component Tests for UI.
- Aim for good test coverage.
Tools and Frameworks¶
- For integration Tests with Spring, use
spring-boot-starter-test, which includes utilities like@SpringBootTest,MockMvc, and assertion helpers. - Use
mockito-coreor Spring's built-in mocking tools to simulate dependencies. - Use JUnit 5 for modern testing features.
Structure and Style¶
- Follow AAA pattern (Arrange, Act, Assert).
- Use descriptive test method names (e.g.,
shouldDoXWhenY). - Name test classes and files with the
*Testsuffix (e.g.,UserServiceTest).
📦 Dependency Management (Maven)¶
Usage and Structure¶
- Use a single
pom.xmlas the source of truth unless using a well-justified multi-module setup. - Group dependencies logically using comments (e.g., "Spring Boot", "Testing", "Database").
- Use dependency
managementto centralize versions when using multiple modules.
Versions and Scopes¶
- Prefer exact version numbers for dependencies (avoid
LATESTorRELEASE). - Use
compile(default),provided,runtime,test, orimportscopes appropriately. - Use properties (
<version.spring.boot>) to define shared versions for better maintainability.
Plugins and Configuration¶
- Use the
maven-compiler-pluginto enforce source and target compatibility (e.g., Java 17). - Configure the
maven-surefire-pluginandmaven-failsafe-pluginproperly for test phases. - Validate the build using
mvn verifyormvn clean installas part of CI pipelines.
Dependency Hygiene¶
- Avoid unused dependencies; periodically audit using tools like
mvn dependency:analyze. - Prefer managed BOMs (e.g.,
spring-boot-dependencies) over direct version declarations. - Avoid version conflicts by using dependency mediation and exclusions where necessary.
- Document third-party licenses if required for compliance.
🔄 Mapeo de Entidades y DTOs con MapStruct y Lombok¶
Configuración de Dependencias Maven¶
<properties>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Estructura de Entidades con Lombok¶
// Entidad JPA con Lombok
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
public class user extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String first name;
@Column(nullable = false)
private String last name;
@Column(name = "fecha_nacimiento")
private LocalDate fechaNacimiento;
@Enumerated(EnumType.STRING)
private EstadoUsuario status;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private List<Pedido> pedidos = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "usuario_roles",
joinColumns = @JoinColumn(name = "usuario_id"),
inverseJoinColumns = @JoinColumn(name = "rol_id")
)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Set<role> roles = new HashSet<>();
}
// Entidad base para auditoría
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseEntity {
@CreationTimestamp
@Column(name = "fecha_creacion", nullable = false, updatable = false)
private LocalDateTime fechaCreacion;
@UpdateTimestamp
@Column(name = "fecha_modificacion")
private LocalDateTime fechaModificacion;
@Column(name = "creado_por")
private String creadoPor;
@Column(name = "modificado_por")
private String modificadoPor;
@Version
private Long version;
}
DTOs con Lombok¶
// DTO para respuestas (sin datos sensibles)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsuarioResponseDto {
private Long id;
private String email;
private String first name;
private String last name;
private LocalDate fechaNacimiento;
private EstadoUsuario status;
private LocalDateTime fechaCreacion;
private List<String> roles;
private Integer totalPedidos;
}
// DTO para creación (sin ID ni campos de auditoría)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Valid
public class CrearUsuarioDto {
@NotBlank(message = "El email es obligatorio")
@Email(message = "El formato del email no es válido")
private String email;
@NotBlank(message = "El first name es obligatorio")
@Size(min = 2, max = 50, message = "El first name debe tener entre 2 y 50 caracteres")
private String first name;
@NotBlank(message = "El last name es obligatorio")
@Size(min = 2, max = 50, message = "El last name debe tener entre 2 y 50 caracteres")
private String last name;
@Past(message = "La birth date debe ser anterior a hoy")
private LocalDate fechaNacimiento;
@NotEmpty(message = "Debe asignar al menos un role")
private Set<Long> roleIds;
}
// DTO para actualización (campos opcionales)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Valid
public class ActualizarUsuarioDto {
@Email(message = "El formato del email no es válido")
private String email;
@Size(min = 2, max = 50, message = "El first name debe tener entre 2 y 50 caracteres")
private String first name;
@Size(min = 2, max = 50, message = "El last name debe tener entre 2 y 50 caracteres")
private String last name;
@Past(message = "La birth date debe ser anterior a hoy")
private LocalDate fechaNacimiento;
private EstadoUsuario status;
private Set<Long> roleIds;
}
// DTO para listados (información mínima)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UsuarioListDto {
private Long id;
private String email;
private String nombreCompleto;
private EstadoUsuario status;
private LocalDateTime fechaCreacion;
}
Mapper con MapStruct¶
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
uses = {RolMapper.class}
)
public interface UsuarioMapper {
// Conversión de entidad a DTO de respuesta
@Mapping(target = "nombreCompleto", expression = "java(user.getNombre() + \" \" + user.getApellido())")
@Mapping(target = "roles", source = "roles", qualifiedByName = "rolesToStringList")
@Mapping(target = "totalPedidos", expression = "java(user.getPedidos() != null ? user.getPedidos().size() : 0)")
UsuarioResponseDto toResponseDto(user user);
// Conversión de entidad a DTO de lista
@Mapping(target = "nombreCompleto", expression = "java(user.getNombre() + \" \" + user.getApellido())")
UsuarioListDto toListDto(user user);
// Conversión de DTO de creación a entidad
@Mapping(target = "id", ignore = true)
@Mapping(target = "fechaCreacion", ignore = true)
@Mapping(target = "fechaModificacion", ignore = true)
@Mapping(target = "creadoPor", ignore = true)
@Mapping(target = "modificadoPor", ignore = true)
@Mapping(target = "version", ignore = true)
@Mapping(target = "pedidos", ignore = true)
@Mapping(target = "roles", ignore = true)
@Mapping(target = "status", constant = "ACTIVO")
user toEntity(CrearUsuarioDto dto);
// Actualización de entidad existente
@Mapping(target = "id", ignore = true)
@Mapping(target = "fechaCreacion", ignore = true)
@Mapping(target = "fechaModificacion", ignore = true)
@Mapping(target = "creadoPor", ignore = true)
@Mapping(target = "modificadoPor", ignore = true)
@Mapping(target = "version", ignore = true)
@Mapping(target = "pedidos", ignore = true)
@Mapping(target = "roles", ignore = true)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFromDto(ActualizarUsuarioDto dto, @MappingTarget user user);
// Conversiones de listas
List<UsuarioResponseDto> toResponseDtoList(List<user> users);
List<UsuarioListDto> toListDtoList(List<user> users);
// Métodos auxiliares
@Named("rolesToStringList")
default List<String> rolesToStringList(Set<role> roles) {
if (roles == null || roles.isEmpty()) {
return Collections.emptyList();
}
return roles.stream()
.map(role::getNombre)
.sorted()
.collect(Collectors.toList());
}
// Método para mapear después de la conversión
@AfterMapping
default void setAuditFields(@MappingTarget user user, CrearUsuarioDto dto) {
// Aquí puedes establecer campos de auditoría si es necesario
// Por Example, obtener el user actual del contexto de seguridad
}
}
// Mapper para roles (Example de mapper auxiliar)
@Mapper(componentModel = "spring")
public interface RolMapper {
@Mapping(target = "users", ignore = true)
RolDto toDto(role role);
List<RolDto> toDtoList(List<role> roles);
}
Controlador que NO manipula entidades directamente¶
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Validated
@Tag(name = "users", description = "Gestión de users del sistema")
public class UsuarioController {
private final UsuarioService usuarioService;
@GetMapping
@Operation(summary = "Listar users", description = "Obtiene una lista paginada de users")
@ApiResponse(responseCode = "200", description = "Lista de users obtenida exitosamente")
public ResponseEntity<Page<UsuarioListDto>> listarUsuarios(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir,
@RequestParam(required = false) String filtro) {
Page<UsuarioListDto> users = usuarioService.listarUsuarios(page, size, sortBy, sortDir, filtro);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
@Operation(summary = "Obtener user", description = "Obtiene un user por su ID")
@ApiResponse(responseCode = "200", description = "user encontrado")
@ApiResponse(responseCode = "404", description = "user no encontrado")
public ResponseEntity<UsuarioResponseDto> obtenerUsuario(
@PathVariable @Positive Long id) {
UsuarioResponseDto user = usuarioService.obtenerUsuarioPorId(id);
return ResponseEntity.ok(user);
}
@PostMapping
@Operation(summary = "Crear user", description = "Crea un nuevo user en el sistema")
@ApiResponse(responseCode = "201", description = "user creado exitosamente")
@ApiResponse(responseCode = "400", description = "Datos de entrada inválidos")
@ApiResponse(responseCode = "409", description = "El email ya está en uso")
public ResponseEntity<UsuarioResponseDto> crearUsuario(
@Valid @RequestBody CrearUsuarioDto crearUsuarioDto) {
UsuarioResponseDto usuarioCreado = usuarioService.crearUsuario(crearUsuarioDto);
return ResponseEntity.status(HttpStatus.CREATED).body(usuarioCreado);
}
@PutMapping("/{id}")
@Operation(summary = "Actualizar user", description = "Actualiza un user existente")
@ApiResponse(responseCode = "200", description = "user actualizado exitosamente")
@ApiResponse(responseCode = "404", description = "user no encontrado")
@ApiResponse(responseCode = "400", description = "Datos de entrada inválidos")
public ResponseEntity<UsuarioResponseDto> actualizarUsuario(
@PathVariable @Positive Long id,
@Valid @RequestBody ActualizarUsuarioDto actualizarUsuarioDto) {
UsuarioResponseDto usuarioActualizado = usuarioService.actualizarUsuario(id, actualizarUsuarioDto);
return ResponseEntity.ok(usuarioActualizado);
}
@DeleteMapping("/{id}")
@Operation(summary = "Eliminar user", description = "Elimina un user del sistema")
@ApiResponse(responseCode = "204", description = "user eliminado exitosamente")
@ApiResponse(responseCode = "404", description = "user no encontrado")
public ResponseEntity<Void> eliminarUsuario(@PathVariable @Positive Long id) {
usuarioService.eliminarUsuario(id);
return ResponseEntity.noContent().build();
}
}
Servicio que maneja la lógica de negocio¶
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UsuarioService {
private final UsuarioRepository usuarioRepository;
private final RolRepository rolRepository;
private final UsuarioMapper usuarioMapper;
@Transactional(readOnly = true)
public Page<UsuarioListDto> listarUsuarios(int page, int size, String sortBy, String sortDir, String filtro) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<user> usuariosPage;
if (StringUtils.hasText(filtro)) {
usuariosPage = usuarioRepository.findByNombreContainingIgnoreCaseOrApellidoContainingIgnoreCaseOrEmailContainingIgnoreCase(
filtro, filtro, filtro, pageable);
} else {
usuariosPage = usuarioRepository.findAll(pageable);
}
return usuariosPage.map(usuarioMapper::toListDto);
}
@Transactional(readOnly = true)
public UsuarioResponseDto obtenerUsuarioPorId(Long id) {
user user = usuarioRepository.findById(id)
.orElseThrow(() -> new UsuarioNoEncontradoException("user no encontrado con ID: " + id));
return usuarioMapper.toResponseDto(user);
}
public UsuarioResponseDto crearUsuario(CrearUsuarioDto crearUsuarioDto) {
// Validar que el email no exista
if (usuarioRepository.existsByEmail(crearUsuarioDto.getEmail())) {
throw new EmailYaExisteException("Ya existe un user con el email: " + crearUsuarioDto.getEmail());
}
// Convertir DTO a entidad
user user = usuarioMapper.toEntity(crearUsuarioDto);
// Asignar roles
Set<role> roles = rolRepository.findAllById(crearUsuarioDto.getRoleIds())
.stream()
.collect(Collectors.toSet());
if (roles.size() != crearUsuarioDto.getRoleIds().size()) {
throw new RolNoEncontradoException("Uno o más roles especificados no existen");
}
user.setRoles(roles);
// Establecer campos de auditoría
String usuarioActual = obtenerUsuarioActual();
user.setCreadoPor(usuarioActual);
user.setModificadoPor(usuarioActual);
// Guardar
user usuarioGuardado = usuarioRepository.save(user);
log.info("user creado exitosamente con ID: {}", usuarioGuardado.getId());
return usuarioMapper.toResponseDto(usuarioGuardado);
}
public UsuarioResponseDto actualizarUsuario(Long id, ActualizarUsuarioDto actualizarUsuarioDto) {
user user = usuarioRepository.findById(id)
.orElseThrow(() -> new UsuarioNoEncontradoException("user no encontrado con ID: " + id));
// Validar email único si se está cambiando
if (StringUtils.hasText(actualizarUsuarioDto.getEmail()) &&
!user.getEmail().equals(actualizarUsuarioDto.getEmail())) {
if (usuarioRepository.existsByEmail(actualizarUsuarioDto.getEmail())) {
throw new EmailYaExisteException("Ya existe un user con el email: " + actualizarUsuarioDto.getEmail());
}
}
// Actualizar campos usando MapStruct
usuarioMapper.updateEntityFromDto(actualizarUsuarioDto, user);
// Actualizar roles si se proporcionaron
if (actualizarUsuarioDto.getRoleIds() != null && !actualizarUsuarioDto.getRoleIds().isEmpty()) {
Set<role> nuevosRoles = rolRepository.findAllById(actualizarUsuarioDto.getRoleIds())
.stream()
.collect(Collectors.toSet());
if (nuevosRoles.size() != actualizarUsuarioDto.getRoleIds().size()) {
throw new RolNoEncontradoException("Uno o más roles especificados no existen");
}
user.setRoles(nuevosRoles);
}
// Actualizar campos de auditoría
user.setModificadoPor(obtenerUsuarioActual());
user usuarioActualizado = usuarioRepository.save(user);
log.info("user actualizado exitosamente con ID: {}", usuarioActualizado.getId());
return usuarioMapper.toResponseDto(usuarioActualizado);
}
public void eliminarUsuario(Long id) {
if (!usuarioRepository.existsById(id)) {
throw new UsuarioNoEncontradoException("user no encontrado con ID: " + id);
}
usuarioRepository.deleteById(id);
log.info("user eliminado exitosamente con ID: {}", id);
}
private String obtenerUsuarioActual() {
// Implementar lógica para obtener el user actual del contexto de seguridad
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}
Mejores Prácticas para MapStruct + Lombok¶
✅ Configuración Correcta¶
- Orden de procesadores: Lombok debe procesarse antes que MapStruct
- Binding: Usar
lombok-mapstruct-bindingpara compatibilidad - Component Model: Usar
componentModel = "spring"para inyección de dependencias
✅ Separación de Responsabilidades¶
- Controladores: Solo manejan DTOs, nunca entidades
- Servicios: Convierten entre DTOs y entidades usando mappers
- Repositorios: Solo trabajan con entidades
✅ Mapeo Seguro¶
- Ignorar campos sensibles: ID, campos de auditoría, relaciones complejas
- Validación: Usar
@Validen DTOs y validaciones de negocio en servicios - Null Safety: Configurar
nullValuePropertyMappingStrategyapropiadamente
✅ Performance¶
- Lazy Loading: Evitar cargar relaciones innecesarias
- Proyecciones: Usar DTOs específicos para listados
- Batch Operations: Mapear listas completas cuando sea posible
❌ Evitar¶
- Entidades en controladores: Nunca exponer entidades JPA directamente
- Mapeo manual: Usar MapStruct en lugar de mapeo manual
- DTOs anémicos: Incluir validaciones y lógica de presentación en DTOs
- Circular references: Usar
@ToString.Excludey@EqualsAndHashCode.Exclude
Esta configuración garantiza una separación limpia entre la capa de presentación y la capa de persistencia, evitando la manipulación directa de entidades en los controladores y proporcionando un mapeo eficiente y mantenible.
🌊 Java Streams y Características Modernas¶
Uso de Streams API¶
✅ Operaciones de Filtrado y Transformación¶
// Filtrar y transformar listas
public List<UsuarioDto> obtenerUsuariosActivos() {
return usuarioRepository.findAll()
.stream()
.filter(user -> EstadoUsuario.ACTIVO.equals(user.getEstado()))
.filter(user -> Objects.nonNull(user.getEmail()))
.map(usuarioMapper::toDto)
.collect(Collectors.toList());
}
// Agrupar por criterios
public Map<EstadoUsuario, List<UsuarioDto>> agruparUsuariosPorEstado() {
return usuarioRepository.findAll()
.stream()
.collect(Collectors.groupingBy(
user::getEstado,
Collectors.mapping(usuarioMapper::toDto, Collectors.toList())
));
}
// Buscar elementos específicos
public Optional<UsuarioDto> buscarUsuarioPorEmail(String email) {
return usuarioRepository.findAll()
.stream()
.filter(user -> Objects.equals(email, user.getEmail()))
.findFirst()
.map(usuarioMapper::toDto);
}
✅ Operaciones de Agregación¶
// Contar elementos con condiciones
public long contarUsuariosActivos() {
return usuarioRepository.findAll()
.stream()
.filter(user -> EstadoUsuario.ACTIVO.equals(user.getEstado()))
.count();
}
// Calcular estadísticas
public EstadisticasUsuarios calcularEstadisticas() {
List<user> users = usuarioRepository.findAll();
long totalUsuarios = users.size();
long usuariosActivos = users.stream()
.filter(user -> EstadoUsuario.ACTIVO.equals(user.getEstado()))
.count();
OptionalDouble edadPromedio = users.stream()
.filter(user -> Objects.nonNull(user.getFechaNacimiento()))
.mapToInt(user -> Period.between(user.getFechaNacimiento(), LocalDate.now()).getYears())
.average();
return EstadisticasUsuarios.builder()
.totalUsuarios(totalUsuarios)
.usuariosActivos(usuariosActivos)
.edadPromedio(edadPromedio.orElse(0.0))
.build();
}
// Encontrar valores máximos/mínimos
public Optional<user> usuarioMasReciente() {
return usuarioRepository.findAll()
.stream()
.filter(user -> Objects.nonNull(user.getFechaCreacion()))
.max(Comparator.comparing(user::getFechaCreacion));
}
✅ Operaciones Complejas con Streams¶
// Procesamiento de datos anidados
public List<String> obtenerEmailsDeUsuariosConRolAdmin() {
return usuarioRepository.findAll()
.stream()
.filter(user -> user.getRoles()
.stream()
.anyMatch(role -> "ADMIN".equals(role.getNombre())))
.map(user::getEmail)
.filter(Objects::nonNull)
.distinct()
.sorted()
.collect(Collectors.toList());
}
// Transformaciones complejas
public Map<String, Set<String>> obtenerRolesPorDominio() {
return usuarioRepository.findAll()
.stream()
.filter(user -> Objects.nonNull(user.getEmail()))
.collect(Collectors.groupingBy(
user -> user.getEmail().substring(user.getEmail().indexOf("@") + 1),
Collectors.flatMapping(
user -> user.getRoles().stream().map(role::getNombre),
Collectors.toSet()
)
));
}
// Validaciones con streams
public boolean validarUsuariosUnicos(List<CrearUsuarioDto> users) {
Set<String> emails = users.stream()
.map(CrearUsuarioDto::getEmail)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return emails.size() == users.size();
}
Uso de Objects para Comparaciones y Validaciones¶
✅ Comparaciones Null-Safe¶
// Comparación segura de objetos
public boolean sonUsuariosIguales(user usuario1, user usuario2) {
return Objects.equals(usuario1.getEmail(), usuario2.getEmail()) &&
Objects.equals(usuario1.getNombre(), usuario2.getNombre()) &&
Objects.equals(usuario1.getApellido(), usuario2.getApellido());
}
// Validaciones de nulidad
public void validarUsuario(user user) {
if (Objects.isNull(user)) {
throw new IllegalArgumentException("El user no puede ser nulo");
}
if (Objects.isNull(user.getEmail()) || user.getEmail().trim().isEmpty()) {
throw new IllegalArgumentException("El email es obligatorio");
}
Objects.requireNonNull(user.getNombre(), "El first name es obligatorio");
Objects.requireNonNull(user.getApellido(), "El last name es obligatorio");
}
// Valores por defecto con Objects
public String obtenerNombreCompleto(user user) {
String first name = Objects.requireNonNullElse(user.getNombre(), "Sin first name");
String last name = Objects.requireNonNullElse(user.getApellido(), "Sin last name");
return first name + " " + last name;
}
✅ Hash y Equals con Objects¶
@Entity
public class user {
// ... otros campos
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
user user = (user) obj;
return Objects.equals(id, user.id) &&
Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email);
}
@Override
public String toString() {
return String.format("user{id=%d, email='%s', first name='%s'}",
id, email, first name);
}
}
Uso de Optional para Manejo de Valores Opcionales¶
✅ Operaciones con Optional¶
// Búsquedas que pueden no encontrar resultados
public Optional<UsuarioDto> buscarUsuarioPorId(Long id) {
return usuarioRepository.findById(id)
.map(usuarioMapper::toDto);
}
// Encadenamiento de operaciones opcionales
public Optional<String> obtenerDominioDelEmail(Long usuarioId) {
return usuarioRepository.findById(usuarioId)
.map(user::getEmail)
.filter(email -> email.contains("@"))
.map(email -> email.substring(email.indexOf("@") + 1));
}
// Valores por defecto con Optional
public String obtenerNombreUsuario(Long usuarioId) {
return usuarioRepository.findById(usuarioId)
.map(user::getNombre)
.orElse("user Desconocido");
}
// Lanzar excepciones con Optional
public UsuarioDto obtenerUsuarioObligatorio(Long id) {
return usuarioRepository.findById(id)
.map(usuarioMapper::toDto)
.orElseThrow(() -> new UsuarioNoEncontradoException("user no encontrado: " + id));
}
// Operaciones condicionales con Optional
public void enviarNotificacionSiTieneEmail(Long usuarioId, String mensaje) {
usuarioRepository.findById(usuarioId)
.map(user::getEmail)
.filter(email -> !email.trim().isEmpty())
.ifPresent(email -> notificationService.enviarEmail(email, mensaje));
}
Características Modernas de Java¶
✅ Text Blocks (Java 15+)¶
public String generarReporteUsuarios(List<UsuarioDto> users) {
String template = """
<!DOCTYPE html>
<html>
<head>
<title>Reporte de users</title>
</head>
<body>
<h1>Total de users: %d</h1>
<table>
<tr>
<th>Email</th>
<th>first name</th>
<th>status</th>
</tr>
%s
</table>
</body>
</html>
""";
String filas = users.stream()
.map(user -> String.format(
"<tr><td>%s</td><td>%s</td><td>%s</td></tr>",
user.getEmail(),
user.getNombre(),
user.getEstado()
))
.collect(Collectors.joining("\n"));
return String.format(template, users.size(), filas);
}
✅ Pattern Matching con instanceof (Java 16+)¶
public String procesarEntidad(Object entidad) {
return switch (entidad) {
case user user -> String.format("user: %s (%s)",
user.getNombre(), user.getEmail());
case role role -> String.format("role: %s", role.getNombre());
case String texto -> String.format("Texto: %s", texto);
case null -> "Entidad nula";
default -> "Tipo de entidad desconocido: " + entidad.getClass().getSimpleName();
};
}
✅ Records para DTOs Inmutables (Java 14+)¶
// Record para respuestas simples
public record UsuarioResumenDto(
Long id,
String email,
String nombreCompleto,
EstadoUsuario status,
LocalDateTime fechaCreacion
) {
// Constructor compacto con validaciones
public UsuarioResumenDto {
Objects.requireNonNull(email, "Email no puede ser nulo");
Objects.requireNonNull(nombreCompleto, "first name completo no puede ser nulo");
Objects.requireNonNull(status, "status no puede ser nulo");
}
// Métodos adicionales
public boolean esActivo() {
return EstadoUsuario.ACTIVO.equals(status);
}
public String getDominio() {
return email.substring(email.indexOf("@") + 1);
}
}
// Record para estadísticas
public record EstadisticasUsuarios(
long totalUsuarios,
long usuariosActivos,
long usuariosInactivos,
double edadPromedio,
Map<String, Long> usuariosPorDominio
) {
public double porcentajeActivos() {
return totalUsuarios > 0 ? (double) usuariosActivos / totalUsuarios * 100 : 0;
}
}
✅ Sealed Classes para Jerarquías Controladas (Java 17+)¶
// Jerarquía sellada para eventos
public sealed interface EventoUsuario
permits UsuarioCreado, UsuarioActualizado, UsuarioEliminado {
Long usuarioId();
LocalDateTime timestamp();
String tipoEvento();
}
public record UsuarioCreado(
Long usuarioId,
String email,
LocalDateTime timestamp
) implements EventoUsuario {
@Override
public String tipoEvento() {
return "USUARIO_CREADO";
}
}
public record UsuarioActualizado(
Long usuarioId,
Map<String, Object> cambios,
LocalDateTime timestamp
) implements EventoUsuario {
@Override
public String tipoEvento() {
return "USUARIO_ACTUALIZADO";
}
}
public record UsuarioEliminado(
Long usuarioId,
String motivoEliminacion,
LocalDateTime timestamp
) implements EventoUsuario {
@Override
public String tipoEvento() {
return "USUARIO_ELIMINADO";
}
}
// Procesamiento con pattern matching
public void procesarEvento(EventoUsuario evento) {
switch (evento) {
case UsuarioCreado creado -> {
log.info("user creado: {} con email: {}",
creado.usuarioId(), creado.email());
enviarEmailBienvenida(creado.email());
}
case UsuarioActualizado actualizado -> {
log.info("user {} actualizado. Cambios: {}",
actualizado.usuarioId(), actualizado.cambios());
auditService.registrarCambios(actualizado);
}
case UsuarioEliminado eliminado -> {
log.info("user {} eliminado. Motivo: {}",
eliminado.usuarioId(), eliminado.motivoEliminacion());
limpiarDatosUsuario(eliminado.usuarioId());
}
}
}
Mejores Prácticas para Características Modernas¶
✅ Streams¶
- Usar para transformaciones: Preferir streams para operaciones de filtrado, mapeo y reducción
- Evitar efectos secundarios: No modificar status externo dentro de streams
- Paralelización cuidadosa: Usar
parallelStream()solo cuando sea beneficioso - Collectors apropiados: Usar collectors específicos para mejor performance
✅ Objects¶
- Comparaciones null-safe: Siempre usar
Objects.equals()para comparaciones - Validaciones: Usar
Objects.requireNonNull()para validaciones tempranas - Hash codes: Usar
Objects.hash()para implementarhashCode()
✅ Optional¶
- No como parámetros: Evitar Optional como parámetros de métodos
- No en campos: No usar Optional como campos de clase
- Encadenamiento: Aprovechar métodos como
map(),filter(),flatMap()
✅ Records¶
- DTOs inmutables: Usar para DTOs que no necesitan mutabilidad
- Validaciones en constructor: Implementar validaciones en constructor compacto
- Métodos derivados: Agregar métodos que calculen valores derivados
❌ Evitar¶
- Streams innecesarios: No usar streams para operaciones simples
- Optional.get(): Evitar llamar
get()sin verificar presencia - Null checks manuales: Usar Objects en lugar de comparaciones manuales con null
- Mutabilidad en records: No intentar hacer records mutables
🧱 Principios SOLID en Architecture y Diseño¶
Los principios SOLID ayudan a diseñar software flexible, extensible y fácil de mantener. Deben aplicarse tanto a nivel de clases como de servicios y módulos.
S — Single Responsibility Principle (SRP)¶
Cada clase, componente o módulo debe tener una única razón para cambiar. ✔️ Buenas prácticas:
- Separar validaciones, lógica de negocio, acceso a datos y presentación.
- Dividir servicios monolíticos en componentes cohesivos.
// ❌ Servicio violando SRP
public class UsuarioService {
public void crearUsuario(...) {...}
public void enviarEmail(...) {...}
public void auditarOperacion(...) {...}
}
// ✅ Separación de responsabilidades
public class UsuarioService { ... }
public class EmailService { ... }
public class AuditoriaService { ... }
O — Open/Closed Principle (OCP)¶
El software debe estar abierto para extensión pero cerrado para modificación. ✔️ Buenas prácticas:
- Usar interfaces, herencia, patrones de estrategia o decorador.
- Aplicar polimorfismo en lugar de
ifoswitchbasados en tipos.
L — Liskov Substitution Principle (LSP)¶
Las clases hijas deben poder usarse como sus clases base sin romper funcionalidad. ✔️ Buenas prácticas:
- No sobrescribir métodos si cambian el contrato.
- No lanzar excepciones inesperadas.
I — Interface Segregation Principle (ISP)¶
Las interfaces deben ser específicas y enfocadas. ✔️ Buenas prácticas:
- Dividir interfaces con muchos métodos en varias más pequeñas.
- No forzar a las clases a implementar métodos que no usan.
D — Dependency Inversion Principle (DIP)¶
Depender de abstracciones, no de implementaciones. ✔️ Buenas prácticas:
- Usar interfaces e inyección de dependencias.
- Configurar beans en Spring usando
@Component,@Serviceo@Configuration.
public interface Notificador { void enviar(String mensaje); }
@Service
public class EmailNotificador implements Notificador { ... }
🧪 Tests Unitarias con JUnit 5 y Mockito¶
Las Tests deben ser claras, aisladas y de rápida ejecución. Deben cubrir la lógica de negocio, validaciones y condiciones límite.
✅ Recomendaciones generales¶
- Usar
@DisplayNamepara claridad. - AAA: Arrange (preparar), Act (ejecutar), Assert (verificar).
- Evitar mocks innecesarios; testear la lógica, no la implementación.
✅ Estructura de clases de test¶
- first name:
UsuarioServiceTest,PlanValidatorTest, etc. - Métodos:
shouldReturnXWhenY(),deberiaHacerXCuandoY().
✅ Herramientas y anotaciones clave¶
| Herramienta | Anotaciones |
|---|---|
| JUnit 5 | @Test, @DisplayName, @Nested, @BeforeEach |
| Mockito | @Mock, @InjectMocks, verify(), when() |
✅ Example completo¶
@ExtendWith(MockitoExtension.class)
class UsuarioServiceTest {
@Mock UsuarioRepository usuarioRepository;
@Mock RolRepository rolRepository;
@InjectMocks UsuarioService usuarioService;
@Test
@DisplayName("Debería crear user cuando el email es único")
void shouldCreateUsuarioWhenEmailIsUnique() {
CrearUsuarioDto dto = CrearUsuarioDto.builder().email("nuevo@dominio.com").build();
when(usuarioRepository.existsByEmail("nuevo@dominio.com")).thenReturn(false);
when(usuarioRepository.save(any())).thenReturn(new user(1L, "nuevo@dominio.com", ...));
UsuarioResponseDto response = usuarioService.crearUsuario(dto);
assertEquals("nuevo@dominio.com", response.getEmail());
verify(usuarioRepository).save(any(user.class));
}
}
📘 Documentación con OpenAPI (Swagger)¶
✅ Buenas prácticas de documentación¶
- Anotar todos los controladores con
@Tag, métodos con@Operation, respuestas con@ApiResponse. - Describir campos importantes con
@Schema. - Documentar los posibles errores y códigos de status.
✅ Validaciones con @Valid¶
- Siempre usar
@Validen los métodos que reciben DTOs como@RequestBody. - Usar anotaciones como
@NotNull,@Email,@Sizeen los campos del DTO. - Anotar el controlador con
@Validatedpara permitir validaciones en@PathVariableo@RequestParam.
✅ Estandarización de paths con API name¶
📌 Regla: el path base del Swagger debe contener el API name.
Example:
- API name:
ai-demo-api - Swagger UI:
http://localhost:8080/ai-demo-api/swagger-ui.html - Base path:
/ai-demo-api
springdoc:
api-docs:
path: /ai-demo-api/v3/api-docs
swagger-ui:
path: /ai-demo-api/swagger-ui.html
server:
servlet:
context-path: /ai-demo-api
✅ Example completo¶
@RestController
@RequestMapping("/users")
@Validated
@Tag(name = "users", description = "Operaciones relacionadas a users")
public class UsuarioController {
@PostMapping
@Operation(summary = "Crear user", description = "Crea un nuevo user")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "user creado"),
@ApiResponse(responseCode = "400", description = "Datos inválidos"),
@ApiResponse(responseCode = "409", description = "Email duplicado")
})
public ResponseEntity<UsuarioResponseDto> crear(
@Valid @RequestBody CrearUsuarioDto dto) {
...
}
}
🛠️ Migraciones de Base de Datos con Flyway¶
✅ Estructura y convenciones¶
- Ubicación de scripts:
src/main/resources/db/migration/ - Nomenclatura:
V1__init_schema.sql,V2__add_user_table.sql - Evitar scripts destructivos o modificables una vez ejecutados.
✅ Configuración recomendada en application.yml¶
spring:
flyway:
enabled: true
locations: classpath:/db/migration
baseline-on-migrate: true
validate-on-migrate: true
clean-disabled: true
✅ Buenas prácticas¶
- Cada script debe ser autocontenible e idempotente.
- Incluir comentarios para contexto de cambios.
- Validate locally antes de subir a entornos superiores.
- Nunca modificar scripts ya aplicados.
✅ Example¶
-- V2__add_usuario_table.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(150) NOT NULL UNIQUE,
first name VARCHAR(100) NOT NULL,
last name VARCHAR(100) NOT NULL,
fecha_nacimiento DATE,
status VARCHAR(20),
fecha_creacion TIMESTAMP DEFAULT now()
);
📄 Mantenimiento del README.md y Documentación en docs/¶
✅ Objetivo¶
Establecer una convención clara para mantener el README.md del proyecto como fuente inicial de referencia y navegación hacia la documentación técnica completa. Toda documentación adicional debe residir en la carpeta docs/ con formato Markdown compatible con MkDocs.
📘 project structure¶
├── README.md
├── docs/
│ ├── index.md
│ ├── Architecture.md
│ ├── api.md
│ ├── uso.md
│ └── troubleshooting.md
✅ Reglas para el README.md¶
- Should serve as documento de entrada al repositorio: descripción, uso rápido, e índice de documentación.
- Debe incluir enlaces actualizados hacia la documentación en
docs/. - must stay updated cada vez que se agregan, eliminan o modifican componentes relevantes del sistema.
- Debe incluir al menos las siguientes secciones mínimas:
Example de plantilla base:¶
# AI Demo API
API de Example para validación de users y manejo de planes.
## 📚 Documentación
- [Full documentation](./docs/index.md)
- [Architecture](./docs/Architecture.md)
- [API endpoints](./docs/api.md)
- [Usage guide](./docs/uso.md)
- [Troubleshooting](./docs/troubleshooting.md)
## 🚀 How to run
```bash
./mvnw spring-boot:run
🧪 Tests¶
./mvnw test
🛠️ Requirements¶
- Java 17+
- Maven 3.8+
📂 Code structure¶
src/main/java/...: código de negociosrc/test/java/...: Tests unitarias e integración
---
### 📁 Reglas para la carpeta `docs/`
- Debe contener documentación modular separada por tema (ej: `api.md`, `Architecture.md`, `flujos.md`, etc.)
- Todos los archivos deben estar en formato **Markdown** compatible con **MkDocs**.
- Se debe incluir un `index.md` como página de inicio.
- Each file must start con un título `#` claro, y opcionalmente incluir una tabla de contenido (`[TOC]` o índice manual).
- Se debe mantener actualizado como parte del proceso de cambio de código, especialmente para:
- Nuevos endpoints
- Cambios en flujos o Architecture
- Consideraciones de seguridad
- Reglas de validación o nuevos campos en DTOs
- Si se usan herramientas automáticas como Swagger2Markup, deben documentarse en un archivo `docs/README.md`.
---
### 🧪 Validación en CI (opcional)
- It is recommended to include un paso en el pipeline de CI/CD que valide la existencia del `docs/index.md`.
- Se puede agregar un verificador de enlaces rotos para evitar referencias obsoletas.
---
### 🔄 Reglas de sincronización
| Evento | Acción obligatoria |
|----------------------------------------|---------------------------------------------|
| Se agrega o modifica un endpoint | Actualizar `docs/api.md` y enlazar en `README.md` |
| Se cambia flujo de negocio | Actualizar `docs/Architecture.md` |
| Se modifica validación o DTO | Actualizar `docs/api.md` o `docs/uso.md` |
| Se agrega un feature significativo | Incluir resumen en `README.md` |
| Se cambia configuración del proyecto | Documentar en `docs/uso.md` o `index.md` |
---
---
# ✅ Extended Java Backend Guidelines (2025 Edition)
## 🔄 Enhancements Summary
This document extends the original Copilot-based backend conventions with the following additions:
- Usage of Specifications, Sorting and Pagination for all GET endpoints returning entity lists
- Standardized structured logging for both success and failure paths
- Strict separation of concerns (no business logic in controllers)
- Centralized validation and exception handling
- Integration of Java modern features (Java 8 to 21)
- Use of parallelization (CompletableFuture, Virtual Threads) when performance requires it
## 📥 GET Endpoints: Filtering + Ordering + Pagination
All GET list endpoints must:
- Support dynamic filtering via `JpaSpecificationExecutor`
- Accept Spring Data `Pageable` parameter
- Allow ordering via `sort` query param (e.g. `?sort=id,desc`)
Example:
```java
@GetMapping
public ResponseEntity<Page<UserListDto>> getAll(
@Parameter(hidden = true) Pageable pageable,
@ParameterObject UserFilter filter
) {
log.info("Fetching users with filters: {}", filter);
return ResponseEntity.ok(userService.getAll(filter, pageable));
}
📦 Service Layer: Specifications¶
public Page<UserListDto> getAll(UserFilter filter, Pageable pageable) {
Specification<User> spec = userSpecificationBuilder.from(filter);
return userRepository.findAll(spec, pageable).map(userMapper::toListDto);
}
🧾 Logging Best Practices¶
- Always use
@Slf4j(Lombok) - Log controller entry, input, and output where appropriate
- Log error messages with full context
try {
log.info("[USER CREATE] Request: {}", requestDto);
...
} catch (Exception e) {
log.error("[USER CREATE ERROR] {}", e.getMessage(), e);
throw e;
}
❌ Controllers Must Not Contain Business Logic¶
Allowed in controllers:
- Request validation (@Valid)
- Calling services
- Mapping request to service params
Not allowed: - Any logic, repository calls, or state transformations
🛡️ Validation and Error Flows¶
- Annotate DTOs with
@Valid - Use
@Validatedin controller - Create
@ControllerAdviceto manage: - Validation errors (
MethodArgumentNotValidException) ConstraintViolationException- Business Exceptions
🧠 Modern Java (8 → 21)¶
Recommended features for backend:
| Feature | Version | Usage |
|---|---|---|
| Lambdas + Stream API | 8 | Functional processing |
| Optional | 8 | Null-safe results |
| CompletableFuture | 8 | Async execution |
| HttpClient | 11 | Modern HTTP integration |
| Text Blocks | 15 | Multi-line string literals |
| Records | 16 | DTOs with less boilerplate |
| Pattern Matching | 16+ | Cleaner conditional logic |
| Virtual Threads | 21 | Highly concurrent request handling |
| Sequenced Collections | 21 | Ordered Sets, Maps |
⚙️ Parallelization¶
Use when performance or volume requires it:
CompletableFuture.supplyAsync(...)Thread.ofVirtual().start(...)in Java 21+- Ensure proper error propagation and tracing context
All previous conventions (naming, testing, documentation, DTOs with MapStruct, Lombok usage, OpenAPI, Flyway) remain 100% applicable and recommended.