Guardar una oferta funciona. Guardar veinte copiando empresa, puesto y enlace empieza a parecer otro trabajo. Después quieres llevar una entrevista al calendario y consultar qué tareas se han quedado pendientes. El gestor ya tiene reglas, formularios y almacenamiento; las nuevas necesidades tienen que encontrar su sitio sin convertirlo todo en un servicio enorme.
En el primer taller construimos una aplicación local con arquitectura hexagonal: el dominio protege las reglas, los casos de uso coordinan operaciones y los adaptadores conectan la web y PostgreSQL. Esa separación sigue siendo útil. Lo que ahora necesitamos es organizar varias capacidades y hacer explícito qué pueden pedir unas a otras.
Vamos a ampliar ese proyecto con una importación de ofertas, dos exportadores, recordatorios y estadísticas. La evolución aplica tuberías y filtros, plugins y monolito modular. También utiliza los criterios del artículo de microservicios para tomar una decisión que merece explicación: mantener el gestor en un solo proceso.
Primero, conservar algo que podamos comparar
La aplicación anterior ya distingue guardar una oferta de enviar una candidatura. Ya rechaza una edición antigua, conserva actividades e historial y protege los formularios. Añadir módulos no autoriza a perder ninguna de esas garantías.
La primera transformación consiste en mover responsabilidades y adaptar la construcción. Las funcionalidades llegan después. Esta distinción importa: si una prueba deja de pasar mientras cambiamos paquetes, necesitamos saber si hemos perdido comportamiento o si estamos intentando introducir una regla nueva durante la mudanza.
Tampoco vamos a convertir el gestor en una plataforma de selección. Sigue siendo una aplicación personal y local, con la web existente y una base de datos. El problema que estamos resolviendo cabe en ese alcance.
Módulos por capacidad, capas dentro de cada módulo
En la primera versión, applications reúne dominio, aplicación y adaptadores. Ahora se incorporan importaciones, exportaciones, avisos y estadísticas. No todas necesitan conocer lo mismo ni deberían poder modificar los mismos datos.
El resultado se organiza en ocho módulos Maven:
| Módulo | Qué le corresponde |
|---|---|
tracker-applications |
Candidaturas, actividades, reglas, persistencia y su contrato público. |
tracker-imports |
Leer CSV, previsualizar resultados y confirmar ofertas. |
tracker-exports |
Preparar datos, registrar exportadores y delegar la escritura del formato. |
tracker-export-csv |
Generar el CSV de candidaturas. |
tracker-export-ics |
Generar el calendario de entrevistas. |
tracker-reminders |
Calcular y conservar avisos internos. |
tracker-insights |
Consultar estadísticas y presentar la tasa de entrevistas. |
tracker-app |
Arrancar la aplicación, conectar piezas y configurar lo común. |
La separación por capacidades convive con las capas del primer taller. Importar tiene una entrada web, una lógica de procesamiento y un adaptador de persistencia. Candidaturas conserva sus propias reglas y sus adaptadores. Los controladores de cada funcionalidad permanecen junto a ella; no acabamos de reunirlos todos en una carpeta central que haya que visitar para cualquier cambio.

