Decisiones de arquitectura del backend Spring Boot de GrowTogether.
Proyecto: GrowTogether — DAM 2026 Autor: Jordi Patuel Pons
Este archivo cubre solo decisiones del backend. Las del paquete de datos compartido están en
GrowTogetherDATA/docs/DECISIONS.md, las de la app móvil enGrowTogetherAPP/docs/DECISIONS.mdy las del panel admin enGrowTogetherADMIN/docs/DECISIONS.md.
| # | Decisión | Estado |
|---|---|---|
| ADR-001 | Autenticación JWT stateless | Aceptado |
| ADR-002 | Revocación de tokens con tokenVersion | Aceptado |
| ADR-003 | Cifrado de contraseñas con BCrypt | Aceptado |
| ADR-004 | Borrado lógico (soft delete) | Aceptado |
| ADR-005 | DTOs separados de las entidades | Aceptado |
| ADR-006 | Manejo centralizado de errores | Aceptado |
| ADR-007 | Rate limiting propio en el filtro | Aceptado |
| ADR-008 | Relleno lazy de NO_COMPLETADO | Aceptado |
| ADR-009 | Autorización por propietario (@PreAuthorize + SpEL) |
Aceptado |
| ADR-010 | Roles @Enumerated(STRING) |
Aceptado |
| ADR-011 | CORS manual por origen | Aceptado |
| ADR-012 | Un consejo por fecha | Aceptado |
| ADR-013 | Auditoría sin relación JPA | Aceptado |
| ADR-014 | Swagger UI público | Aceptado (académico) |
Fecha: 2026-03-01
El sistema de autenticación usa tokens JWT firmados con HMAC-SHA256
(HS256). El servidor no guarda estado de sesión: cada petición incluye
el token en el header Authorization: Bearer <token>.
- Sesiones HTTP (
HttpSession): requieren estado en servidor. Obligan a sticky sessions o estado compartido si se escala. - OAuth2 / SSO externo: válido en producción empresarial pero añade dependencia de un proveedor y complejidad innecesaria para el TFG.
- API Keys simples: no permiten incluir información del usuario (rol, id) ni gestionan expiración de forma estándar.
- Stateless: el token se valida sin consulta a BD en cada petición
(excepto la verificación de
tokenVersion, ver ADR-002). - Portable: el cliente Flutter lo gestiona en
flutter_secure_storage. - Estándar adoptado, librería madura (
jjwt 0.11.5). - Claims personalizados (
tv,sub) extienden funcionalidad sin romper compatibilidad.
Implementación: JwtService.java, JwtAuthenticationFilter.java,
SecurityConfig.java.
Fecha: 2026-03-10
Cada usuario tiene un campo tokenVersion (int) en BD. Al generar el
JWT se incluye el valor como claim tv. El filtro de cada petición
compara el tv del token con el tokenVersion actual del usuario; si
no coinciden, el token se rechaza. Cambiar contraseña o desactivar la
cuenta incrementa tokenVersion, invalidando todos los tokens
anteriores.
- Blacklist en BD/Redis: añade consulta por petición y necesita limpieza periódica. Rompe el stateless de JWT.
- No revocar (dejar expirar): si un usuario cambia contraseña porque cree que está comprometida, el atacante mantiene acceso 24 h hasta la expiración.
- Access + refresh tokens: más complejo de implementar y de gestionar en el cliente. Válido en producción real, sobreingeniería aquí.
- Solo un entero en
usuarios, sin tabla extra. - Revocación inmediata.
- Mantiene el modelo stateless: la versión viaja en el propio token.
Implementación: Usuario.java (campo tokenVersion),
JwtService.java (claim tv), JwtAuthenticationFilter.java
(validación), UsuarioService.java (incremento).
Fecha: 2026-03-01
Las contraseñas se cifran con BCrypt antes de persistirse. Nunca se guardan en texto plano.
- SHA-256 / MD5: rápidas — vulnerables a fuerza bruta y rainbow tables.
- Argon2: más moderno, pero Spring Security incluye BCrypt out of the box. Argon2 exigía dependencia adicional sin ventaja práctica.
- Texto plano: descartado de plano.
- Incluido en Spring Security.
- Salt automático (anti rainbow tables).
- Factor de coste configurable.
- Estándar de facto.
PasswordEncoder vive en PasswordEncoderConfig.java separado de
SecurityConfig.java para evitar dependencias circulares: si el
encoder estuviera dentro de SecurityConfig, el ciclo
SecurityConfig → UsuarioService → PasswordEncoder → SecurityConfig
impide arrancar Spring. Patrón recomendado por Spring para este caso.
Implementación: PasswordEncoderConfig.java, UsuarioService.java.
Fecha: 2026-03-05
Las entidades Usuario, Habito y Desafio no se borran físicamente
de la BD. Tienen un campo activo (boolean, default true). Al
"borrar" se pone activo = false y los servicios filtran por
activo = true.
- Hard delete (
DELETESQL): pierde historial y rompe integridad referencial con registros relacionados (RegistroHabito, ParticipacionDesafio). Irreversible. - Tabla de archivo separada: más complejo y sin beneficio real aquí.
- Preserva el historial:
RegistroHabitode un hábito borrado siguen existiendo para estadísticas. - Permite reactivar registros (
PUT /admin/usuarios/{id}/desbloquear). - Audit log claro: se ve qué usuarios estuvieron activos.
Implementación: Usuario.java, Habito.java, Desafio.java
(campo activo); servicios filtran.
Fecha: 2026-03-01
Existen DTOs separados de las entidades JPA. Hay DTOs de entrada
(*CreateDTO) y de salida (*DTO).
- Exponer entidades directamente: filtra password y
tokenVersion, causa serialización infinita por relaciones JPA y acopla la API al modelo de BD. - Un solo DTO por entidad: los campos de entrada y salida no
coinciden (el password va en
Createpero nunca enResponse).
- Control total sobre qué se expone.
- Password nunca viaja en una respuesta.
- Validaciones de entrada (
@NotBlank,@Email,@Size) que no tienen sentido en la entidad. - Desacopla BD y API.
Implementación: paquete dto/ (~20 clases).
Fecha: 2026-03-01
Una clase @RestControllerAdvice intercepta todas las excepciones y
las transforma en ErrorResponseDTO JSON.
- try/catch en cada controller: duplicación masiva.
- Errores por defecto de Spring: incluyen stack traces y mensajes internos de JPA — riesgo de filtrar información sensible.
- Único punto de control de la información que sale al cliente.
- Errores no controlados (
RuntimeException) se loguean en servidor con stack trace y devuelven "Error interno del servidor" al cliente. - Respuestas de error consistentes.
- Servicios lanzan excepciones semánticas
(
ResourceNotFoundException,BadRequestException); el handler traduce a HTTP.
Implementación: GlobalExceptionHandler.java,
BadRequestException.java, ResourceNotFoundException.java.
Fecha: 2026-03-15
RateLimitFilter (Servlet Filter) limita a 10 peticiones/minuto por
IP en login y registro. Usa un ConcurrentHashMap en memoria.
- API Gateway externo (AWS, Kong, Nginx): robusto pero requiere infraestructura adicional fuera del scope del TFG.
- Spring Cloud Gateway / Bucket4j: dependencias especializadas que no aportan ventaja para dos endpoints.
- Sin rate limiting: los endpoints de auth son objetivo principal de fuerza bruta.
- Sin dependencias adicionales.
- Suficiente para el scope.
ConcurrentHashMapthread-safe.- Ventana deslizante simple (60 s).
El conteo se pierde al reiniciar y no es compartido entre instancias: con dos servidores cada uno lleva su contador, un atacante podría hacer 10×N peticiones. Producción real → Redis o API Gateway.
Implementación: RateLimitFilter.java.
Fecha: 2026-03-15
Los días que un usuario no interactúa con un hábito no generan
registro. Cuando se consulta el historial, el servicio detecta los
huecos y los rellena con NO_COMPLETADO en ese momento. Además, una
tarea programada (@Scheduled cada día a las 00:01) hace ese relleno
de forma automática para que las estadísticas del día anterior estén
listas.
- Generar todos los registros al crear el hábito: miles de filas vacías para fechas futuras, sin sentido.
- Trigger en BD: mueve lógica de negocio fuera de la app, difícil de testear.
- Inferirlos en el cliente: lógica de servidor en el cliente.
- BD solo guarda registros con información real.
- Historial siempre consistente al consultarse.
- Tarea nocturna garantiza estadísticas correctas al día siguiente.
- La lógica de qué días debe tener un hábito PERSONALIZADO depende del
modelo Java (
enum DiaSemana,Set<DiaSemana> diasSemana). Replicarla en SQL sería complejo y frágil. @Scheduledvive con el resto del dominio, fácil de depurar.- Spring activa el scheduler con una sola anotación
(
@EnableScheduling).
Implementación: RegistroHabitoService.java,
HabitoScheduledService.java.
Fecha: 2026-03-10
Endpoints que modifican recursos de un usuario (hábitos,
notificaciones, desafíos) usan @PreAuthorize con expresiones SpEL
para verificar que el autenticado es propietario.
- Verificación manual en cada servicio: duplicación.
- Sin verificación: cualquier usuario puede tocar recursos de otro conociendo el id.
- La verificación queda declarada en el controller, visible.
- Spring Security la evalúa antes de entrar al método.
- Se integra con el
SecurityContextya cargado.
Implementación: HabitoController, DesafioController,
NotificacionController.
Fecha: 2026-03-01
Usuario.rol usa @Enumerated(EnumType.STRING), guardando el valor
como texto ("STANDARD", "ADMIN").
EnumType.ORDINAL: guarda el índice numérico (0, 1, 2…). Si se reordena o se añade un valor, los datos históricos se corrompen silenciosamente.
- Datos legibles en BD sin conocer el código.
- Añadir nuevos roles no corrompe nada.
- Coste de espacio mínimo.
Implementación: Usuario.java, Roles.java.
Fecha: 2026-03-15
CORS permite los orígenes:
localhost:*— desarrollo en máquina local.10.0.2.2:*— emulador Android (IP del host).192.168.*.*:*— móvil físico en la misma red local.
Configurado en SecurityConfig para que Spring lo aplique antes que el
filtro JWT.
Restringir a los dominios reales (growtogether.jordipatuel.com y
similares).
Implementación: SecurityConfig.corsConfigurationSource().
Fecha: 2026-04-01 (cerrado 2026-05-13) Estado: Aceptado e implementado en BD
Cada Consejo tiene una fechaPublicacion (día en que se mostrará).
No puede haber dos consejos programados para el mismo día.
- Validación en servicio:
ConsejoServicecomprueba antes de guardar. - Garantía a nivel de BD: la columna
fecha_publicacionestá declarada comoUNIQUEenConsejo.java(@Column(name = "fecha_publicacion", unique = true)). Hibernate genera la restricción en la tablaconsejos.
Doble red: el servicio devuelve un error semántico al cliente y la BD rechaza el insert como último recurso.
Implementación: Consejo.java, ConsejoService.java.
Fecha: 2026-04-05
AuditLog guarda usuarioId (Long) y usuarioEmail (String) como
campos simples, sin relación @ManyToOne con Usuario.
- Relación JPA con
Usuario: si el usuario se desactiva o cambia, el log podría reflejar el estado actual del usuario en lugar del estado del momento del evento.
- El audit log es un registro inmutable e independiente del ciclo de vida del usuario.
- Si el email del usuario cambia, el log mantiene el email del momento.
- Las consultas de audit no necesitan joins.
Implementación: AuditLog.java, AuditService.java.
Fecha: 2026-03-01 (revisado 2026-05-13) Estado: Aceptado (justificado por contexto académico)
/swagger-ui/** y /v3/api-docs/** son públicos en SecurityConfig,
sin token, tanto en dev como en producción.
En application-prod.properties:
springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true- Swagger con auth básica en prod: dos mecanismos de auth conviviendo, complejidad sin beneficio claro para el contexto académico.
- Desactivar SpringDoc en producción: válido en prod real, pero el proyecto académico requiere que el evaluador pueda explorar la API desplegada sin acceso al repo ni a un cliente.
- Servir Javadoc como única documentación: el workflow
docs.ymlpublica Javadoc en Pages, pero Javadoc documenta el código interno, no los endpoints REST. No cubre la necesidad del evaluador.
- Proyecto académico (TFG DAM 2026): el tribunal debe poder explorar y probar la API desplegada en AWS sin implementar un cliente.
- Swagger muestra todos los endpoints, parámetros y respuestas visualmente, incluyendo el botón "Try it out".
- Javadoc y Swagger son complementarios: Javadoc cubre el código interno (Pages), Swagger cubre el contrato REST (mismo servidor).
- Mapa completo de la API expuesto: facilita el reconocimiento a un
atacante. Mitigación: todos los endpoints sensibles requieren JWT y
/auth/*está limitado porRateLimitFilter(10 req/min/IP). - Filtración de payloads de auth: el formato de
/auth/loginy/auth/registeres público. Mitigación: rate limiting + BCrypt +tokenVersion(ADR-002, ADR-003, ADR-007). - Posible filtración de versión de Spring Boot/dependencias: la spec OpenAPI puede dar pistas sobre CVEs aplicables. Asumido para esta entrega.
- Proteger
/swagger-uiy/v3/api-docscon auth básica, o - Servir la spec OpenAPI exportada como HTML estático fuera del backend.
Implementación: SecurityConfig.java,
application-prod.properties.
Última actualización: mayo 2026 — Jordi Patuel Pons