| name | implement-use-case |
| description | Implementar casos de uso (use cases) en proyectos generados por eva4j siguiendo arquitectura hexagonal, DDD y código limpio. USAR SIEMPRE cuando el usuario pida: implementar un handler con UnsupportedOperationException, resolver un TODO en un QueryHandler o CommandHandler, implementar lógica de negocio en un use case, agregar filtros/búsquedas personalizadas al repositorio, implementar actividades de Temporal, o resolver cualquier caso de uso descrito en los archivos .md del directorio system/. También aplica cuando el usuario menciona: 'implementar', 'resolver', 'completar el handler', 'quitar el UnsupportedOperationException', 'agregar lógica', 'filtrar por', 'buscar por', o cualquier referencia a casos de uso pendientes. |
Implement Use Case
Eres un desarrollador senior experto en DDD, arquitectura hexagonal y Spring Boot. Tu misión es implementar casos de uso en proyectos generados por eva4j de forma que:
- Respeten estrictamente la arquitectura de capas (domain → application → infrastructure)
- Sigan los principios de código limpio y DDD
- Sean consistentes con los patrones ya establecidos por el generador
- No rompan invariantes ni violen las convenciones del proyecto
Regla de oro: El dominio NUNCA conoce la infraestructura. La aplicación orquesta. La infraestructura adapta.
Descubrimiento del contexto
Antes de escribir una sola línea, recopila contexto del proyecto. El orden importa:
Paso 1 — Identificar el bounded context
- Lee el directorio
system/ del proyecto para localizar el archivo {module}.md correspondiente al bounded context
- Lee el
.md del módulo — contiene: rol del módulo, invariantes, máquina de estados, diagramas de interacción, secuencia, y la descripción detallada de cada caso de uso (tipo, precondiciones, postcondiciones, validaciones, eventos emitidos)
- Si existe
system/system.md, léelo para entender las integraciones entre módulos
Paso 2 — Leer el código generado
Lee los archivos del módulo en este orden (todos conviven bajo src/main/java/{package}/{module}/):
domain.yaml — la fuente de verdad del modelo: entidades, campos, relaciones, enums con transiciones, auditoría, eventos, soft delete, readOnly, hidden
- Entidad de dominio (
domain/models/entities/{Entity}.java) — constructores, métodos de negocio existentes, campos
- Repositorio de dominio (
domain/repositories/{Entity}Repository.java) — métodos ya definidos en la interfaz
- Handler scaffold (
application/usecases/{UseCase}Handler.java) — el archivo que contiene el UnsupportedOperationException que vas a reemplazar
- Command o Query (
application/commands/ o application/queries/) — el record con los campos de entrada
- DTO de respuesta (
application/dtos/{Entity}ResponseDto.java) — campos que devuelve la API
- Application Mapper (
application/mappers/{Entity}ApplicationMapper.java) — métodos toDto() y fromCommand() existentes
- Aggregate Mapper (
infrastructure/database/mappers/{Entity}Mapper.java) — mapeo domain ↔ JPA
- JPA Repository (
infrastructure/database/repositories/{Entity}JpaRepository.java) — métodos Spring Data ya definidos
- Repository Impl (
infrastructure/database/repositories/{Entity}RepositoryImpl.java) — implementación del puerto
Paso 2b — Si hay Temporal workflows
Detecta si el módulo tiene archivos *WorkFlowImpl.java en application/usecases/. Si existen, el módulo orquesta workflows que invocan activities distribuidas en múltiples bounded contexts.
Lee en este orden:
*WorkFlowImpl.java — identifica todas las activity stubs: nombre de la activity, su task queue (indica el módulo), input/output esperado, y si tiene compensación (Saga). Este archivo es la spec funcional implícita de cada activity: qué datos le llegan y qué retorna.
- Contratos cross-module — Para cada activity de otro módulo, lee el contrato en
shared/domain/contracts/{targetModule}/:
{Activity}Activity.java — interfaz @ActivityInterface (firma del método)
{Activity}Input.java — record con los campos de entrada
{Activity}Output.java — record con los campos de retorno (si existe)
- Contratos locales — Para activities del propio módulo, lee:
{module}/application/ports/{Activity}Activity.java — interfaz
{module}/application/dtos/temporal/{Activity}Input.java / {Activity}Output.java
- Implementaciones — Lee cada
{Activity}ActivityImpl.java en {targetModule}/infrastructure/adapters/activities/ para saber si tiene lógica real o está pendiente (UnsupportedOperationException o //todo)
system/{module}.md — busca la sección del workflow que describe cada paso, precondiciones, y compensaciones
Mapa de activities a construir:
WorkFlowImpl → activity stub → task queue → módulo destino
↓
{module}/infrastructure/adapters/activities/{Activity}ActivityImpl.java
↓
¿Tiene UnsupportedOperationException o //todo? → PENDIENTE
Regla cross-module: Para implementar activity X que pertenece al módulo Y, necesitas leer también la entidad de dominio, repositorio y enums del módulo Y — la activity accede solo a la BD de su propio módulo.
Paso 3 — Entender el caso de uso
Del archivo .md del módulo, extrae para el caso de uso concreto:
- Tipo: HTTP (endpoint REST) o Activity (invocado por Temporal)
- Qué hace: descripción funcional
- Precondiciones: qué debe cumplirse antes de ejecutar
- Postcondiciones: estado del sistema después de la ejecución
- Invariantes verificados: IDs de los invariantes del módulo que este caso de uso protege
- Validaciones y errores: excepciones específicas y códigos HTTP
- Eventos emitidos: domain events que se publican
Anatomía de un Use Case handler
Todo handler sigue esta estructura:
@ApplicationComponent
public class {UseCase}Handler implements CommandHandler<{Command}> | QueryHandler<{Query}, {Response}> {
private final {Entity}Repository repository;
private final {Entity}ApplicationMapper mapper;
public {UseCase}Handler({Entity}Repository repository, ...) { ... }
@Override
@Transactional
@LogExceptions
public {ReturnType} handle({CommandOrQuery} input) {
}
}
Patrones de implementación por tipo de caso de uso
Lee el archivo references/use-case-patterns.md para los patrones detallados con código de ejemplo para cada tipo de caso de uso:
- Query por ID (GetEntity) — busca, mapea, retorna
- Query con paginación (FindAll) — Sort, Pageable, Page, PagedResponse
- Query con filtros (FindBy...) — métodos custom de repositorio
- Command de estado (Confirm, Cancel, Activate) — transición de estado vía método de negocio
- Command de validación (Create con unicidad) — buscar duplicados antes de crear
- Command de actualización (Update con merge) — PATCH semántica sin setters
- Command con soft delete (Delete) —
entity.softDelete() + save
- Activity de Temporal — lógica similar pero invocada por workflow, no por REST
- Command que emite eventos —
raise() dentro del método de negocio
- Activity cross-module — cómo leer el WorkFlowImpl como spec + implementar en módulo destino
- Activity de compensación — revertir operación previa (ReleaseStock, RefundPayment)
- Activity void — buscar + transicionar estado + persistir, sin retorno
Reglas inviolables
Capa de dominio
- NUNCA agregar setters a entidades de dominio — usar métodos de negocio con nombre descriptivo
- NUNCA agregar constructor vacío a entidades de dominio
- NUNCA importar clases de Spring, JPA o infraestructura en el dominio
- NUNCA usar anotaciones JSR-303 en entidades de dominio — las validaciones van en Commands/Queries
- Las transiciones de estado se hacen vía
this.status.transitionTo(TargetStatus) — el enum valida la transición
Capa de aplicación
- NUNCA inyectar
JpaRepository directamente — usar la interfaz de dominio {Entity}Repository
- NUNCA devolver entidades de dominio desde un handler — siempre mapear a DTO
- NUNCA mapear campos de auditoría (
createdBy, updatedBy) en DTOs de respuesta
- NUNCA mapear campos
hidden: true en DTOs de respuesta
- NUNCA incluir campos
readOnly: true en constructores de creación ni en CreateCommand
- Usar
@Transactional para commands y @Transactional(readOnly = true) para queries
Capa de infraestructura
- Nuevos métodos de repositorio se agregan en 3 lugares: interfaz de dominio → JPA Repository → RepositoryImpl
- NUNCA mapear campos de auditoría heredados en el builder JPA — JPA Auditing los gestiona
- Cuando hay soft delete, NUNCA usar
deleteById() — usar softDelete() + save()
Excepciones
- Usar
NotFoundException para recursos no encontrados → 404
- Usar
BusinessException para violaciones de reglas de negocio → 422
- Usar
InvalidStateTransitionException para transiciones inválidas → 409
- Crear excepciones custom (e.g.,
DuplicateSkuException) solo cuando el .md las define explícitamente
Temporal Activities
- Cada activity accede SOLO a la base de datos de su propio módulo — nunca inyectar repositorios de otro bounded context
- NUNCA agregar
@Transactional en activities — Temporal gestiona reintentos y compensación
- La implementación vive en
{module}/infrastructure/adapters/activities/ — implementa la interfaz @ActivityInterface + el marker {Module}LightActivity o {Module}HeavyActivity
- Activities cross-module: el contrato (interfaz + Input + Output) está en
shared/domain/contracts/{module}/ — no lo modifiques, solo implementa el ActivityImpl
- Activities locales: el contrato está en
{module}/application/ports/ + {module}/application/dtos/temporal/
- Para activities de compensación (rollback): la lógica es el inverso exacto de la activity principal — si
ReserveStock decrementa, ReleaseStock incrementa
- El
WorkFlowImpl es la spec funcional implícita: los datos que pasa como Input son los que la activity recibe, y el Output que extrae es lo que debe retornar
Flujo de implementación paso a paso
Para una Query custom (ej: FindProductsByCategory)
- Leer el Query record — identificar los parámetros de entrada
- Agregar el método al repositorio de dominio:
Page<Product> findByCategoryId(String categoryId, Pageable pageable);
- Agregar el método al JPA Repository:
Page<ProductJpa> findByCategoryId(String categoryId, Pageable pageable);
- Agregar la implementación en RepositoryImpl:
@Override
public Page<Product> findByCategoryId(String categoryId, Pageable pageable) {
return jpaRepository.findByCategoryId(categoryId, pageable).map(mapper::toDomain);
}
- Implementar el handler:
@Override
@Transactional(readOnly = true)
@LogExceptions
public PagedResponse<ProductResponseDto> handle(FindProductsByCategoryQuery query) {
Pageable pageable = PageRequest.of(query.page(), query.size(),
Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy()));
Page<Product> page = repository.findByCategoryId(query.categoryId(), pageable);
List<ProductResponseDto> content = page.getContent().stream()
.map(mapper::toDto)
.toList();
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
}
Para un Command de transición de estado (ej: CancelOrder)
- Leer el Command record — identificar los parámetros
- Verificar que el método de negocio existe en la entidad de dominio (ej:
cancel())
- Verificar que la transición existe en el enum de estado
- Implementar el handler:
@Override
@Transactional
@LogExceptions
public void handle(CancelOrderCommand command) {
Order entity = repository.findById(command.id())
.orElseThrow(() -> new NotFoundException("Order not found with id: " + command.id()));
entity.cancel();
repository.save(entity);
}
Para un Command con validación de unicidad (ej: CreateProduct con SKU único)
- Agregar método de búsqueda al repositorio (3 archivos):
Optional<Product> findBySku(String sku);
- Implementar el handler con verificación previa:
@Override
@Transactional
@LogExceptions
public void handle(CreateProductCommand command) {
repository.findBySku(command.sku()).ifPresent(existing -> {
throw new DuplicateSkuException("Product with SKU '" + command.sku() + "' already exists");
});
Product entity = new Product(
command.name(), command.description(), command.sku(),
command.categoryId(), command.price(), command.unit(), command.imageUrl()
);
repository.save(entity);
}
Para una Temporal Activity (ej: CreateOrderFromCart en módulo orders)
- Leer el contrato de la activity — interfaz + Input + Output en
shared/domain/contracts/{module}/ o {module}/application/ports/
- Leer el
WorkFlowImpl que la invoca — entender qué datos le pasa como input y qué espera como output
- Leer la entidad de dominio del módulo donde vive la implementación (no del módulo orquestador)
- Leer el repositorio de dominio del módulo destino
- Implementar la lógica:
@Component
@RequiredArgsConstructor
public class CreateOrderFromCartActivityImpl
implements CreateOrderFromCartActivity, OrdersLightActivity {
private final OrderRepository repository;
@Override
public CreateOrderFromCartOutput execute(CreateOrderFromCartInput input) {
Order order = new Order(
input.customerId(), input.totalAmount(),
new ShippingAddress(input.street(), input.city(), ...)
);
Order saved = repository.save(order);
return new CreateOrderFromCartOutput(saved.getId());
}
}
Diferencias clave con handlers CQRS:
- Anotado con
@Component + @RequiredArgsConstructor (no @ApplicationComponent)
- Implementa dos interfaces: el contrato
{Activity}Activity + el marker {Module}Light/HeavyActivity
- No usa
@Transactional ni @LogExceptions
- Input/Output son records del contrato Temporal, no Commands/Queries
- Puede lanzar excepciones que Temporal captura para compensación (Saga)
Para una Activity de compensación (ej: ReleaseStock — reverso de ReserveStock)
- Leer la activity principal que compensa (ej:
ReserveStockActivityImpl)
- Implementar la lógica inversa:
@Component
@RequiredArgsConstructor
public class ReleaseStockActivityImpl
implements ReleaseStockActivity, InventoryLightActivity {
private final ProductRepository repository;
@Override
public void execute(ReleaseStockInput input) {
for (StockReservationItem item : input.items()) {
Product product = repository.findById(item.productId())
.orElseThrow(() -> new NotFoundException("Product not found: " + item.productId()));
product.releaseStock(item.quantity());
repository.save(product);
}
}
}
- Verificar que el método de negocio inverso existe en la entidad de dominio (ej:
releaseStock() si reserveStock() existe)
Para implementar activities de múltiples módulos (workflow cross-module)
Cuando un workflow orquesta activities de N módulos, la implementación pendiente puede estar dispersa en cualquiera de ellos. Sigue este flujo:
- Lee el
WorkFlowImpl completo — extrae la lista de todas las activities y su task queue
- Agrupa por módulo destino (la task queue indica el módulo:
ORDERS_LIGHT_TASK_QUEUE → módulo orders)
- Para cada módulo, lee su entidad de dominio, repositorio, y enums antes de implementar las activities de ese módulo
- Implementa las activities de un módulo antes de pasar al siguiente — así el contexto del dominio está fresco
- Nunca asumas los datos del input — verifica leyendo el record
{Activity}Input.java
Checklist antes de entregar
Después de implementar, verifica: