- Visión General
- Principios Arquitectónicos
- Estructura de Capas
- Reglas de Dependencia
- Patrones de Configuración
- Guía de Implementación
- Ejemplos Prácticos
- Anti-Patrones (Qué NO Hacer)
Este proyecto sigue una arquitectura hexagonal (puertos y adaptadores) con un enfoque pragmático que prioriza la simplicidad y mantenibilidad sobre la pureza teórica.
- ✅ Bean-based configuration: Usamos
@Beanen clases*Config.javaen lugar de anotaciones en el dominio - ✅ Separación de capas: Domain, Infrastructure, Presentation
- ✅ Inyección de dependencias manual: Sin
@Service,@Repository,@Componenten dominio - ✅ POJOs en dominio: Clases simples sin dependencias de frameworks
El dominio NO debe conocer la infraestructura
// ❌ INCORRECTO - Dominio importando infraestructura
package com.outfitlab.project.domain.useCases.user;
import com.outfitlab.project.infrastructure.model.UserEntity; // ❌
import org.springframework.stereotype.Service; // ❌
@Service // ❌
public class RegisterUser {
// ...
}// ✅ CORRECTO - Dominio independiente
package com.outfitlab.project.domain.useCases.user;
import com.outfitlab.project.domain.interfaces.repositories.UserRepository; // ✅
import com.outfitlab.project.domain.model.UserModel; // ✅
public class RegisterUser { // ✅ POJO sin anotaciones
private final UserRepository userRepository;
public RegisterUser(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}Toda la configuración se hace en clases *Config.java en la capa de infraestructura
// ✅ CORRECTO
@Configuration
public class UserConfig {
@Bean
public UserRepository userRepository(UserJpaRepository jpaRepo) {
return new UserRepositoryImpl(jpaRepo);
}
@Bean
public RegisterUser registerUser(UserRepository userRepository) {
return new RegisterUser(userRepository);
}
}Está permitido inyectar infraestructura directamente en use cases cuando simplifica el código
// ✅ ACEPTABLE - Inyección directa de infraestructura
@Bean
public RegisterUser registerUser(
UserRepository userRepository,
PasswordEncoder passwordEncoder, // ← Spring Security
AuthenticationManager authManager, // ← Spring Security
TokenRepository tokenRepository, // ← JPA Repository
JwtService jwtService, // ← Servicio de infraestructura
UserJpaRepository userJpaRepository, // ← JPA Repository
GmailGateway gmailGateway
) {
return new RegisterUser(userRepository, passwordEncoder, authManager,
tokenRepository, jwtService, userJpaRepository, gmailGateway);
}Motivación: Evitar crear abstracciones innecesarias que solo agregan complejidad sin valor real.
src/main/java/com/outfitlab/project/
│
├── domain/ # Capa de Dominio (Lógica de Negocio)
│ ├── model/ # Modelos de dominio (POJOs)
│ │ ├── UserModel.java
│ │ ├── Role.java # ← Conceptos de negocio van aquí
│ │ └── ...
│ ├── interfaces/ # Contratos (interfaces)
│ │ ├── repositories/
│ │ │ ├── UserRepository.java
│ │ │ └── ...
│ │ └── gateways/
│ │ └── GmailGateway.java
│ ├── useCases/ # Casos de uso (lógica de negocio)
│ │ ├── user/
│ │ │ ├── RegisterUser.java # ← POJO sin anotaciones
│ │ │ ├── LoginUser.java
│ │ │ └── ...
│ │ └── garment/
│ │ └── ...
│ └── exceptions/ # Excepciones de dominio
│ └── UserNotFoundException.java
│
├── infrastructure/ # Capa de Infraestructura (Detalles técnicos)
│ ├── config/ # Configuración de Spring
│ │ ├── UserConfig.java # ← Beans de User
│ │ ├── GarmentConfig.java # ← Beans de Garment
│ │ └── security/
│ │ ├── SecurityConfig.java
│ │ ├── AuthResponse.java # ← DTOs de infraestructura
│ │ └── jwt/
│ │ ├── JwtService.java
│ │ └── Token.java # ← Entidad JPA
│ ├── model/ # Entidades JPA
│ │ ├── UserEntity.java
│ │ ├── PrendaEntity.java
│ │ └── ...
│ ├── repositories/ # Implementaciones de repositorios
│ │ ├── UserRepositoryImpl.java # ← Implementa UserRepository
│ │ ├── interfaces/ # JPA Repositories
│ │ │ ├── UserJpaRepository.java
│ │ │ └── ...
│ │ └── ...
│ ├── services/ # Servicios de infraestructura
│ │ └── ...
│ └── gateways/ # Implementaciones de gateways
│ └── GmailGatewayImpl.java
│
└── presentation/ # Capa de Presentación (API REST)
├── controllers/
│ ├── UserController.java
│ └── ...
└── dto/
├── UserDTO.java
└── ...
Presentation → Domain
Presentation → Infrastructure
Infrastructure → Domain
Domain → (NADA, solo Java estándar)
Domain → Infrastructure ❌
Domain → Presentation ❌
Domain → Spring ❌
Domain → JPA ❌
Para verificar que no hay violaciones:
# Buscar imports de infraestructura en dominio (NO debe haber resultados)
grep -r "import com.outfitlab.project.infrastructure" src/main/java/com/outfitlab/project/domain/
# Buscar anotaciones Spring en dominio (NO debe haber resultados)
grep -r "@Service\|@Repository\|@Component" src/main/java/com/outfitlab/project/domain/Cada módulo de dominio tiene su propia clase de configuración:
UserConfig.java→ Beans relacionados con usuariosGarmentConfig.java→ Beans relacionados con prendasCombinationConfig.java→ Beans relacionados con combinacionesSubscriptionConfig.java→ Beans relacionados con suscripciones
@Configuration
public class UserConfig {
// 1. Repositorios del dominio
@Bean
public UserRepository userRepository(UserJpaRepository jpaRepo, BrandJpaRepository brandRepo) {
return new UserRepositoryImpl(jpaRepo, brandRepo);
}
// 2. Use Cases
@Bean
public RegisterUser registerUser(
UserRepository userRepository,
PasswordEncoder passwordEncoder,
// ... otras dependencias
) {
return new RegisterUser(userRepository, passwordEncoder, ...);
}
@Bean
public GetAllUsers getAllUsers(UserRepository userRepository) {
return new GetAllUsers(userRepository);
}
// 3. Servicios de infraestructura (si es necesario)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}// domain/useCases/user/UpdateUserProfile.java
package com.outfitlab.project.domain.useCases.user;
import com.outfitlab.project.domain.interfaces.repositories.UserRepository;
import com.outfitlab.project.domain.model.UserModel;
public class UpdateUserProfile { // ← POJO sin anotaciones
private final UserRepository userRepository;
// Constructor para inyección de dependencias
public UpdateUserProfile(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Método de ejecución
public UserModel execute(String email, String newName) {
UserModel user = userRepository.findUserByEmail(email);
user.setName(newName);
return userRepository.saveUser(user);
}
}// infrastructure/config/UserConfig.java
@Configuration
public class UserConfig {
// ... otros beans
@Bean
public UpdateUserProfile updateUserProfile(UserRepository userRepository) {
return new UpdateUserProfile(userRepository);
}
}// presentation/controllers/UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UpdateUserProfile updateUserProfile;
public UserController(UpdateUserProfile updateUserProfile) {
this.updateUserProfile = updateUserProfile;
}
@PutMapping("/{email}/profile")
public ResponseEntity<UserDTO> updateProfile(
@PathVariable String email,
@RequestBody UpdateProfileRequest request
) {
UserModel updated = updateUserProfile.execute(email, request.getName());
return ResponseEntity.ok(UserDTO.convertToDTO(updated));
}
}// domain/interfaces/repositories/ProductRepository.java
package com.outfitlab.project.domain.interfaces.repositories;
import com.outfitlab.project.domain.model.ProductModel;
import java.util.List;
public interface ProductRepository {
ProductModel findById(Long id);
List<ProductModel> findAll();
ProductModel save(ProductModel product);
void delete(Long id);
}// infrastructure/model/ProductEntity.java
package com.outfitlab.project.infrastructure.model;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
// Getters, setters, constructores
// Método de conversión
public static ProductModel convertToModel(ProductEntity entity) {
return new ProductModel(entity.getId(), entity.getName(), entity.getPrice());
}
}// infrastructure/repositories/interfaces/ProductJpaRepository.java
package com.outfitlab.project.infrastructure.repositories.interfaces;
import com.outfitlab.project.infrastructure.model.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductJpaRepository extends JpaRepository<ProductEntity, Long> {
// Métodos de consulta personalizados si es necesario
}// infrastructure/repositories/ProductRepositoryImpl.java
package com.outfitlab.project.infrastructure.repositories;
import com.outfitlab.project.domain.interfaces.repositories.ProductRepository;
import com.outfitlab.project.domain.model.ProductModel;
import com.outfitlab.project.infrastructure.model.ProductEntity;
import com.outfitlab.project.infrastructure.repositories.interfaces.ProductJpaRepository;
import java.util.List;
import java.util.stream.Collectors;
public class ProductRepositoryImpl implements ProductRepository { // ← Sin anotaciones
private final ProductJpaRepository jpaRepository;
public ProductRepositoryImpl(ProductJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public ProductModel findById(Long id) {
ProductEntity entity = jpaRepository.findById(id).orElse(null);
return entity != null ? ProductEntity.convertToModel(entity) : null;
}
@Override
public List<ProductModel> findAll() {
return jpaRepository.findAll().stream()
.map(ProductEntity::convertToModel)
.collect(Collectors.toList());
}
@Override
public ProductModel save(ProductModel product) {
ProductEntity entity = new ProductEntity(product.getName(), product.getPrice());
ProductEntity saved = jpaRepository.save(entity);
return ProductEntity.convertToModel(saved);
}
@Override
public void delete(Long id) {
jpaRepository.deleteById(id);
}
}// infrastructure/config/ProductConfig.java
@Configuration
public class ProductConfig {
@Bean
public ProductRepository productRepository(ProductJpaRepository jpaRepo) {
return new ProductRepositoryImpl(jpaRepo);
}
}// domain/useCases/user/GetAllUsers.java
public class GetAllUsers {
private final UserRepository userRepository;
public GetAllUsers(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<UserDTO> execute() {
return userRepository.findAll()
.stream()
.map(UserDTO::convertToDTO)
.toList();
}
}// domain/useCases/user/RegisterUser.java
public class RegisterUser {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final TokenRepository tokenRepository;
private final GmailGateway gmailGateway;
public RegisterUser(
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtService jwtService,
TokenRepository tokenRepository,
GmailGateway gmailGateway
) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
this.tokenRepository = tokenRepository;
this.gmailGateway = gmailGateway;
}
public UserModel execute(RegisterDTO request) {
// Lógica de negocio
String hashedPassword = passwordEncoder.encode(request.getPassword());
UserModel user = new UserModel(request.getEmail(), hashedPassword);
UserModel saved = userRepository.saveUser(user);
// Enviar email de verificación
gmailGateway.sendEmail(user.getEmail(), "Bienvenido", "...");
return saved;
}
}// infrastructure/repositories/UserRepositoryImpl.java
public class UserRepositoryImpl implements UserRepository {
private final UserJpaRepository jpaRepository;
@Override
public UserModel findUserByEmail(String email) {
UserEntity entity = jpaRepository.findByEmail(email);
return entity != null ? UserEntity.convertToModel(entity) : null;
}
@Override
public UserModel saveUser(UserModel model) {
UserEntity entity = UserEntity.convertFromModel(model);
UserEntity saved = jpaRepository.save(entity);
return UserEntity.convertToModel(saved);
}
}// ❌ INCORRECTO
package com.outfitlab.project.domain.useCases.user;
import org.springframework.stereotype.Service;
@Service // ❌ NO usar anotaciones en dominio
public class RegisterUser {
// ...
}// ❌ INCORRECTO
package com.outfitlab.project.domain.useCases.user;
import com.outfitlab.project.infrastructure.model.UserEntity; // ❌
import com.outfitlab.project.infrastructure.config.security.jwt.JwtService; // ❌
public class RegisterUser {
private UserEntity user; // ❌ Usar UserModel en su lugar
}// ❌ INCORRECTO
@RestController
public class UserController {
@Autowired
private UserJpaRepository userRepository; // ❌
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterDTO dto) {
// ❌ Lógica de negocio en el controller
if (userRepository.findByEmail(dto.getEmail()) != null) {
return ResponseEntity.badRequest().body("User exists");
}
UserEntity user = new UserEntity();
user.setEmail(dto.getEmail());
// ... más lógica
userRepository.save(user);
return ResponseEntity.ok(user);
}
}// ✅ CORRECTO
@RestController
public class UserController {
private final RegisterUser registerUser; // ✅ Use case
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterDTO dto) {
try {
UserModel user = registerUser.execute(dto); // ✅ Delegar al use case
return ResponseEntity.ok(UserDTO.convertToDTO(user));
} catch (UserAlreadyExistsException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}// ❌ INCORRECTO
public interface UserRepository {
UserEntity findByEmail(String email); // ❌ Retornar Entity
}// ✅ CORRECTO
public interface UserRepository {
UserModel findUserByEmail(String email); // ✅ Retornar Model
}Excepción: Está permitido retornar Entities en casos específicos donde simplifica el código y no viola la separación de responsabilidades (ej: addToFavorite() en repositorios de favoritos).
// ❌ SOBRE-INGENIERÍA - No crear interfaces para todo
public interface AuthenticationService {
void authenticate(String email, String password);
}
public class AuthenticationServiceImpl implements AuthenticationService {
private final AuthenticationManager authManager;
public void authenticate(String email, String password) {
authManager.authenticate(new UsernamePasswordAuthenticationToken(email, password));
}
}// ✅ PRAGMÁTICO - Inyectar directamente cuando tiene sentido
@Bean
public LoginUser loginUser(
UserRepository userRepository,
AuthenticationManager authManager // ✅ Directo, sin wrapper innecesario
) {
return new LoginUser(userRepository, authManager);
}Regla: Los use cases NO deben llamar a otros use cases. La orquestación de múltiples casos de uso debe hacerse en el controller.
// ❌ INCORRECTO - Use case orquestando otros use cases
public class RegisterUser {
private final UserRepository userRepository;
private final AssignFreePlanToUser assignFreePlanToUser; // ❌
public UserModel execute(RegisterDTO request) {
// Lógica de registro
UserModel user = userRepository.saveUser(newUser);
// ❌ Llamando a otro use case
assignFreePlanToUser.execute(user.getEmail());
return user;
}
}// ✅ CORRECTO - Controller orquestando use cases
@RestController
public class UserController {
private final RegisterUser registerUser;
private final AssignFreePlanToUser assignFreePlanToUser;
@PostMapping("/register")
@Transactional // ✅ Control explícito de transacción
public ResponseEntity<?> register(@RequestBody RegisterDTO dto) {
try {
// 1. Registrar usuario
UserModel user = registerUser.execute(dto);
// 2. Asignar plan gratuito
assignFreePlanToUser.execute(user.getEmail());
return ResponseEntity.ok(UserDTO.convertToDTO(user));
} catch (UserAlreadyExistsException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}Razones:
- ✅ Responsabilidad Única: Cada use case hace UNA cosa
- ✅ Reusabilidad:
AssignFreePlanToUserpuede usarse en otros contextos - ✅ Testabilidad: Más fácil testear use cases independientes
- ✅ Claridad: El controller muestra explícitamente el flujo completo
- ✅ Transaccionalidad: Mejor control sobre transacciones
Usa este checklist al revisar Pull Requests:
- ¿Los use cases son POJOs sin anotaciones?
- ¿No hay imports de
com.outfitlab.project.infrastructureen domain? - ¿No hay imports de
org.springframeworken domain? - ¿Los modelos de dominio son POJOs simples?
- ¿Las interfaces de repositorio retornan Models (no Entities)?
- ¿Los use cases NO llaman a otros use cases?
- ¿Los repositorios implementan las interfaces de dominio?
- ¿Las implementaciones convierten entre Entity y Model?
- ¿Los beans están registrados en las clases
*Config.java? - ¿Las entidades JPA están en
infrastructure.model?
- ¿Los controllers solo orquestan (no tienen lógica de negocio)?
- ¿Los controllers usan DTOs para entrada/salida?
- ¿Los controllers delegan a use cases?
# Verificar violaciones de arquitectura
./scripts/check-architecture.sh
# Ejecutar tests
./mvnw test
# Build con Docker
docker compose up -d --buildPara mantener el dominio independiente de Spring. Los use cases deben ser POJOs que puedan ejecutarse sin un contenedor de Spring.
Cuando crear una abstracción adicional no aporta valor y solo agrega complejidad. Ejemplos: PasswordEncoder, AuthenticationManager, JwtService.
En el dominio (domain.model.Role), no en infraestructura. Si es un concepto de negocio, va en domain.
No directamente. Los use cases deben trabajar con Models. Los repositorios se encargan de la conversión Entity ↔ Model.
Crea una interfaz (gateway) en domain y la implementación en infrastructure. Ejemplo: GmailGateway (interface) → GmailGatewayImpl (implementación).
No. La orquestación de múltiples use cases debe hacerse en el controller. Esto mantiene cada use case con una responsabilidad única, facilita el testing, y da mejor control sobre transacciones.
Última actualización: 2025-11-21
Mantenido por: Lucas - (FELI lo va a revisar.)