Saltar a contenido

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, not HTTPClient).
  • Avoid abbreviations unless more common than the full term (e.g., URL is fine, Usr is not).
  • Use consistent terms across codebase (e.g., always use customer instead of alternating user, client, etc.).
  • For boolean variables and methods:
    • Prefer positive forms: isEnabled, not isDisabled
    • Use isX() instead of getX()
    • 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
  • Avoid method or field names that match the enclosing class name
  • Avoid suspicious or incorrect method signature overrides like equals() or hashCode() that violate contracts
  • Prefer descriptive nouns last in identifiers (e.g., userProfileManager, not managerUserProfile)
  • Make method and class names read like natural language when possible (e.g., UserService.getUserByEmail())
  • Do not use reserved keywords like record, var, or sealed as identifiers
  • Ensure the outer type name matches the filename (e.g., class UserService in file UserService.java)
  • Use descriptive and intention-revealing names (e.g., retryLimit instead of rl, isVisible instead of flag; avoid userDataObject — prefer userProfile or userDetails)
  • Avoid noise words in identifiers (e.g., avoid the, data, object unless truly meaningful)
  • Use context in names when needed, but avoid overly long or repetitive identifiers (e.g., userRepository is better than applicationCoreUserDataAccessRepository)
  • Follow existing naming conventions for type parameters in your codebase when consistency outweighs strict style (e.g., continuing with UserT if 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.
  • default cases must be last in switch statements.
  • 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.
  • throws clauses 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 final over 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 Object only 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 @Override and @Test annotated methods may bypass JavaDoc requirements.
  • Document public APIs first; private ones only if necessary.
  • Avoid empty or malformed JavaDocs.

Content and clarity

  • Use @link to 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; main must 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-core or 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 *Test suffix (e.g., UserServiceTest).

📦 Dependency Management (Maven)

Usage and Structure

  • Use a single pom.xml as 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 management to centralize versions when using multiple modules.

Versions and Scopes

  • Prefer exact version numbers for dependencies (avoid LATEST or RELEASE).
  • Use compile (default), provided, runtime, test, or import scopes appropriately.
  • Use properties (<version.spring.boot>) to define shared versions for better maintainability.

Plugins and Configuration

  • Use the maven-compiler-plugin to enforce source and target compatibility (e.g., Java 17).
  • Configure the maven-surefire-plugin and maven-failsafe-plugin properly for test phases.
  • Validate the build using mvn verify or mvn clean install as 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-binding para 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 @Valid en DTOs y validaciones de negocio en servicios
  • Null Safety: Configurar nullValuePropertyMappingStrategy apropiadamente

✅ 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.Exclude y @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 implementar hashCode()

✅ 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 if o switch basados 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, @Service o @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 @DisplayName para 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 @Valid en los métodos que reciben DTOs como @RequestBody.
  • Usar anotaciones como @NotNull, @Email, @Size en los campos del DTO.
  • Anotar el controlador con @Validated para permitir validaciones en @PathVariable o @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 negocio
  • src/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 @Validated in controller
  • Create @ControllerAdvice to 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.