Las flechas del diagrama representan dependencias de código. Los recuadros no son programas independientes: todo termina en un ejecutable y se despliega junto. Una llamada del importador al contrato de candidaturas es una llamada normal dentro del proceso.
Los dos módulos de plugins son artefactos técnicos que permiten mostrar la extensión. No estamos afirmando que cada JAR sea una capacidad del negocio del mismo tamaño. Esa distinción evita que el número de módulos se convierta en una medida de calidad bastante peculiar.
Una frontera también necesita decidir qué devuelve
El contrato ApplicationsApi permite registrar ofertas y consultar proyecciones de candidaturas, entrevistas, actividades y estadísticas. Un consumidor recibe los datos que necesita, con identificadores estables; no recibe entidades JPA ni una candidatura completa con todo su historial.
Es una decisión sobre dependencias y sobre coste. El exportador de entrevistas necesita el identificador, el título, la hora de inicio y la ubicación. No necesita reconstruir las notas y tareas de cada candidatura para extraer esos campos. El módulo de candidaturas es quien conoce las tablas y prepara la consulta.
Ese mismo criterio se aplica a las escrituras. Importar no ejecuta un INSERT sobre las tablas del gestor: pide registrar una oferta mediante el contrato. El módulo de candidaturas sigue decidiendo qué significa una oferta guardada y qué reglas debe satisfacer.
La interfaz es pública dentro del programa; no es una API HTTP. Tampoco es una invitación a exponer todas las operaciones internas. Cada método se justifica con un consumidor real del taller.
Una fachada para colaborar sin conocer las piezas internas
El importador necesita registrar una oferta. Podríamos conectarlo directamente con RegisterApplication y pedirle que construyese sus argumentos. Para exportar, otro consumidor tendría que conocer ApplicationQueries, sus criterios de búsqueda y sus resultados paginados. Las operaciones existen, pero estaríamos repartiendo el conocimiento de las piezas internas de candidaturas entre quienes las utilizan.
Aquí encaja el patrón Facade, o fachada: ofrecer una entrada más sencilla y acotada a un conjunto de operaciones internas. El consumidor pide una capacidad y la fachada se ocupa de conectarla con las piezas que la resuelven. El módulo conserva margen para reorganizarlas sin exigir que todos sus consumidores aprendan la nueva distribución.
En el gestor distinguimos dos elementos. ApplicationsApi define el contrato público: qué se puede pedir y qué datos cruzan la frontera. ApplicationsFacade implementa ese contrato: recibe las llamadas y las adapta a los casos de uso y consultas existentes. Los demás módulos dependen de la interfaz; la composición de Spring les proporciona la implementación. No necesitan importar la clase de la fachada.

Las flechas muestran llamadas dentro del mismo proceso. ApplicationsApi aparece como el contrato que utiliza el importador; el objeto que atiende la llamada es ApplicationsFacade. No hay una petición HTTP ni un servicio intermedio que tengamos que desplegar.
Seguir una oferta a través de la fachada
Después de validar una fila, el importador pide register(id, offer). La oferta contiene empresa, puesto y los demás campos admitidos por el contrato. La fachada recibe por constructor un RegisterApplication, que conserva en el campo register. Este es el método que atiende la llamada:
public UUID register(UUID id, Offer offer) {
return register.execute(id, OfferMapping.details(offer), Status.SAVED, null).id();
}
OfferMapping.details convierte la representación pública en ApplicationDetails, cuya construcción aplica las validaciones del dominio. Después, RegisterApplication ejecuta el caso de uso. La operación pública registra una oferta guardada: por eso utiliza SAVED y deja vacía la fecha de solicitud, representada aquí por null. Devuelve el identificador, en lugar de entregar el objeto de dominio al importador.
Las reglas de candidaturas continúan en su dominio y sus casos de uso. La fachada adapta la operación que ofrece al exterior. En las lecturas también hace trabajo concreto: reúne las páginas que devuelve la búsqueda interna y las convierte en las proyecciones que necesita la exportación. El exportador puede centrarse en representar esos datos sin aprender cómo se pagina la consulta.
La implementación vive en adapter.module, como adaptador de entrada al módulo. Puede utilizar Spring y delimitar una transacción de lectura; los casos de uso y el dominio siguen sin depender de ese framework. Conservar la arquitectura hexagonal implica respetar esa dirección de dependencias, también al añadir una entrada para otros módulos.
Qué ganamos y qué estamos pagando
La fachada concentra la adaptación entre el contrato público y la implementación. Si cambia la organización interna de una consulta, podemos ajustar esa adaptación conservando la operación que utilizan sus consumidores. Si cambia el significado de la operación pública, sí habrá que revisar el contrato y a quienes dependen de él. La fachada reduce el acoplamiento a los detalles internos; no elimina las dependencias entre capacidades.
| Alternativa | Qué implica en este gestor |
|---|---|
| Acceder directamente a las clases internas | Ahorra la adaptación inicial, pero cada consumidor conoce casos de uso, criterios y resultados que pueden cambiar por necesidades internas de candidaturas. |
| Mantener una fachada del módulo | Añade una implementación y mapeos, a cambio de ofrecer operaciones acotadas sobre las piezas existentes. Es la opción de esta entrega. |
| Separar contratos de registro y consulta | Permite que cada consumidor dependa de menos operaciones, con más interfaces y conexiones que mantener. Tendría sentido si sus necesidades o ritmos de cambio empiezan a divergir. |
También hay un riesgo de concentración. Si cualquier necesidad nueva acaba como otro método de ApplicationsFacade, la clase puede convertirse en un servicio enorme que acumule decisiones de importación, exportación y recordatorios. Esas políticas pertenecen a sus módulos. La fachada de candidaturas debe seguir ofreciendo capacidades de candidaturas.
No hace falta añadir una fachada cada vez que dos componentes colaboran. Una operación pública existente puede ser suficiente. Aquí la elegimos porque adaptamos varias operaciones internas a un contrato entre módulos con consumidores reales. Revisaríamos la decisión si la interfaz creciese con operaciones que sus consumidores no comparten o si la fachada empezase a concentrar reglas de otras capacidades.
La fachada también necesita una frontera comprobada
Hay un detalle fácil de exagerar: Maven no oculta automáticamente todas las implementaciones. Si un tipo no está en el classpath, el compilador no puede utilizarlo. Pero una dependencia puede llegar de forma transitiva y los tipos públicos de un JAR disponible siguen siendo accesibles. Llamar internal a un paquete no cambia la visibilidad de Java.
Por eso mantenemos ArchUnit para rechazar accesos a implementaciones y ciclos. Una prueba negativa demuestra el fallo de compilación por ausencia de un tipo. Otra utiliza una clase inválida de pruebas para demostrar que una implementación accesible puede infringir la regla arquitectónica. El control y su límite quedan visibles. Diseñar artefactos exclusivos de contratos o adoptar JPMS podría reforzar otras fronteras, con un coste adicional que esta entrega no necesita asumir.
Importar empieza bastante antes de guardar
El CSV del taller es un formato propio y documentado. No se presenta como una exportación real de un portal. Eso permite concentrar el ejercicio en la arquitectura sin depender de que un proveedor mantenga una cabecera o permita descargar ofertas.
Una entrada sencilla sería:
source,external_id,company,role,url,location,work_mode,technologies
demo,offer-001,Example Labs,Java Developer,https://example.test/jobs/001,Madrid,REMOTE,Java
demo,offer-002,Example Studio,Backend Developer,https://example.test/jobs/002,Sevilla,HYBRID,Spring
demo,offer-003,,Invalid offer,,,,
Las cuatro primeras columnas son obligatorias. Las demás son opcionales, con modalidad UNSPECIFIED si no se indica. El archivo utiliza UTF-8, coma y comillas CSV; puede contener comas o saltos de línea dentro de una celda. Por eso lo lee una biblioteca de CSV, no una llamada a split(",") que funciona hasta que una empresa se llama «Example, Inc».
Antes del procesamiento se comprueban el tamaño, el número de registros y la estructura. El límite es 1 MiB y 1.000 ofertas. Una cabecera desconocida o un archivo ilegible impiden continuar: no podemos prometer un informe completo si ni siquiera sabemos dónde termina el siguiente registro.
Eso es distinto de una fila bien formada con una empresa vacía. En ese caso sabemos qué dato falta, podemos explicar el rechazo y seguir con las demás.
La tubería transforma o rechaza; todavía no escribe
La primera etapa recorta espacios y normaliza los campos que tienen una política conocida. La fuente pasa a minúsculas; el identificador externo conserva sus mayúsculas. La modalidad se normaliza y recibe su valor por defecto cuando está vacía.
Después se exigen los campos obligatorios y se valida la oferta con el contrato de candidaturas. Reutilizar esa validación evita que importar acepte una URL o una modalidad que el registro normal del gestor rechazaría.
El contrato de cada filtro devuelve una fila aceptada o un motivo de rechazo. La tubería conecta los filtros en orden y detiene únicamente el recorrido de la fila rechazada:
public FilterResult apply(ImportRow original) {
FilterResult result = FilterResult.accept(original);
for (RowFilter filter : filters) {
result = filter.apply(result.row());
if (!result.accepted()) {
return result;
}
}
return result;
}
El orden tiene consecuencias. Una empresa formada por espacios debe quedar vacía antes de comprobar si falta. Si la validación se ejecutase sobre una representación distinta de la que guardamos, estaríamos haciendo dos promesas diferentes con el mismo formulario.

Aquí las flechas representan ejecución. La previsualización llega hasta el informe y se detiene. La escritura se produce en la confirmación, con una nueva comprobación del estado guardado.
Con el archivo del ejemplo, las dos primeras filas pueden aparecer como nuevas y la tercera se rechaza por empresa obligatoria. Todavía no hay candidaturas nuevas en la base. Esa ausencia de escrituras es un comportamiento comprobado expresamente, no una impresión que obtenemos mirando la pantalla.
El archivo previsualizado se conserva en la sesión durante quince minutos. El navegador no decide qué filas han pasado ni vuelve a enviar un conjunto de datos normalizados como prueba de validez. Al confirmar, el servidor utiliza el contenido que conservó y vuelve a evaluarlo contra el estado actual. Entre ambos pasos podría haberse importado otra vez la misma oferta.
Repetir una importación no debe pisar una edición personal
Identificar duplicados exige una regla. En esta entrega utilizamos la pareja source e external_id. No deducimos que dos puestos parecidos en la misma empresa sean la misma oferta, ni intentamos identificar oportunidades que llegan desde portales distintos.
La normalización forma parte de esa identidad: DEMO y demo son la misma fuente; AbC y abc pueden seguir siendo identificadores externos diferentes. La pareja queda protegida por una clave única en PostgreSQL.
Además conservamos una huella del contenido original normalizado. Si vuelve la misma clave con el mismo contenido, se omite como repetida. Si cambia el contenido importado, aparece un conflicto. No se actualiza automáticamente la candidatura.
Imagina que importas una oferta y después corriges el nombre de la empresa o añades tus notas. Reimportar el CSV original debe reconocer la repetición y respetar esas modificaciones. Comparar el archivo con la candidatura vigente daría un conflicto engañoso o, peor, podría sobrescribir el trabajo personal. La huella se compara con la entrada importada anterior.
Las claves repetidas dentro del mismo archivo siguen el mismo criterio. Una copia idéntica se omite; una segunda versión distinta se señala. El informe conserva el número del registro CSV. Ese número puede no coincidir con una línea física si una celda contiene saltos de línea, por lo que la documentación explica la diferencia.
Dos escrituras que deben confirmarse juntas
El importador guarda un registro de procedencia y pide al gestor que cree la candidatura. Esas dos escrituras tienen que ocurrir juntas. Si solo guardásemos la procedencia y fallase la creación, una nueva importación podría omitir una oferta que nunca llegó a existir.
La confirmación utiliza una transacción por fila. Primero intenta reservar la clave de importación y después registra la oferta en el mismo contexto transaccional:
private String saveInsideTransaction(ImportEntry entry) {
UUID id = UUID.randomUUID();
int inserted =
jdbc.update(
"""
INSERT INTO imported_offers (source, external_id, fingerprint, application_id)
VALUES (?, ?, ?, ?) ON CONFLICT (source, external_id) DO NOTHING
""",
entry.source(),
entry.externalId(),
entry.fingerprint(),
id);
if (inserted == 0) {
return ledger
.fingerprint(entry.source(), entry.externalId())
.orElseThrow()
.equals(entry.fingerprint())
? "DUPLICATE"
: "CONFLICT";
}
applications.register(id, entry.offer());
return "IMPORTED";
}
La llamada a candidaturas sigue pasando por su contrato. La transacción compartida no autoriza al importador a conocer sus tablas. Es una dependencia real de ejecución, documentada en el ADR: ambos módulos participan en el mismo gestor de transacciones y en la misma base.
¿Y si dos pestañas confirman a la vez? Consultar antes de guardar no basta, porque ambas podrían haber visto la clave libre. La restricción única y la operación de reserva arbitran esa carrera. Una confirmación crea la candidatura; la otra reconoce la clave confirmada y responde como repetición o conflicto. La garantía no depende del orden en que el usuario haya pulsado los botones.
El lote completo, sin embargo, no es atómico. Si hay filas rechazadas, se guardan las válidas. Si falla el almacenamiento a mitad del proceso, se detiene y se informa de lo confirmado y lo pendiente. Una nueva previsualización reconoce las claves anteriores y permite revisar el resto.
Esa elección favorece una importación personal que puede corregirse por partes. Un proceso que exigiese aceptar todo el documento o nada necesitaría otra política. No sería correcto cambiarla silenciosamente manteniendo el mismo mensaje de confirmación.
Dos formatos, un mecanismo de extensión
Exportar candidaturas e entrevistas tiene otra forma de variar. La consulta prepara datos y el formato decide cómo representarlos. Mezclar ambas responsabilidades haría que añadir un exportador obligase a revisar consultas, selección y escritura del archivo a la vez.
ExportPlugin expone un descriptor y una operación de escritura. El descriptor identifica el plugin, el conjunto de datos, el formato, el tipo de contenido y el nombre de archivo. ExportData contiene columnas y filas inmutables, junto con la fecha de generación.
El registro no pregunta si el formato es CSV o ICS. Busca un identificador y delega. Al arrancar, rechaza identificadores duplicados:
public ExportRegistry(List<ExportPlugin> exporters) {
var registered = new LinkedHashMap<String, ExportPlugin>();
for (ExportPlugin plugin : exporters) {
if (registered.putIfAbsent(plugin.descriptor().id(), plugin) != null) {
throw new IllegalArgumentException("Duplicate export plugin: " + plugin.descriptor().id());
}
}
plugins = Map.copyOf(registered);
}
Hay otra variación separada: cómo preparar el conjunto de datos. Las fuentes de candidaturas y entrevistas utilizan ApplicationsApi; los plugins reciben el resultado y no conocen ni ese contrato de consulta ni las tablas. Añadir otro formato para un conjunto existente requiere otra implementación y su registro. Añadir un conjunto de datos requiere también definir su fuente y su contrato de columnas.

