Construir para Escalar Desde el Principio: Lecciones de Arquitectura Temprana
Las decisiones de arquitectura tomadas en los primeros días de un proyecto tienen un efecto desproporcionado en lo que se puede construir años después. Las interfaces solidificadas, los modelos de datos con suposiciones incorrectas incorporadas y los límites de servicio trazados por conveniencia en lugar de por coherencia del dominio se acumulan hasta convertirse en restricciones que dan forma a cada función sucesiva.
Este artículo examina tres decisiones de arquitectura temprana que resultaron decisivas en un proyecto de SaaS en el que hemos trabajado durante varios años. Ninguna de ellas fue obvia en el momento. Todas tienen una deuda correspondiente que sigue siendo costosa.
Primera Decisión: Separación de Servicios
El proyecto comenzó como un monolito, lo cual es correcto. Hay mucho que se puede aprender sobre los límites del dominio antes de comprometerse con ellos. Pero había una pregunta que surgió temprano: ¿cuándo separar el procesamiento de datos del proceso de la API web?
Esperamos demasiado. La lógica de procesamiento de datos creció dentro del proceso de la API web durante dieciocho meses antes de que la separáramos. El costo fue real: las rutas de código de larga duración competían con las solicitudes de la API por la capacidad del proceso, el comportamiento de escala era impredecible, y la extracción requirió un cuidadoso trabajo de refactorización que interrumpió el desarrollo de funciones durante varias semanas.
La regla de oro que aplicamos en retrospectiva: cuando el trabajo de backend tiene tiempos de ejecución significativamente diferentes —solicitudes de API que deben completarse en milisegundos frente a tareas de procesamiento que se ejecutan durante segundos o minutos— esos son dos servicios, incluso si comparten una base de código o se ejecutan en el mismo proceso al principio. La interfaz que los separa —una cola de tareas, un bus de eventos, incluso solo una función bien nombrada con una firma estable— es lo que hace posible la separación eventual sin reescritura.
Segunda Decisión: Modelado de Datos
El modelo de datos más costoso que cometimos fue tratar las relaciones como atributos. Una entidad tenía múltiples tipos de propietario en el dominio del negocio: una organización, un usuario individual y, en algunos casos, un socio externo. La implementación inicial codificó esto como tres campos de clave foránea opcionales en la tabla central.
Este diseño refleja bien cómo se pensaba el dominio en ese momento. Resultó ser incorrecto de dos maneras. Primero, el número de tipos de propietario creció: cuatro, luego cinco. Cada adición requería una migración de esquema, actualizaciones en la lógica de autorización, nuevos casos en el ORM y nuevas ramas en las consultas de visualización. Segundo, los controles de autorización sobre quién tenía acceso a qué se volvieron difíciles de razonar porque la pregunta "¿quién es dueño de este registro?" requería verificar múltiples campos posibles.
Una tabla polimórfica de propiedad —(entity_id, entity_type, owner_id, owner_type)— habría admitido tanto propietarios adicionales como múltiples propietarios simultáneos sin cambios de esquema. Introducir ese modelo un año después, con datos de producción existentes que requerían migración, fue el trabajo de refactorización más caro del proyecto.
Tercera Decisión: Interfaces de API
La decisión de API que cometemos con más frecuencia es exponer la estructura de datos de la base de datos directamente como respuesta de la API. Es rápido de construir. Los primeros clientes simplemente reciben lo que está en la base de datos. El problema aparece cuando la base de datos necesita cambiar.
Cuando un campo en la base de datos necesita ser renombrado, dividido en dos campos, o eliminado completamente, todos los clientes que hacen referencia a ese campo se ven afectados. Si el cliente API es también de nuestra propiedad, el costo es el trabajo de coordinación y los cambios simultáneos. Si el cliente es de terceros, el costo es la coordinación de versiones de API y el mantenimiento de compatibilidad hacia atrás indefinidamente.
La capa de transformación entre el almacenamiento y la respuesta de la API protege contra este acoplamiento. No tiene que ser compleja —una función que toma un registro de base de datos y devuelve una forma de respuesta— pero debe existir. La disciplina de mantenerlo separado es lo que da la libertad de cambiar el almacenamiento subyacente sin negociar cambios de cliente.
El Patrón Subyacente
Las tres decisiones tienen algo en común: la implementación rápida fue fácil de construir inicialmente, y el costo llegó más tarde cuando las suposiciones incorporadas resultaron ser incorrectas. Esto es cierto para la mayoría de las decisiones de arquitectura. La pregunta no es cómo evitar todas las suposiciones incorrectas —eso es imposible— sino cómo estructurar el código de manera que las suposiciones incorrectas se puedan corregir sin reescribir el sistema.
Las interfaces son la principal herramienta para esto. Un límite bien definido entre componentes —ya sea una interfaz de servicio, un contrato de API o una transformación de capa de datos— es lo que hace que el comportamiento sea reemplazable localmente. Sin ese límite, corregir una suposición incorrecta requiere rastrear hacia atrás a través de todo lo que depende de ella.
Aplicación Práctica
Las lecciones no se resumen en "diseña todo correctamente por adelantado." Se resumen en: para cada componente que estés construyendo, identifica cuál es la suposición más costosa de equivocarse, y luego construye la interfaz que hace que esa suposición sea reemplazable.
Para la separación de servicios, la suposición más costosa es sobre las características de tiempo de ejecución. La interfaz que la hace reemplazable es una cola de tareas o bus de eventos entre los componentes de trabajo síncrono y asíncrono.
Para el modelado de datos, la suposición más costosa es sobre la cardinalidad de las relaciones. La interfaz que la hace reemplazable es una tabla de unión con una clave bien definida, en lugar de claves foráneas opcionales en la tabla principal.
Para las API, la suposición más costosa es sobre la forma de los datos subyacentes. La interfaz que la hace reemplazable es una capa de transformación explícita entre almacenamiento y respuesta.
Estas no son prescripciones universales. Son las versiones de esas lecciones que hemos encontrado relevantes en el contexto específico de construir SaaS B2B con equipos pequeños. El patrón subyacente —identificar la suposición costosa, construir la interfaz que la hace reemplazable— es más generalizable.