← Volver a proyectos

Proyecto 01 / Caso de estudio

Backend y producto · en desarrollo activo

De una hoja de contactos a un CRM que sabe a quién escribe.

Un SaaS de captación comercial construido desde cero para un circuito de eventos: multiempresa, con entregabilidad vigilada y un modelo que clasifica industrias sin salir del servidor. Es el proyecto donde más he aprendido de sistemas, de correo y de límites.

103Endpoints REST
49Entidades JPA
135Tests unitarios
6APIs externas integradas
RolDiseño, backend, frontend y operación
RecorridoDesde cero, en evolución continua
TecnologíasJava 21, Spring Boot 3.5, React 18, MariaDB, Docker
01

El reto

La captación de patrocinio vivía en hojas de cálculo: nadie sabía a quién se había escrito, los correos rebotaban sin que nadie lo viera y los dominios se quemaban en dos campañas. Hacía falta una sola fuente de verdad para decenas de miles de fichas, con reglas de envío que no hundan la reputación del remitente.

02

La solución

Un monolito modular con aislamiento por entidad, cola de envíos sobre la propia base de datos, perfiles de calentamiento por cuenta y webhooks que devuelven la realidad (entrega, apertura, rebote, queja, baja). La clasificación por industria la hace un modelo local, y cada ejecución queda registrada y es revisable.

03

El aprendizaje

Que el trabajo interesante no era el CRUD sino la entregabilidad y el aislamiento de datos. Y que el control de secretos fue mi punto flaco: un fichero de configuración acabado donde no debía me costó más de lo que jamás me costaría un checklist.

Arquitectura

Un monolito con capas claras y una cola dentro.

Monorepo con backend, frontend, experimento de IA y paquete de despliegue. El backend separa veintitrés controllers, cuarenta y cuatro services y treinta y dos repositorios sobre cuarenta y nueve entidades JPA, con ciento veintidós registros de Java haciendo de contratos de la API —noventa y siete con sufijo Request o Response—.

El aislamiento multiempresa es la decisión que arrastra todo lo demás. La jerarquía Entidad → Proyecto → Usuarios cuelga de un servicio de autorización que comprueba en cada operación si la fila pertenece a la entidad del usuario. Las empresas no llevan columna de entidad: se aíslan a través de la campaña que las mueve, porque la misma ficha puede aparecer en dos campañas con tratamientos distintos.

Los envíos no pasan por un broker: se encolan en una tabla y un planificador revisa cada segundo, con reintento y recuperación automática de envíos colgados a los sesenta segundos. Si el proceso muere a mitad de una campaña, al arrancar la cola sigue ahí y el trabajo no se duplica.

Alrededor de ese núcleo: validación de direcciones antes de escribir, blocklist por entidad que cancela lo encolado, bajas de un clic con token firmado, plantillas con adjuntos y más de quince variables, y sincronización periódica del correo de cada usuario por OAuth con los tokens refrescados cifrados con AES-256-GCM en base de datos.

Campaña → movilización (sin duplicados, fuera blocklist) → perfil de envío: ventana, pausa, cuota diaria, rampa → cola en tabla → worker (concurrencia 5, ciclo 1 s) → proveedor de correo → webhook: entrega · apertura · rebote · queja · baja → bandeja única con hilos, asignación y estado
  • Backend

      Java 21Spring Boot 3.5Spring SecuritySpring Data JPAActuatorLombokMaven
  • Datos

      MariaDB49 entidades109 claves foráneas27 guiones de evolución de esquemaRestricciones CHECK
  • Frontend

      React 18TypeScript 5.7Vite 6DOMPurifyCSS propio
  • Integraciones

      Correo transaccionalDNS programáticoValidación de direccionesGoogle OAuthAPI de correoOllama
  • Operación

      DockerNginxHealthchecksTres ramas de publicación

Decisiones

Lo que elegí y lo que dejé de lado a propósito.

Cuatro decisiones que se pueden discutir. Cada una con su alternativa y su factura.

01

Sesión de servidor con CSRF, no JWT

Una SPA servida en el mismo origen no necesita repartir tokens. Descarté JWT en almacenamiento local: ante una inyección de script se lo llevan, y revocarlo de verdad exige una lista negra que anula la ventaja. El precio es afinidad de sesión y cookies; a cambio, el estado de sesión vive donde yo mando.

02

Cola en tabla en lugar de broker

Miles de envíos diarios, un solo entorno activo y la necesidad de ver la cola desde la propia interfaz y rescatar envíos colgados. Descarté Redis y RabbitMQ: son otra máquina que vigilar y otro modo de fallar. El coste real es que no hay orden garantizada ni multicast, y que la cola compite con la base de datos por recursos.

03

Permisos programáticos en vez de anotaciones

Cinco roles y unos trece permisos, aplicados desde un servicio que comprueba la pertenencia a la entidad en cada llamada —ciento dieciocho puntos de control—. Descarté los anotadores de método: un permiso que depende de a qué empresa pertenece la fila no cabe en una etiqueta estática sobre el método.

04

Modelo local, resultados auditables

Clasificar la industria de unas doscientas sesenta mil fichas contra una API de pago era inviable. Un modelo pequeño en mi propia GPU lo hace a coste marginal cero, con temperatura baja y salida JSON. Cada ejecución se guarda con su entrada y su resultado para poder reprocharla. Lo que falta: el agente que decide el nivel de patrocinio con evidencias está diseñado en documento y sin implementar, y el proveedor comercial sigue preparado por variables sin código detrás.

Resultados

Lo que se puede contar con el código delante.

Cifras medidas sobre el repositorio, no sensaciones.

  • 21.733líneas de Java en 297 ficheros, 3.513 de ellas de prueba
  • 9.468líneas de TypeScript y TSX
  • 4.531líneas de SQL en 27 guiones de evolución de esquema
  • 7.116líneas de documentación interna viva
  • 135pruebas en 30 ficheros: unitarias con Mockito y dos de integración
  • 3.000fichas de muestra en la base de desarrollo, con el esquema completo

Lo que haría distinto

La interfaz es una aplicación de una sola hoja de seis mil cuatrocientas líneas sin enrutador y sin una sola prueba: funciona, pero es el sitio donde una segunda persona no podría entrar. Le falta integración continua y un despliegue que no dependa de que yo recuerde el orden. Y el episodio del fichero de configuración con credenciales en el paquete de despliegue acabó en rotación de claves y en una regla simple: ejemplo sin valores y secretos solo en el host. La parte aburrida de la ingeniería es, al final, la que te evalúa.