Las flechas representan dependencias y delegación dentro del proceso. No hay descarga de extensiones ni carga dinámica de JAR. Los plugins se incluyen al construir la aplicación y comparten sus recursos. Una interfaz permite sustituir el comportamiento; no aísla un plugin que agote la memoria.
La extensión se comprueba con un plugin de texto que existe solo en pruebas. Se registra y produce su salida sin introducir una excepción por formato en el núcleo. Esa prueba pone una condición concreta a la promesa de extensibilidad.
Exportar correctamente también tiene reglas
El CSV de candidaturas respeta los filtros del listado y exporta todas las coincidencias, no solo las veinte que caben en la página visible. Las consultas se recorren con un orden estable dentro de una transacción de lectura repetible.
El archivo incluye identificador, empresa, puesto, estado, modalidad y fechas. No es el mismo formato que la entrada de ofertas ni pretende sustituir una copia de seguridad. Comas, comillas y saltos de línea se escapan como CSV. Los textos que una hoja de cálculo podría tratar como fórmulas se protegen anteponiendo un apóstrofo; es una transformación deliberada que también se documenta.
El calendario exige otras decisiones. Cada entrevista tiene un UID basado en su identificador, de modo que exportarla dos veces conserva esa identidad. Las fechas se representan en UTC. El texto se escapa y las líneas largas se pliegan contando octetos, sin cortar un carácter UTF-8 por la mitad.
El gestor no registra una hora de fin. El exportador no inventa que todas las entrevistas duren una hora: genera el inicio sin duración. Se incluyen las entrevistas programadas de candidaturas no archivadas dentro del intervalo, y se excluyen las canceladas y completadas.
Todo esto sigue siendo una descarga. El archivo no mantiene una suscripción ni garantiza que un calendario elimine una cita importada anteriormente. Tener un UID estable ayuda a representar la misma entrevista; no convierte un fichero en un protocolo de sincronización.
Recordatorios y estadísticas como consumidores del contrato
Los recordatorios consultan actividades abiertas y aplican una política pequeña: tareas con fecha anterior al día actual y entrevistas que empiezan en las próximas veinticuatro horas. El día se calcula en la zona configurada. La ventana de entrevistas se mide con instantes; no se supone que un día local siempre tenga veinticuatro horas cuando cambia el horario.
La política recibe un reloj, así que las pruebas pueden situarse justo en el límite, durante un cambio de hora o en una fecha concreta sin esperar al calendario real.
El trabajo se ejecuta al arrancar y cada minuto. Conserva los avisos mediante una clave formada por actividad, tipo y vencimiento. Repetir la revisión no añade copias; completar, cancelar o reprogramar una actividad resuelve el aviso anterior. La sustitución de los avisos activos y la marca de actualización se confirman juntas.
La pantalla muestra cuándo se revisaron los datos. Si la revisión falla, conserva la última instantánea e indica la incidencia. Con la aplicación apagada no se producen avisos: al volver a arrancar se revisan los pendientes. El mecanismo no incluye correo ni notificaciones del móvil.
Las estadísticas también usan el contrato de candidaturas. Se muestran candidaturas por estado y una tasa de entrevistas con denominador explícito: candidaturas con fecha de solicitud que tienen alguna entrevista no cancelada, divididas entre todas las candidaturas con fecha de solicitud. Se excluyen las archivadas; las ofertas solo guardadas no cuentan como solicitudes enviadas.
Con tres candidaturas enviadas y una con entrevista, la tasa es 33,3 %, redondeada a una cifra decimal. Si no hay solicitudes, se muestra «Sin datos». La fecha de un cambio manual de estado no demuestra cuándo respondió una empresa, por lo que no añadimos un tiempo medio de respuesta que el modelo no puede sostener.
Por qué los avisos todavía no son un microservicio
Ahora que los recordatorios tienen contrato y almacenamiento propios, separarlos parece un siguiente paso tentador. Pero una frontera de código y una necesidad de despliegue independiente son preguntas distintas.
En este gestor sigue habiendo una persona usuaria y una aplicación local. No hay equipos que necesiten publicar a ritmos distintos ni medidas que exijan escalar los avisos por separado. Separarlos añadiría configuración, disponibilidad de otro proceso y una comunicación que puede fallar de forma parcial.
También cambiarían las garantías. La importación utiliza una transacción local para conectar procedencia y candidatura. Si esa frontera cruzase servicios con bases independientes, esa transacción dejaría de existir. Habría que diseñar cómo reconocer resultados inciertos y recuperar operaciones incompletas.
El monolito tiene sus costes: todos los módulos comparten memoria y despliegue, y una operación pesada puede afectar al conjunto. El ADR no los esconde. Fija cuándo revisar la decisión: necesidades sostenidas de aislamiento, capacidad o autonomía de entrega que compensen el coste adicional.
Un cliente móvil podría justificar otra entrada a las capacidades del gestor. Por sí solo no justificaría repartirlas en servicios. La decisión se evalúa por el problema de operación que resuelve.
Comprobar el recorrido y reconocer lo que sigue costando
La calidad conserva los controles del primer proyecto: formato compartido, límites de tamaño y complejidad, detección de duplicados y pruebas de arquitectura en código y tests. Dividir el proyecto obliga a aplicarlos al reactor completo y a recoger los informes de cada módulo. Un módulo nuevo no debe quedar fuera porque el script todavía busque una sola carpeta de fuentes.
Las pruebas de importación distinguen un filtro que rechaza una fila, una previsualización que no escribe, una reimportación que respeta ediciones personales y dos confirmaciones que compiten por la misma clave. Otra provoca un fallo en la creación para comprobar que el registro de procedencia también se revierte.
Los archivos se leen con bibliotecas independientes de las que los escriben. La migración parte del esquema de la primera versión y verifica la conservación de identificadores, fechas, versión, notas, tareas e historial. El navegador recorre la importación y las páginas nuevas en escritorio y móvil, además del recorrido original del gestor.
El diseño acepta límites concretos. Las exportaciones se preparan en memoria; el CSV de entrada está acotado; los avisos conservan registros resueltos y podrían necesitar retención. El contrato tabular de plugins es cómodo para estos dos formatos, pero otro conjunto más rico podría pedir otra representación. Nada de eso mejora automáticamente por tener más módulos.
La medida útil es seguir una necesidad completa: el dato que entra, la regla que lo acepta, el contrato que cruza y el efecto que se confirma. Si cambiar el formato de un archivo exige modificar las reglas de candidaturas, o calcular una tasa obliga a consultar tablas desde otro módulo, la frontera necesita revisión aunque el diagrama siga quedando estupendo.
Para practicar
- Añade espacios a la empresa y a la fuente de una fila. Explica qué etapa los elimina y qué ocurriría si validases antes de normalizar.
- Previsualiza un archivo, confirma y vuelve a importarlo. Después cambia el puesto conservando la clave. Compara repetición y conflicto sin modificar la candidatura guardada.
- Añade un exportador de texto para candidaturas. Comprueba que basta con implementar el contrato y registrarlo; no añadas un caso especial al servicio de exportación.
- Abre dos confirmaciones de la misma oferta. Localiza qué garantía aporta la base de datos y por qué la consulta previa no puede sustituirla.
- Escribe qué necesidad concreta justificaría separar los recordatorios y cómo comprobarías que esa separación la resuelve.
- Sigue una llamada desde el importador hasta
ApplicationsFacade. Identifica qué tipos internos dejaría de conocer el consumidor y qué cambios en el contrato público seguirían obligando a revisarlo.