Prototipo de un workflow de aprobación de gastos implementado con Java 21, Maven, PostgreSQL mediante JDBC puro y una API REST mínima con Javalin. No utiliza Spring Boot, JPA ni Hibernate.
Cada proceso contiene un monto, una descripción y tareas de aprobación. Las tareas pueden depender de otras tareas y siguen estas transiciones:
BLOCKED -> PENDING -> IN_PROGRESS -> APPROVED
-> REJECTED
Una tarea sin dependencias comienza en PENDING. Una tarea dependiente comienza en BLOCKED y pasa automáticamente a PENDING cuando todas sus dependencias están aprobadas.
domain.model: agregadoApprovalProcess, tareas, identificadores, estados y excepciones del negocio.domain.service: detección de ciclos mediante DFS.domain.event: eventos y abstracción de publicación.application: coordinación de casos de uso enApprovalWorkflowService.infrastructure.repository: repositorios JDBC y en memoria.infrastructure.config: configuración externa de PostgreSQL y del puerto HTTP.infrastructure.web: servidor Javalin, controlador, DTO y manejo de errores.
El dominio no importa JDBC, PostgreSQL, Javalin ni clases HTTP. InMemoryProcessRepository continúa disponible para pruebas unitarias y demostraciones.
- JDK 21.
- Maven 3.9 o posterior.
- PostgreSQL accesible para ejecutar la aplicación.
- Docker en ejecución para las pruebas de integración con Testcontainers.
Docker no es necesario para las pruebas unitarias o HTTP. Este proyecto no incluye Docker Compose.
Crear una base de datos, por ejemplo:
createdb workflow_dbAplicar el esquema versionado:
psql -d workflow_db -f src/main/resources/db/schema.sqlEl script crea:
approval_processesworkflow_taskstask_dependencies- El enum PostgreSQL
task_status
El script está pensado para una base vacía y no es idempotente. Para volver a ejecutarlo debe recrearse el esquema o eliminar previamente sus objetos.
La aplicación lee exclusivamente estas variables de entorno:
WORKFLOW_DB_URL=jdbc:postgresql://localhost:5432/workflow_db
WORKFLOW_DB_USER=postgres
WORKFLOW_DB_PASSWORD=change_me
WORKFLOW_PORT=7070
WORKFLOW_PORT es opcional y usa 7070 por defecto. Las tres variables de base de datos son obligatorias. El archivo .env.example sirve como plantilla, pero la aplicación no carga archivos .env automáticamente.
Ejemplo para PowerShell:
$env:WORKFLOW_DB_URL = "jdbc:postgresql://localhost:5432/workflow_db"
$env:WORKFLOW_DB_USER = "postgres"
$env:WORKFLOW_DB_PASSWORD = "tu_password"
$env:WORKFLOW_PORT = "7070"No deben guardarse credenciales reales en .env.example ni en archivos versionados. .env y *.env están ignorados por Git.
Pruebas unitarias y HTTP, sin PostgreSQL:
mvn testPruebas de integración JDBC contra PostgreSQL real:
mvn -Pintegration testEl perfil levanta temporalmente PostgreSQL 17 con Testcontainers, aplica schema.sql y destruye el contenedor al finalizar. No usa las variables WORKFLOW_DB_*, pero requiere acceso al daemon de Docker. Las pruebas crean UUID aleatorios y eliminan los procesos que crean.
Empaquetar:
mvn clean packageCon PostgreSQL configurado y el esquema aplicado:
mvn exec:javaLa clase de entrada es org.example.workflow.infrastructure.web.WorkflowHttpServer. El servidor construye manualmente DatabaseConfig, JdbcProcessRepository, InMemoryEventPublisher, ApprovalWorkflowService y WorkflowController, sin contenedor de inyección de dependencias.
WorkflowDemo usa ApprovalWorkflowService con InMemoryProcessRepository, por lo que permite demostrar el flujo completo sin preparar infraestructura:
mvn compile exec:java -Dexec.mainClass=org.example.workflow.WorkflowDemoLa salida muestra una solicitud por S/ 1500, la revisión del jefe inicialmente en PENDING y la revisión financiera en BLOCKED. También muestra el rechazo esperado al intentar iniciar la tarea bloqueada, su desbloqueo automático después de aprobar la tarea del jefe y el resultado final con ambas tareas en APPROVED.
Primero inicie el servicio PostgreSQL de su instalación, cree la base de datos y aplique src/main/resources/db/schema.sql como se explica en la sección Base de datos. Después configure WORKFLOW_DB_URL, WORKFLOW_DB_USER y WORKFLOW_DB_PASSWORD; puede conservar el puerto predeterminado 7070. Desde la raíz del proyecto, inicie WorkflowHttpServer:
mvn exec:javaCon el servidor activo, abra otra terminal PowerShell y ejecute:
.\scripts\demo-api.ps1El script crea el proceso y las dos tareas, captura los UUID de cada respuesta y conecta automáticamente la dependencia. A continuación enseña el error HTTP 409 esperado para la tarea bloqueada, aprueba la revisión del jefe, consulta el desbloqueo a PENDING, completa la revisión financiera y muestra el JSON final. No es necesario copiar identificadores manualmente.
Si la API se ejecuta en otra dirección, use el parámetro opcional, por ejemplo:
.\scripts\demo-api.ps1 -BaseUrl "http://localhost:7070"curl -i -X POST http://localhost:7070/processes \
-H "Content-Type: application/json" \
-d '{"amount":1500.00,"description":"Compra de monitores"}'Responde 201 Created, incluye Location y devuelve el proceso.
curl -i -X POST http://localhost:7070/processes/PROCESS_ID/tasks \
-H "Content-Type: application/json" \
-d '{"name":"Revisión financiera","assignedUser":"carlos","dependencyIds":[]}'Para agregar una dependencia:
{
"name": "Aprobación final",
"assignedUser": "ana",
"dependencyIds": ["TASK_ID_EXISTENTE"]
}Las dependencias deben pertenecer al mismo proceso y haber sido creadas previamente.
curl -i http://localhost:7070/processes/PROCESS_IDLa respuesta contiene monto, descripción, tareas, estados, usuarios asignados y dependencias.
curl -i -X POST http://localhost:7070/processes/PROCESS_ID/tasks/TASK_ID/startcurl -i -X POST http://localhost:7070/processes/PROCESS_ID/tasks/TASK_ID/approvecurl -i -X POST http://localhost:7070/processes/PROCESS_ID/tasks/TASK_ID/rejectLas tres operaciones de estado responden 200 OK con la tarea actualizada.
Los errores usan un formato homogéneo:
{
"error": "INVALID_TRANSITION",
"message": "Invalid transition from PENDING to APPROVED"
}Mapeo principal:
400: JSON, UUID o entrada básica inválida.404: proceso o tarea inexistente.409: transición, dependencia o ciclo inválido.500: error inesperado de persistencia, sin exponer credenciales ni stack traces.
No hay regla de autorización por usuario en el dominio actual, por lo que esta versión no devuelve 403 por asignación.
- Los montos se representan con
BigDecimaly PostgreSQLNUMERIC(19, 2). - Las claves son UUID y las dependencias se guardan en una tabla relacional, no como listas serializadas.
JdbcProcessRepository.savereemplaza las tareas y dependencias completas del agregado dentro de una transacción. Es sencillo y consistente para este prototipo, a cambio de efectuar más escrituras.- La aprobación y el desbloqueo de dependientes se persisten con una sola llamada al repositorio; los eventos se publican después de guardar correctamente.
- Los eventos permanecen síncronos y en memoria. No se añadió una tabla de eventos porque no forma parte del estado necesario para reconstruir el agregado.
- Las tareas dependientes de una tarea rechazada permanecen en
BLOCKED. - La API utiliza DTO y nunca cambia estados de las entidades directamente.
- No hay autenticación ni autorización.
- No hay control de concurrencia optimista o pesimista; dos actualizaciones simultáneas podrían producir una actualización perdida.
- No hay pool de conexiones: cada operación abre una conexión JDBC.
- El reemplazo completo no está optimizado para agregados grandes.
- La publicación de eventos no participa en la transacción PostgreSQL.
- No hay migrador automático;
schema.sqlse aplica manualmente. - No hay paginación ni endpoint para listar todos los procesos.
- Añadir versionado optimista del agregado.
- Incorporar un pool de conexiones y una herramienta de migraciones.
- Persistir eventos mediante outbox si aparecen integraciones externas.
- Añadir observabilidad, paginación y pruebas de concurrencia.