El lenguaje ubicuo no es un glosario: por qué tu modelo contable habla el idioma equivocado


Los problemas difíciles del software son, en el fondo, problemas de significado. No de rendimiento, no de escalabilidad, no de qué framework usar. De significado. Qué quiere decir este concepto, dónde terminan sus límites, qué relaciones mantiene con otros conceptos. La filosofía del lenguaje lleva más de un siglo trabajando en esto — Frege distinguió entre sentido y referencia precisamente porque un mismo objeto puede nombrarse de formas que implican comprensiones radicalmente distintas. En software, esa distinción se paga con bugs.

El nombre que le ponemos a las cosas importa

Hay una tentación permanente en los equipos de desarrollo: tratar el lenguaje como un detalle cosmético. Ponemos nombres genéricos, usamos abreviaciones, mapeamos conceptos del dominio a estructuras técnicas sin preguntarnos si la traducción preserva el significado. Pero nombrar no es etiquetar — es constituir. El nombre que le das a una clase determina cómo piensas sobre ella, qué operaciones consideras válidas, qué invariantes esperas que cumpla.

Eric Evans lo dijo hace más de veinte años, y conviene repetirlo. El lenguaje ubicuo no es un glosario. No es un documento que vive en Confluence y que nadie lee. Es la forma en la que el equipo piensa, discute y modela el problema. En términos filosóficos, es el medio a través del cual el dominio se hace inteligible. Wittgenstein tenía razón: los límites de tu lenguaje son los límites de tu mundo. Si tu vocabulario de dominio es pobre, tu modelo será pobre — no porque te falte talento técnico, sino porque no tienes las palabras para pensar con precisión.

Cuando el código dice una cosa y el negocio dice otra, el bug no está en el código. Está en la conversación que nunca tuvieron.

Cuando un desarrollador escribe Transaction y el contador dice “asiento contable”, no están hablando de lo mismo. No es un problema de traducción — es que cada término carga un sentido distinto, una red de implicaciones diferente. Transaction evoca algo genérico, intercambiable, sin reglas propias. “Asiento contable” evoca partida doble, balance, una invariante que no se negocia. Esa fractura se propaga: a los tests, a la documentación, a las conversaciones en los pull requests. El equipo termina manteniendo dos modelos mentales en paralelo, y ninguno es el correcto.

Un caso concreto: el asiento contable

En un sistema contable, el concepto de “asiento” no es trivial. Un asiento no es un registro. No es una fila en una tabla. Es una operación atómica que debe cumplir invariantes estrictas: la suma de los débitos debe igualar la suma de los créditos. Siempre. Sin excepciones.

Si tu modelo tiene una clase Transaction con un campo amount: float y un type: str que puede ser "debit" o "credit", el problema ya está instalado. Ese modelo no expresa la invariante. No la protege. El dinero es un primitivo que no sabe su moneda, el tipo es un string que no impone ninguna regla, y la responsabilidad de mantener el balance recae en el código que usa la clase, no en la clase misma. Y aquí aparece una distinción que la filosofía conoce bien —la diferencia entre verdad contingente y verdad necesaria—. Que un asiento esté balanceado no es algo que puede pasar — es algo que tiene que pasar para que el concepto mismo de “asiento” tenga sentido. Es una condición de verdad constitutiva, no una validación opcional.

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def plus(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise IncompatibleCurrencyError(self.currency, other.currency)
        return Money(self.amount + other.amount, self.currency)

    def is_zero(self) -> bool:
        return self.amount == Decimal("0")

    @classmethod
    def zero(cls, currency: str) -> "Money":
        return cls(Decimal("0"), currency)


class JournalEntry:
    def __init__(self, lines: list[EntryLine]) -> None:
        balance = reduce(
            lambda total, line: total.plus(line.amount),
            lines,
            Money.zero(lines[0].amount.currency),
        )
        if not balance.is_zero():
            raise UnbalancedEntryError(balance)

        self._lines = tuple(lines)

Observa lo que cambió respecto al modelo plano. No es solo el lenguaje. Es el diseño. Money no es un float — es un Value Object inmutable que sabe su moneda y se niega a sumar euros con dólares. No hay type == "debit" como string. Cada EntryLine lleva un amount con signo, y la invariante se expresa como lo que es — la suma debe ser cero. El nombre JournalEntry no es una etiqueta — es una declaración de intención. La invariante vive dentro del __init__. No hay forma de crear un asiento desbalanceado. El modelo se protege a sí mismo. En términos de Kripke, JournalEntry funciona como un designador rígido: no importa en qué contexto lo uses, refiere siempre al mismo concepto con las mismas condiciones de identidad. Transaction, en cambio, es un término vago que cambia de referencia según quién lo lea.

El glosario no alcanza

Un glosario te dice que “asiento” se traduce como “journal entry”. El lenguaje ubicuo te dice que un JournalEntry es un agregado con invariantes, que Money es un Value Object inmutable que impone reglas de moneda y precisión, que cada EntryLine lleva un amount con signo, y que UnbalancedEntryError es una excepción de dominio que señala una violación de las reglas del negocio.

La diferencia es la misma que hay entre saber el nombre de algo y entender cómo funciona. Feynman lo explicaba con los pájaros: puedes saber el nombre del pájaro en todos los idiomas del mundo y no saber absolutamente nada del pájaro.

Cómo se construye el lenguaje ubicuo

No se decreta. No se escribe en un documento y se distribuye. Se construye en conversación, iterando, peleando por los nombres correctos.

  • Escucha al dominio. Cuando un contador dice “asiento”, no lo traduzcas a “transaction”. Pregúntale qué reglas tiene, qué puede salir mal, qué pasa cuando se viola.
  • Haz que el código hable. Si tienes que explicar qué hace una clase, el nombre está mal. JournalEntry no necesita explicación. Transaction necesita un párrafo.
  • Pelea por los nombres. Cada vez que alguien dice “es lo mismo”, desconfía. En DDD, los sinónimos son sospechosos. Casi siempre esconden un concepto diferente o un bounded context que no viste. Wittgenstein hablaba de “juegos de lenguaje”: contextos donde las mismas palabras obedecen reglas distintas. Un bounded context ES un juego de lenguaje — y “cliente” en ventas y “cliente” en soporte no juegan el mismo juego.
  • Refactoriza el lenguaje antes que el código. Si el equipo no puede explicar el modelo en una frase, el modelo está mal. No importa si los tests pasan.

La deuda semántica es la más cara

Puedes refactorizar código. Puedes mejorar la arquitectura. Pero cuando el equipo entero entiende mal un concepto del dominio, no hay refactoring que lo arregle sin volver a las conversaciones fundamentales.

La próxima vez que alguien te diga “es un detalle de naming”, recuérdale que el lenguaje no describe el dominio — lo constituye. No es una capa de presentación sobre una realidad que existe independientemente de cómo la nombramos. El modelo ES el lenguaje. Y si el lenguaje miente, todo lo que construyas encima va a ser mentira también.

Los filósofos del lenguaje llevan un siglo debatiendo cómo las palabras se conectan con el mundo. Los arquitectos de software deberían prestarles más atención. Porque cada vez que nombras un agregado, defines un bounded context o declaras una invariante, estás haciendo ontología — te des cuenta o no.

Armando Zarate

Software craftsman. Escribe sobre Domain-Driven Design, resolución de problemas complejos y el poder del lenguaje en el desarrollo de software.