Modelos de dominio de MULTAGAL
Objetivo: traducir el modelo conceptual a tipos Java. Vas a crear en domain/model los tipos que representan el dominio de MULTAGAL — todavía sin persistencia: POJOs o record, sin anotaciones de JPA ni de Jackson — y a justificar tus decisiones en un ADR.
0Mapa de la actividad
Sigue este orden: cada paso usa tipos del anterior, así nunca escribes una referencia a algo que aún no existe.
- Entiende el dominioGlosario, entidades, relaciones y estados del expediente (sección 1). Sin esto no sabrás qué campos poner.
- EnumeradosNo dependen de nada: son lo primero que compila.
- Objetos valor
Coordenadas,DireccionPostal,Matricula: piezas pequeñas que usarán las entidades. - Las doce entidadesEn tres bloques: catálogo → personas y vehículos → procedimiento.
- ComprobacionesNulos, imports prohibidos y compilación.
- ADR
docs/adr/0001-modelado-del-dominio.mdcon las cinco decisiones, sus alternativas descartadas y el porqué.
Entregable 1 · código
Los tipos del dominio en domain/model.
Entregable 2 · documento
docs/adr/0001-modelado-del-dominio.md con las cinco decisiones, alternativas descartadas y porqué.
config, domain.model, domain.exception, repository, service, controller, dto).Coordenadas, Gravedad e Infraccion. Intenta hacerlo tú antes de desplegarlas, y recuerda que la actividad te pide tomar y justificar decisiones: puedes discrepar de la propuesta si lo argumentas en el ADR.1Entiende el dominio antes de teclear
MULTAGAL es el subsistema de tramitación de expedientes sancionadores de una administración de tráfico. Cubre el ciclo de vida completo de una denuncia: captación, tramitación, notificación, alegaciones, resolución, pago y, en su caso, detracción de puntos.
Glosario mínimo
| Término | Significado |
|---|---|
| Denuncia | Acto por el que un agente o un dispositivo hace constar un hecho presuntamente infractor. |
| Expediente | Procedimiento administrativo abierto a raíz de una denuncia. Tiene número, estado y plazos. |
| Notificación | Comunicación formal al interesado. Puede requerir varios intentos. |
| Alegación | Escrito con el que el interesado se opone a la denuncia o aporta pruebas. |
| Resolución | Acto que pone fin al expediente: sanción firme, sobreseimiento o estimación de alegaciones. |
| Firmeza | La resolución ya no admite recurso ordinario. Sólo entonces se ejecuta. |
| Detracción de puntos | Descuento del saldo del permiso; sólo ocurre con resolución firme. |
| Prescripción | Extinción de la responsabilidad por transcurso del plazo sin actuación administrativa. |
| Sobreseimiento | Archivo del expediente sin sanción. |
| Pronto pago | Pago anticipado con bonificación, a cambio de renunciar a alegar. |
Las doce entidades y sus datos
La columna «Datos» es tu lista de campos. Léela como un enunciado: cada coma es, casi siempre, un atributo.
| Entidad | Datos (según la fuente) |
|---|---|
Conductor | Titular de permiso de conducción. NIF/NIE, nombre, domicilio, fecha de expedición, saldo de puntos. |
PermisoConduccion | Permiso asociado a un conductor, con sus clases (A, B, C…), fechas de validez y estado. |
Vehiculo | Matrícula, marca, modelo, tipo, fecha de matriculación, estado de ITV. |
Titularidad | Relación temporal N:M entre Conductor y Vehiculo (fecha de alta y de baja). |
Infraccion | Catálogo normativo: código, artículo del Reglamento, descripción, importe base, puntos a detraer, gravedad (leve / grave / muy grave). |
Denuncia | Hecho denunciado: fecha y hora, lugar (con coordenadas), agente o dispositivo denunciante, infracción imputada, vehículo, conductor identificado (si lo hay). |
Expediente | Tramitación administrativa de una denuncia: número de expediente, estado, importe, fechas del procedimiento. |
Notificacion | Intentos de notificación al interesado, con acuse y resultado. |
Alegacion | Escrito presentado por el interesado, con documentos adjuntos. |
Resolucion | Acto administrativo que pone fin al expediente. |
Pago | Abonos totales o parciales, con posible bonificación por pronto pago. |
Radar | Dispositivo de captación automática: identificador, ubicación, tipo, límite de velocidad controlado. |
Relaciones
Del diagrama entidad-relación de la fuente. Te dice qué tipo apunta a qué otro (qué campo de tipo entidad lleva cada record/clase).
| De | Relación | A | Cardinalidad |
|---|---|---|---|
| Conductor | posee | PermisoConduccion | 1 → 0..N |
| Conductor | es titular en | Titularidad | 1 → 0..N |
| Vehiculo | figura en | Titularidad | 1 → 0..N |
| Radar | capta | Denuncia | 1 → 0..N |
| Infraccion | tipifica | Denuncia | 1 → 0..N |
| Vehiculo | implicado en | Denuncia | 1 → 0..N |
| Conductor | identificado en | Denuncia | 1 → 0..N |
| Denuncia | origina | Expediente | 1 → 1 |
| Expediente | genera | Notificacion | 1 → 0..N |
| Expediente | recibe | Alegacion | 1 → 0..N |
| Expediente | concluye | Resolucion | 1 → 0..1 |
| Expediente | liquida | Pago | 1 → 0..N |
Titularidad es una N:M con atributos (fecha de alta y de baja): necesita un tipo propio, no basta con una lista dentro de Conductor. Además es temporal: un vehículo puede haber tenido varios titulares, y «¿quién era el titular el día de la denuncia?» no tiene respuesta trivial.Conductor puede ser nulo en Denuncia. Un radar capta una matrícula, no una persona: la identificación del conductor es un paso posterior del procedimiento.El expediente es una máquina de estados
Casi todas las reglas de negocio son restricciones sobre las transiciones válidas. De aquí salen los valores de EstadoExpediente.
| Desde | Hacia | Cuándo |
|---|---|---|
| (inicio) | INICIADO | — |
| INICIADO | NOTIFICADO | notificación con acuse |
| INICIADO | PRESCRITO | vence el plazo sin notificar |
| NOTIFICADO | PAGADO_BONIFICADO | pago en 20 días naturales |
| NOTIFICADO | ALEGADO | el interesado presenta alegación |
| NOTIFICADO | RESUELTO | vence el plazo de alegaciones |
| ALEGADO | RESUELTO | se contestan las alegaciones |
| RESUELTO | FIRME | no cabe recurso ordinario |
| RESUELTO | SOBRESEIDO | se estiman las alegaciones |
| FIRME | PAGADO | — |
Estados finales: PAGADO, PAGADO_BONIFICADO, SOBRESEIDO, PRESCRITO.
Reglas de negocio que afectan al modelo
No las implementas ahora (eso llega desde la A0.4), pero tu modelo tiene que poder expresarlas. Si una regla necesita un dato, ese dato tiene que existir como campo.
| Regla | Enunciado | Qué exige del modelo |
|---|---|---|
| R1 | Bonificación del 50 % si el pago se produce dentro de los 20 días naturales siguientes a la notificación, con renuncia a alegaciones. | Conocer la fecha de notificación y la del pago. |
| R2 | La detracción de puntos sólo se materializa cuando la resolución es firme. | Poder saber si una Resolucion es firme. |
| R3 | Saldo cero ⇒ permiso suspendido, atómicamente. | Saldo de puntos en Conductor; estado en PermisoConduccion. |
| R4 | Prescripción a los 3 meses (leves) o 6 meses (graves y muy graves) desde la fecha de comisión si no se ha notificado. | Fecha de comisión y gravedad de la infracción. |
| R5 | Un expediente no puede resolverse mientras existan alegaciones pendientes de contestar. | Saber si cada Alegacion está contestada. |
| R6 | La identificación del conductor es obligatoria para el titular del vehículo; su omisión genera una infracción autónoma muy grave. | Un expediente puede generar otro: cuidado con las referencias circulares. |
2Prepara los ficheros
Paquete obligatorio del proyecto: es.edu.multagal.domain.model (arquitectura en capas, apartado f). Diecinueve ficheros como mínimo.
src/main/java/es/edu/multagal/domain/model/
├── Gravedad.java ← enumerados (4)
├── EstadoExpediente.java
├── EstadoPermiso.java
├── ResultadoNotificacion.java
├── Coordenadas.java ← objetos valor (3)
├── DireccionPostal.java
├── Matricula.java
├── Infraccion.java ← entidades (12)
├── Radar.java
├── Conductor.java
├── PermisoConduccion.java
├── Vehiculo.java
├── Titularidad.java
├── Denuncia.java
├── Expediente.java
├── Notificacion.java
├── Alegacion.java
├── Resolucion.java
└── Pago.java
3Enumerados: adiós a los String mágicos
Criterio 3: los enumerados sustituyen a los String mágicos. Si un campo sólo admite un conjunto cerrado de valores, es un enum.
Gravedad — viene dada en la fuente, cópiala tal cual:
package es.edu.multagal.domain.model;
public enum Gravedad { LEVE, GRAVE, MUY_GRAVE }
EstadoExpediente — sale directamente de la máquina de estados de la sección 1. Escríbelo tú: son nueve valores.
Ver una propuesta de EstadoExpediente
package es.edu.multagal.domain.model;
public enum EstadoExpediente {
INICIADO,
NOTIFICADO,
ALEGADO,
RESUELTO,
FIRME,
SOBRESEIDO,
PAGADO,
PAGADO_BONIFICADO,
PRESCRITO
}
EstadoPermiso y ResultadoNotificacion — aquí la fuente no enumera los valores: los decides tú. Pistas de la fuente:
- R3 habla de un permiso suspendido y de un conductor a 0 puntos con «el permiso activo».
- La notificación «puede requerir varios intentos», cada uno «con acuse y resultado»: el resultado tiene que distinguir un intento que llega de uno que no.
Ver una propuesta (valores construidos, no de la fuente)
// EstadoPermiso.java — ACTIVO y SUSPENDIDO salen de R3; añade otros si los justificas
public enum EstadoPermiso { ACTIVO, SUSPENDIDO }
// ResultadoNotificacion.java — propuesta: la fuente sólo dice «con acuse y resultado»
public enum ResultadoNotificacion { ENTREGADA, RECHAZADA, AUSENTE, DESCONOCIDO }
Idea opcional: que Gravedad sepa su plazo de prescripción (R4)
El dominio contiene las «reglas invariantes del concepto, tipos, enums» (apartado f). El plazo de R4 depende sólo de la gravedad, así que puede vivir en el propio enum sin romper la regla de dependencia:
public enum Gravedad {
LEVE(3), GRAVE(6), MUY_GRAVE(6);
private final int mesesPrescripcion;
Gravedad(int mesesPrescripcion) { this.mesesPrescripcion = mesesPrescripcion; }
public int mesesPrescripcion() { return mesesPrescripcion; }
}
4Objetos valor
Un objeto valor no tiene identidad propia: dos Coordenadas con la misma latitud y longitud son la misma. Por eso encajan de forma natural con record (inmutable, con equals/hashCode por valor).
Coordenadas — dada en la fuente:
public record Coordenadas(double latitud, double longitud) { }
Matricula — la fuente te pregunta: ¿debe ser un tipo propio o basta con un String? Piensa qué gana un tipo propio: no puedes pasar un NIF donde se espera una matrícula, y el lugar natural para validar/normalizar el formato es su constructor. Esa es la decisión 4 del ADR.
Ver una propuesta de Matricula y DireccionPostal
// Matricula.java — el formato exacto no lo fija la fuente; aquí sólo se normaliza
public record Matricula(String valor) {
public Matricula { // constructor compacto
if (valor == null || valor.isBlank()) {
throw new IllegalArgumentException("La matrícula no puede estar vacía");
}
valor = valor.replace(" ", "").replace("-", "").toUpperCase();
}
}
// DireccionPostal.java — campos propuestos (la fuente sólo dice «domicilio»)
public record DireccionPostal(
String via,
String numero,
String codigoPostal,
String municipio,
String provincia) { }
Fíjate en que IllegalArgumentException es de java.lang: validar en el dominio no exige importar nada de fuera.
5Las doce entidades, en tres bloques
Para cada campo aplica siempre las mismas tres preguntas: ¿qué tipo Java? · ¿puede ser null? · ¿cambiará con el tiempo?
| Si el dato es… | usa… | nunca… |
|---|---|---|
| dinero (importe, pago) | BigDecimal | double / float |
| un día (plazo, validez, alta/baja) | LocalDate | java.util.Date |
| un instante con hora (hecho denunciado) | LocalDateTime | java.util.Date |
| un conjunto cerrado de valores | un enum | String mágico |
| otra entidad del dominio | ese tipo (Vehiculo, Infraccion…) | — |
Bloque A · Catálogo: Infraccion y Radar
No dependen de ninguna otra entidad. Infraccion viene dada en la fuente:
public record Infraccion(
String codigo,
String articulo,
String descripcion,
BigDecimal importeBase,
int puntos,
Gravedad gravedad) { }
Ahora Radar: identificador, ubicación, tipo, límite de velocidad controlado. Pista: la ubicación ya tiene tipo (Coordenadas).
Ver una propuesta de Radar
public record Radar(
String identificador,
Coordenadas ubicacion,
String tipo, // la fuente no enumera los tipos: si los cierras, pásalo a enum
int limiteVelocidad) { } // km/h
Bloque B · Personas y vehículos
| Tipo | Campos que pide la fuente | Pistas |
|---|---|---|
Conductor | NIF/NIE, nombre, domicilio, fecha de expedición, saldo de puntos | domicilio → DireccionPostal. El saldo cambia (R3): ¿record o clase? |
PermisoConduccion | conductor, clases (A, B, C…), fechas de validez, estado | estado → EstadoPermiso. También cambia (R3). |
Vehiculo | matrícula, marca, modelo, tipo, fecha de matriculación, estado de ITV | matrícula → Matricula. |
Titularidad | conductor, vehículo, fecha de alta, fecha de baja | La N:M con atributos. ¿Qué significa una fecha de baja null? |
Ver una propuesta del bloque B
public record Conductor(
String nif, // NIF/NIE — en pruebas, siempre datos sintéticos
String nombre,
DireccionPostal domicilio,
LocalDate fechaExpedicion,
int saldoPuntos) { }
public record PermisoConduccion(
Conductor conductor,
Set<String> clases, // "A", "B", "C"…
LocalDate validoDesde,
LocalDate validoHasta,
EstadoPermiso estado) { }
public record Vehiculo(
Matricula matricula,
String marca,
String modelo,
String tipo,
LocalDate fechaMatriculacion,
LocalDate caducidadItv) { } // una forma de expresar el «estado de ITV»
public record Titularidad(
Conductor conductor,
Vehiculo vehiculo,
LocalDate fechaAlta,
LocalDate fechaBaja) { } // null = titularidad vigente
La propuesta usa record también para Conductor y PermisoConduccion: un cambio de saldo produce un nuevo valor en vez de modificar el existente. Es una decisión discutible a propósito — revisa la decisión 1 de la sección 7 y elige tú.
Bloque C · El procedimiento
| Tipo | Campos que pide la fuente | Pistas |
|---|---|---|
Denuncia | fecha y hora, lugar (con coordenadas), agente o dispositivo denunciante, infracción imputada, vehículo, conductor identificado (si lo hay) | «fecha y hora» → ¿LocalDate o LocalDateTime? «agente o dispositivo»: uno de los dos quedará vacío. |
Expediente | número, estado, importe, fechas del procedimiento; se origina en una denuncia (1:1) | R1 necesita la fecha de notificación. Importe → BigDecimal. |
Notificacion | expediente, intentos, acuse, resultado | resultado → ResultadoNotificacion. |
Alegacion | expediente, escrito, documentos adjuntos | R5 necesita saber si está contestada. |
Resolucion | expediente, acto que pone fin | R2 necesita saber si es firme. |
Pago | expediente, abono total o parcial, posible bonificación | R1: fecha del pago e importe. |
Ver una propuesta de Denuncia y Expediente
public record Denuncia(
LocalDateTime fechaHora, // el hecho ocurre a una hora concreta
Coordenadas lugar,
String agente, // null si la capta un radar
Radar radar, // null si la formula un agente
Infraccion infraccion,
Vehiculo vehiculo,
Conductor conductor) { } // null cuando la capta un radar
public record Expediente(
String numero,
EstadoExpediente estado,
BigDecimal importe,
LocalDate fechaNotificacion, // null mientras no se haya notificado
Denuncia denuncia) { }
Expediente con cuatro argumentos (número, estado, importe, fecha de notificación). Si añades denuncia como quinto campo, ajusta esas llamadas; si no lo añades, explica en el ADR cómo representas la relación 1:1 con Denuncia.Ver una propuesta de Notificacion, Alegacion, Resolucion y Pago
public record Notificacion(
Expediente expediente,
int intento, // 1, 2, 3… «puede requerir varios intentos»
LocalDateTime fechaIntento,
LocalDate fechaAcuse, // null si no hubo acuse
ResultadoNotificacion resultado) { }
public record Alegacion(
Expediente expediente,
LocalDate fechaPresentacion,
String texto,
List<String> documentos, // nombres de los adjuntos (los binarios llegan en la UD5)
boolean contestada) { } // R5
public record Resolucion(
Expediente expediente,
LocalDate fecha,
String fundamento,
boolean firme) { } // R2
public record Pago(
Expediente expediente,
LocalDate fecha,
BigDecimal importe,
boolean bonificado) { } // R1
Las referencias van siempre del hijo al padre (Pago → Expediente). Con record inmutables no puedes crear un ciclo padre ↔ hijo, y eso te ahorra el problema de las «referencias circulares al serializar» que avisa R6.
6Comprueba antes de escribir el ADR
Tres comprobaciones rápidas. Hazlas ahora: si algo falla, cambia el código antes de documentarlo.
a) Los nulos, uno a uno
Recorre cada tipo y pregúntate qué campos pueden ser null legítimamente (porque el dominio lo permite, no por pereza). Apunta la lista para el ADR. Ejemplo de la fuente: Denuncia.conductor es null cuando la capta un radar.
b) Ningún import de fuera en domain
Es la regla de dependencia del apartado f: domain no depende de nada — ni de Spring, ni de JPA, ni de Jackson. Si tu Expediente importa org.springframework.*, el dominio ha dejado de ser el núcleo estable. Compruébalo mirando los import:
grep -rnE "import (org\.springframework|jakarta\.persistence|com\.fasterxml)" src/main/java/es/edu/multagal/domain
Get-ChildItem -Recurse src/main/java/es/edu/multagal/domain -Filter *.java |
Select-String -Pattern 'import (org\.springframework|jakarta\.persistence|com\.fasterxml)'
Resultado correcto: ninguna línea. Sólo deberías ver imports de java.* (java.math.BigDecimal, java.time.*, java.util.*).
c) Compila
./mvnw clean package
7Las cinco decisiones del ADR
Criterio 5: el ADR justifica las decisiones, no sólo las describe. Para cada una: qué decidiste, qué alternativa descartaste y por qué.
1 · ¿record o clase?
¿Cuáles de estos tipos son inmutables por naturaleza y cuáles necesitarán mutabilidad más adelante? En la UD4, JPA exige constructor sin argumentos y campos mutables: ¿afecta eso a la decisión de ahora?
A favor de record
Catálogo (Infraccion), objetos valor y enumerados no cambian. Inmutable, equals por valor, menos código. Es el criterio de los ejemplos de la fuente.
A favor de clase
Lo que tiene estado que evoluciona (saldo de puntos, estado del permiso, estado del expediente). JPA (UD4) pedirá constructor vacío y campos mutables.
2 · Tipo de los importes
BigDecimal, nunca double: los importes son dinero y double no representa exactamente los decimales. Explica por qué en el ADR (y en la defensa oral: «¿Por qué los importes van en BigDecimal y no en double?»).
Una prueba que puedes citar en el ADR (ejemplo construido)
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(new BigDecimal("0.1")
.add(new BigDecimal("0.2"))); // 0.3
3 · Tipo de las fechas
LocalDate frente a LocalDateTime: ¿cuál para la fecha de comisión de la infracción? ¿Y para el plazo de pronto pago? Pista: la denuncia registra «fecha y hora»; el plazo de R1 se cuenta en «días naturales». Nunca java.util.Date.
4 · Objetos valor frente a String
¿Matricula debe ser un tipo propio o basta con un String? Argumenta: seguridad de tipos, dónde vive la validación, legibilidad de las firmas.
5 · Nulos
¿Qué campos pueden ser null legítimamente? Recuerda que Denuncia.conductor lo es cuando la capta un radar. Usa la lista del paso 11.
Plantilla del ADR
Mismo formato simplificado que el ADR 0000 de la A0.2 (decisión, alternativas descartadas, consecuencias), con una entrada por decisión:
# ADR-0001 — Modelado del dominio
**Fecha:** AAAA-MM-DD
**Estado:** aceptada
## Contexto
Primer modelo Java del dominio MULTAGAL, sin persistencia (UD0).
En la UD4 se mapeará con JPA.
## Decisión 1 — record o clase
**Decisión:** ...
**Alternativa descartada:** ...
**Por qué:** ...
## Decisión 2 — Tipo de los importes
**Decisión:** BigDecimal
**Alternativa descartada:** double
**Por qué:** ...
## Decisión 3 — Tipo de las fechas
**Decisión:** ... para la fecha de comisión; ... para el plazo de pronto pago
**Alternativa descartada:** ...
**Por qué:** ...
## Decisión 4 — Matricula: tipo propio o String
**Decisión:** ...
**Alternativa descartada:** ...
**Por qué:** ...
## Decisión 5 — Campos que admiten null
| Campo | ¿Por qué puede ser null? |
|-------|--------------------------|
| Denuncia.conductor | La capta un radar: capta una matrícula, no una persona |
| ... | ... |
## Consecuencias
Qué implica todo lo anterior para la UD3 (JDBC) y la UD4 (JPA).
✓Criterios de aceptación
Así se corrige. Repásalos uno a uno antes de dar la actividad por cerrada.
| # | Criterio | Cómo lo compruebas |
|---|---|---|
| 1 | Están modeladas las doce entidades del dominio | Cuenta los ficheros del bloque A, B y C: 2 + 4 + 6 = 12 |
| 2 | Los importes usan BigDecimal y las fechas la API java.time | Busca double y Date en domain: sólo double en Coordenadas |
| 3 | Los enumerados sustituyen a los String mágicos | Estados, gravedad y resultado son enum |
| 4 | Ningún tipo del paquete domain importa nada de Spring | La búsqueda del paso 12 no devuelve nada |
| 5 | El ADR justifica las decisiones, no sólo las describe | Cada decisión tiene alternativa descartada y porqué |
?Para la defensa oral
Preguntas de la autoevaluación de la UD0 relacionadas con esta actividad. Respóndelas sin mirar.
- ¿Por qué el paquete
domainno debe importar nada de Spring? - ¿Por qué los importes van en
BigDecimaly no endouble? - Enumera las reglas de dependencia entre las capas del proyecto. ¿Puede
controllerllamar arepository?
Adaptado de apartado i — Actividades (A0.3), apartado b — El proyecto MULTAGAL y apartado f — La regla de dependencia.