Diseño de software: cuando la base de datos secuestra el dominio

Introducción

Durante mucho tiempo, en distintos equipos de desarrollo, he visto repetir el mismo patrón: comenzar un nuevo proyecto generando primero las tablas, pensando en los esquemas, los tipos, los constraints… y luego construir la lógica por encima de esa base de datos. Este enfoque parece natural al principio, sobre todo si vienes de frameworks que promueven el CRUD, pero tiene un coste oculto: el dominio deja de reflejar el negocio y empieza a obedecer a la tecnología.

Database first vs domain first

En este artículo quiero reflexionar sobre los peligros del enfoque database-first, cómo lo he sufrido en proyectos reales, y por qué cambiar este enfoque me ayudó a recuperar la claridad del software y el control del diseño.


1. El atajo que sale caro

El diseño database-first parece un atajo: creas tus tablas, generas modelos automáticamente con un ORM, y ya puedes empezar a «programar». Pero este camino te lleva directo a:

  • Entidades acopladas a la persistencia.
  • Dominio pobre, limitado a getters/setters.
  • Dificultades para representar lógica compleja del negocio.
  • Un modelo de datos que dicta qué puedes o no puedes hacer.

Lo viví en un proyecto donde todo el sistema giraba en torno a una base de datos monolítica. Cada cambio en una tabla requería cascadas de cambios en todo el sistema. El modelo no hablaba el lenguaje del negocio, hablaba SQL.

En un sistema de ecommerce que comenzamos con un enfoque puramente basado en la base de datos, teníamos una tabla orders con columnas como status, is_paid, shipped_at, cancelled_reason, y varios flags booleanos más. Cada nuevo estado requería otra columna, porque no habíamos modelado el ciclo de vida del pedido desde el dominio.

Cuando llegó la necesidad de permitir devoluciones parciales o reintentos de pago, el modelo ya no daba más de sí. Los campos se contradecían entre sí y los desarrolladores empezaban a escribir lógica del tipo: “si is_paid es true pero shipped_at es null y cancelled_reason no es null… entonces probablemente el pedido fue reintentado”. Nadie confiaba en esos datos y menos aún en la lógica.


2. CRUD no es negocio

Frameworks como Laravel, Rails o Spring nos han acostumbrado a pensar que si tenemos create, read, update, delete, ya tenemos un sistema. Pero esto es falso.

El negocio rara vez se reduce a operar sobre registros. Hay validaciones, reglas, restricciones, condiciones, temporalidades… Todo eso queda fuera del modelo cuando seguimos una mentalidad CRUD. Y entonces empieza el caos: validaciones duplicadas, lógica en controladores, reglas perdidas entre servicios.


3. Volver al dominio: diseñar desde el lenguaje

Una alternativa mucho más sana es empezar desde el dominio. Esto implica:

  • Hablar con personas de negocio.
  • Nombrar las cosas como las nombra la empresa.
  • Crear entidades que representen comportamientos reales, no solo estructuras de datos.
  • Aislar la persistencia: que sea un detalle de implementación.

Un buen modelo de dominio debe contar una historia que cualquier persona del negocio pueda entender al leer el código. No tiene por qué ser complejo; tiene que ser claro, coherente y expresivo:

class Order {
  constructor(private readonly items: OrderItem[], private readonly customer: Customer) {}

  confirm(): ConfirmedOrder {
    if (!this.customer.hasValidPaymentMethod()) {
      throw new DomainError('Cannot confirm order without a valid payment method.');
    }

    return new ConfirmedOrder(this);
  }
}

Esta clase expresa una intención clara: confirmar un pedido sólo si el cliente tiene un método de pago válido. Y lo hace sin exponer cómo se guarda, si se genera un email, o si hay un trigger en la base de datos. Se enfoca en el comportamiento, no en la infraestructura.

Esto es mucho más expresivo y mantenible que tener una tabla orders con una flag is_confirmed y confiar en que todos los servicios respeten esa flag. En lugar de depender de convenciones implícitas, el modelo guía el flujo del sistema.


4. DTOs, mapeos y claridad

Una queja habitual cuando propones esto es: «pero entonces hay que mapear entre entidades y modelos del ORM». Sí. Y esa es una gran ventaja.

Ese mapeo es el lugar perfecto para:

  • Controlar qué se guarda y qué no.
  • Separar lógica de negocio de detalles técnicos.
  • Hacer explícitas las dependencias.

La base de datos deja de dictar el modelo. Entonces, es el modelo quien manda. La capa de persistencia y el dominio no tienen que compartir los mismos campos de forma obligatoria.

Por ejemplo, puede que tu entidad de dominio Order tenga un TotalAmount como Value Object que encapsula impuestos, descuentos y moneda, mientras que la tabla orders en la base de datos tenga columnas separadas como subtotal, tax, discount, y currency. El mapeo entre uno y otro te permite adaptar sin sacrificar expresividad ni integridad en el dominio.

Además, ese punto de transformación actúa como una frontera clara entre capas: puedes validar, limpiar o enriquecer datos en un solo lugar. Puedes tener DTOs de entrada (por ejemplo, para exponer en APIs) que no reflejan directamente el estado interno del dominio. Y DTOs de salida que extraen solo lo necesario.

En el sistema de pedidos, refactorizamos un bloque que armaba manualmente la respuesta JSON para mostrar el pedido del cliente. Antes, accedía directamente al modelo de base de datos, con lógica como order.total = subtotal + tax - discount. Ahora, usamos OrderDTO.fromDomain(order), lo que nos permite extraer la lógica de presentación sin contaminar ni el dominio ni la infraestructura. Además, ese DTO es diferente al que se guarda en base de datos, porque no necesita contener campos como internalNotes o paymentAttempts.


Conclusión

📝 Nota: Este enfoque cobra verdadero sentido en proyectos donde hay complejidad en la lógica de negocio, múltiples reglas, validaciones, estados o flujos.

En sistemas simples —como backoffices administrativos o CRUDs de soporte— muchas veces un enfoque más directo puede ser suficiente y más eficiente. El objetivo no es aplicar principios por dogma, sino como herramientas al servicio del contexto. Saber cuándo no usarlos también es parte del buen diseño.

Empezar por la base de datos es como construir una historia desde los pies del protagonista. Puedes hacerlo, pero perderás el alma del relato. En cambio, diseñar desde el dominio te permite crear software que refleja el negocio, evoluciona con él y evita muchos de los dolores típicos de los sistemas legacy.

No siempre puedes cambiarlo todo de golpe. Pero puedes empezar hoy a hacer preguntas distintas:

  • ¿Esto lo estoy haciendo así porque lo necesita el negocio o porque lo dicta la tabla?
  • ¿Este modelo habla el lenguaje del cliente o el del ORM?
  • ¿Podría expresar esta lógica sin pensar en la base de datos?

Si la respuesta te incomoda, estás en el camino correcto.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *