El dominio que no sabía que existía una base de datos
Soy Ambrosio. Este post tiene destinatario con nombre: es para Andrés, que está aprendiendo Java y Spring Boot ahora mismo y acaba de montar su primera aplicación —limpia, funcionando, con el orgullo bien puesto—. Se la enseñó a un amigo, y de ahí saltó un debate de los buenos: las anotaciones de la base de datos estaban pegadas directamente en la entidad del dominio.
Andrés, esto es para que veas ese debate desde abajo, con calma, sin dogmas y sin malos de la película. Porque spoiler: en esa charla teníais razón los dos, cada uno en su sitio. Y quien lea esto sin ser Andrés, tranquilo, que se entiende igual. Vamos.
El punto de partida: la entidad que lo hace todo
Cuando aprendes Spring Boot, lo primero que te enseñan es lo más rápido, y con razón. Coges tu clase, le pones cuatro anotaciones, y ya tienes una tabla en la base de datos y un repositorio que sabe leer y escribir. Magia:
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id @GeneratedValue
private Long id;
private String email;
private String nombre;
protected Usuario() {} // Hibernate exige un constructor vacio
// ...getters y setters...
}
// Y esto, una linea, te da guardar/buscar/borrar gratis:
public interface UsuarioRepository extends JpaRepository<Usuario, Long> { }Esto funciona, es rápido de escribir y para muchísimas aplicaciones es más que suficiente. Que quede claro desde el minuto uno: no es un error. Es el camino corto, y el camino corto muchas veces es el correcto.
Pero tiene una consecuencia escondida, y ahí empieza lo interesante.
La pregunta incómoda: ¿tu dominio sabe que existe una base de datos?
Mira otra vez la clase Usuario. Es,
supuestamente, un concepto de tu negocio: un usuario. Y
sin embargo está llena de cosas que no tienen NADA que ver con el
negocio: @Entity, @Table, @Id, @GeneratedValue… Todo eso son detalles de
cómo se guarda en una base de datos.
Dicho de otro modo: tu concepto de negocio depende de Hibernate, de JPA y de la base de datos. Si lo dibujamos, la flecha de dependencia va así:
┌───────────────────┐ depende de ┌──────────────────────┐
│ Usuario (dominio)│ ───────────────────────► │ JPA / Hibernate / BD │
└───────────────────┘ └──────────────────────┘
El corazón de tu negocio conoce —y necesita— la fontanería.
¿Es esto un drama? Depende. Pero antes de decidir, hay que ver la idea que le da la vuelta a la flecha.
La idea clave: el dominio en el centro
La arquitectura hexagonal (Ports & Adapters, y sus primas hermanas la arquitectura cebolla y la clean architecture —familia, con matices distintos—) parte de una sola regla, y si te quedas con una cosa del post que sea esta:
Lo más importante y estable de tu aplicación —las reglas de tu negocio— debe estar en el centro y no depender de nada. Todo lo demás (la base de datos, la web, el framework) depende de él, nunca al revés.
Visualízalo como capas, con las dependencias apuntando SIEMPRE hacia dentro:
┌──────────────────────────────────────────────┐
│ INFRAESTRUCTURA │
│ (JPA, Spring, la base de datos) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ DOMINIO │ │
│ │ │ │
│ │ Usuario (objeto puro) │ │
│ │ UsuarioRepositorio (interfaz) │◄────┼── el "puerto"
│ │ │ │
│ └──────────────────────────────────┘ │
│ ▲ │
│ │ implementa │
│ UsuarioRepositorioJpa │
│ (el "adaptador") │
└──────────────────────────────────────────────┘
Las flechas apuntan HACIA DENTRO. El dominio no mira afuera.
La base de datos pasa de ser el centro del universo (como en muchos tutoriales) a ser un detalle que se conecta por fuera. En el caso ideal podrías cambiar Postgres por un fichero y el dominio apenas se entera (saltar a algo tan distinto como Mongo a veces te obliga a repensar el diseño, no es magia gratis —pero la dirección es justo esa).
Puertos y adaptadores, con código
La pieza que hace el truco es una interfaz. En el dominio declaras QUÉ necesitas, sin decir CÓMO. Eso es un puerto —y el principio que hay detrás tiene nombre, por si quieres tirar del hilo: *inversión de dependencias*—:
// ---------- DOMINIO: ni una sola anotacion de framework ----------
public final class Usuario { // objeto puro de negocio
private final UsuarioId id;
private final Email email; // un Email valido SIEMPRE (Value Object)
private final String nombre;
public Usuario(UsuarioId id, Email email, String nombre) {
// aqui vive la logica de negocio: validar, invariantes...
this.id = id; this.email = email; this.nombre = nombre;
}
// comportamiento de negocio, no setters tontos
}
public interface UsuarioRepositorio { // EL PUERTO (vive en el dominio)
Optional<Usuario> buscarPorId(UsuarioId id);
void guardar(Usuario usuario);
}Y en infraestructura pones el adaptador: la implementación real, que ahí sí usa JPA y Spring Data todo lo que quiera, porque es su sitio:
// ---------- INFRAESTRUCTURA: aqui vive la fontaneria ----------
@Entity @Table(name = "usuarios")
class UsuarioJpaEntity { // modelo de PERSISTENCIA, aparte
@Id Long id;
String email;
String nombre;
// el constructor vacio y los setters de Hibernate viven AQUI, no en el dominio
}
@Repository
class UsuarioRepositorioJpa implements UsuarioRepositorio { // EL ADAPTADOR
// interface SpringDataUsuarioDao extends JpaRepository<UsuarioJpaEntity, Long>
private final SpringDataUsuarioDao dao; // Spring Data por dentro, oculto
public Optional<Usuario> buscarPorId(UsuarioId id) {
return dao.findById(id.valor()).map(this::aDominio); // mapea BD -> dominio
}
public void guardar(Usuario usuario) {
dao.save(aPersistencia(usuario)); // mapea dominio -> BD
}
// aDominio() y aPersistencia() traducen entre los dos mundos
}Sí, hay más clases. Sí, hay que mapear de un lado a otro. Guárdate esa sensación de "cuánto código de más", que en un momento la recogemos.
Por qué importa de verdad (el lado técnico)
Aquí está el argumento fuerte a favor de separar, y no es religión,
es física del código. Cuando pones @Entity
en tu clase de dominio, Hibernate te obliga a cosas que tu negocio no
pediría jamás:
- Un constructor vacío (para que
Hibernate pueda instanciar el objeto "a medio hacer"). Adiós a
garantizar que un
Usuariosiempre nace válido. - Setters mutables, para que el ORM rellene los campos. Adiós a la inmutabilidad y a proteger tus invariantes.
- Carga perezosa (lazy loading), proxies, y sorpresas de sesión que se cuelan en tu lógica de negocio sin invitación.
Es decir: las anotaciones no son un adorno inocente.
Empujan el diseño de tu dominio a servir a la base de
datos. Ojo, no es que con @Entity no puedas meter lógica de negocio
—puedes, con setters privados y métodos de fábrica hay un punto medio
perfectamente válido: una entidad JPA *rica*—. Es que vas remando contra
el ORM: él quiere su constructor vacío, su mutabilidad, sus proxies. En
un dominio simple te da igual. En un dominio con reglas de verdad, ese
remar te acaba costando caro.
Entonces, ¿el amigo lo hizo mal?
No. Rotundamente no. Y esta es la parte que más me importa del post.
El que está aprendiendo hizo lo correcto para donde está. La entidad anotada con su repositorio automático es:
- La forma más común en el mundo Spring (millones de apps en producción así).
- La más rápida de escribir y la más fácil de entender cuando empiezas.
- Perfecta para un CRUD sencillo, un prototipo, una herramienta interna, un proyecto para aprender.
Meterle arquitectura hexagonal completa a un CRUD de cuatro pantallas es sobre-ingeniería: te comes todo el boilerplate (las interfaces, el mapeo de ida y vuelta) sin cobrar el beneficio. Y el código sobra-diseñado también es deuda, no solo el sub-diseñado.
La gracia del boilerplate: comprar flexibilidad
Y llegamos al corazón, que me lo apuntó el amigo veterano y es exactamente el quid: ese código de más no es un desperdicio, es una inversión.
El mapeo y las interfaces que parecen ceremonia hoy, mañana te compran cosas muy concretas:
- Testear el negocio sin base de datos. Como el dominio no conoce JPA, pruebas tus reglas en milisegundos, sin levantar nada. Un test de dominio puro vuela.
- Cambiar la infraestructura sin tocar el negocio. ¿Migras de SQL a otra cosa? ¿Metes una caché delante? Cambias el adaptador, el dominio ni se entera.
- Un dominio que se lee como el negocio, no como la base de datos. Tus clases hablan de reglas, no de columnas. Cuando la lógica crece, esto es la diferencia entre mantenerlo y odiarlo.
- Invariantes garantizadas. Un
Usuarioque no puede existir en estado inválido, porque nadie lo construye "a medias" para el ORM.
El boilerplate es una póliza de seguro. En un proyecto que va a durar años y a tener lógica de verdad, la pagas encantado. En un script de fin de semana, es tirar el dinero.
La regla para decidir (llévate esto)
No hay una respuesta buena, hay una pregunta buena: ¿qué tipo de aplicación es esta?
¿CRUD simple, prototipo, herramienta interna, estas aprendiendo?
└─► Entidad anotada + Spring Data. Rapido y suficiente. SIN culpa.
¿Dominio rico, reglas de negocio de verdad, app que vivira anos,
necesitas testear la logica en aislado?
└─► Dominio puro + puerto en el dominio + adaptador JPA fuera.
El boilerplate te devuelve flexibilidad con intereses.
Un síntoma concreto, por si no sabes en cuál estás: si tu clase de negocio hoy solo tiene getters y setters, el acoplamiento aún no te cuesta nada. El día que quieras meterle un método que valide una regla de verdad y te estorbe el constructor vacío o la mutabilidad, ahí empieza a compensar separar.
Lo profesional no es "siempre DDD puro". Es saber cuándo cada uno. El que mete hexagonal en una lista de la compra y el que acopla a Hibernate el core de un banco cometen el mismo pecado: no mirar el contexto.
Para el que está empezando
Si estás aprendiendo ahora, este es el camino que yo te diría:
- Aprende primero la forma acoplada. Es real, es útil, no es "la forma mala". Con ella entiendes JPA, Spring y cómo se mueve un dato de punta a punta.
- Cuando la tengas dominada, aprende por qué existe la separación. No para usarla siempre, sino para ver la costura: saber dónde está pegado el dominio a la base de datos.
- El día que un proyecto te empiece a doler —tests lentos, cambios que rompen diez sitios, lógica enredada con consultas— ya sabrás qué herramienta sacar.
Entender el compromiso vale mil veces más que seguir el dogma. El que empieza tiene una base estupenda, y una cabeza que ya pregunta "¿y esto por qué?", que es lo único que de verdad hace falta para llegar lejos.
Y ahora, la invitación de verdad
Andrés, lo mejor de todo esto no es el post — es lo que viene después. Pásanos el enlace de tu repositorio y le echamos un vistazo juntos, sin prisa y sin vergüenza (que tu código está muy bien para donde estás, en serio). La jugada sale ganando todo el mundo: tú ves tu propio código con otros ojos y aprendes más DDD, el amigo con el que lo debatiste afila su Java y su Spring, y yo me lo paso teta haciendo de maestro un rato. No para decirte lo que está "mal" —para enseñarte a ver las costuras y que decidas tú. Cuando quieras, aquí estamos.
—
Así que no, no ganó nadie la discusión. Ganó el hecho de tenerla. Ver la costura entre tu negocio y tu base de datos —y decidir a conciencia si la pegas o la separas— es, con diferencia, lo que distingue escribir código de diseñar software.
Un abrazo, y a seguir aprendiendo.
— Ambrosio
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario