Arquitectura de un SaaS de pedidos: capas, contratos y decisiones
Dambert Muñoz
Staff iOS Architect & AI Architect
Una arquitectura de software tiene que explicar qué cambia junto y qué conviene mantener separado. Para verlo, usemos un SaaS de pedidos de minimarket. Es un ejemplo didáctico: no describe una implementación desplegada ni resultados de un cliente. El objetivo es que puedas leer una decisión, discutir sus límites y construir una prueba que la sostenga.
El recorrido parece corto: alguien elige productos, confirma un pedido y el negocio prepara la entrega. Pero aparecen preguntas que una pantalla por sí sola no resuelve. ¿Qué precio se usa? ¿Quién reserva la última unidad? ¿Qué ocurre si el comprador reintenta porque no recibió respuesta? Esas preguntas definen contratos y reglas antes de definir carpetas.
1. Empieza por las reglas que no pueden romperse
La primera versión necesita un alcance pequeño. Un pedido tiene al menos una línea, la cantidad debe ser un entero positivo y el precio viene del catálogo del negocio. El navegador puede mostrar un total orientativo, pero la confirmación tiene que calcularlo con información confiable del servidor. Los importes se expresan en unidades mínimas de moneda para evitar mezclar reglas comerciales con aproximaciones de punto flotante.
- Un pedido vacío no puede confirmarse.
- Una cantidad de cero, negativa o fraccionaria debe rechazarse.
- El cliente no decide el precio que finalmente se cobra.
- Un reintento no debe crear una segunda venta para la misma intención.
- El stock y el estado del pedido deben mantener una relación consistente.
Todavía no necesitas un microservicio para cada regla. Necesitas escribirlas con suficiente precisión como para saber dónde se validan y cómo se prueban. También tienes que acordar las excepciones: vender por peso, permitir una sustitución o aceptar un pedido pendiente son decisiones de negocio que pueden cambiar el modelo.
2. Separa la decisión del mecanismo
El dominio define conceptos y validaciones. La aplicación coordina un caso de uso. La infraestructura conecta bases de datos y servicios externos. La presentación recibe una intención y muestra el resultado. Esta separación resulta útil cuando protege una regla del cambio de una biblioteca, una pantalla o un proveedor. Si añade interfaces sin proteger ningún cambio real, puede ser complejidad innecesaria.
type OrderLine = { productId: string; quantity: number };
export function validateLines(lines: OrderLine[]): void {
if (lines.length === 0) throw new Error("EMPTY_ORDER");
for (const line of lines) {
if (!line.productId.trim()) throw new Error("INVALID_PRODUCT");
if (!Number.isSafeInteger(line.quantity) || line.quantity <= 0) {
throw new Error("INVALID_QUANTITY");
}
}
}La función no conoce React, una petición HTTP ni una tabla SQL. Puede probarse sin levantar un servidor. Esa es la propiedad que buscamos: una regla importante se mantiene legible aunque cambie la forma de presentar o guardar un pedido. El transporte todavía tiene que comprobar los tipos de la entrada antes de llamar a esta función; TypeScript no valida un JSON recibido en tiempo de ejecución.
3. Diseña el contrato de confirmación
Un caso de uso recibe el identificador de la cuenta, las líneas y una clave de idempotencia. No recibe un total que deba confiar. Lee el catálogo disponible, calcula el total y solicita una confirmación atómica. El siguiente contrato es deliberadamente pequeño: especifica lo que la infraestructura tiene que garantizar, pero no implementa todavía la transacción.
type ConfirmInput = {
tenantId: string;
idempotencyKey: string;
lines: Array<{ productId: string; quantity: number }>;
};
type ConfirmedOrder = { id: string; totalMinor: number };
interface OrderConfirmation {
// Implementations must atomically validate stock and deduplicate retries.
confirm(input: ConfirmInput): Promise<ConfirmedOrder>;
}La palabra atómica es un compromiso, no una mejora que aparece por tener una interfaz. En una base relacional, la implementación podría usar una transacción y restricciones únicas. Tiene que revisar concurrencia, aislamiento y qué ocurre si falla la escritura. Si reserva stock en otro servicio, ya existe una frontera distinta y quizá haga falta compensación. Conviene resolver esa decisión antes de prometer que dos operaciones remotas equivalen a una transacción.
4. Prueba el recorrido, no solo los helpers
Las pruebas del dominio comprueban entradas y reglas. Las de integración comprueban que el contrato se cumple usando la persistencia real. Una prueba de UI puede completar un pedido, pero no demuestra por sí sola que dos confirmaciones simultáneas no sobrevendan stock. Cada nivel debe aportar evidencia que el anterior no podía obtener.
- Cantidades vacías, negativas, fraccionarias y fuera de rango.
- Precio alterado en el navegador: el servidor conserva el precio válido.
- Dos pedidos compiten por una unidad: solo uno puede confirmarse.
- La misma clave se reintenta y devuelve el mismo pedido.
- La escritura falla: no queda stock reservado sin el registro correspondiente.
- Una cuenta no puede confirmar o leer pedidos de otro negocio.
Incluye también el resultado observable: un error de disponibilidad debe permitir corregir el carrito sin perder el resto del pedido. Una respuesta lenta tiene que distinguirse de un fallo definitivo para que la persona no pulse confirmar varias veces sin contexto. La calidad de una decisión incluye cómo llega al usuario cuando el recorrido deja de ser ideal.
5. Escribe un ADR que permita discutir el coste
Un Architecture Decision Record no necesita varias páginas. Registra el contexto, la alternativa elegida, las opciones descartadas y las consecuencias. Para esta primera versión podrías escoger un monolito modular con una base relacional: permite confirmar pedido y stock en una frontera transaccional mientras el equipo aprende de la operación. Eso no significa que sea la elección correcta para cualquier catálogo o volumen.
- Contexto: un equipo pequeño, un canal de pedidos y stock compartido.
- Decisión: mantener pedido e inventario en una unidad transaccional.
- Alternativa: separar inventario en un servicio remoto desde el inicio.
- Coste aceptado: compartir una base y coordinar cambios del esquema.
- Señal para revisar: inventario necesita operar de forma independiente o tiene otra fuente de verdad.
Una decisión defendible también explica lo que aún no se sabe. El volumen real, los canales de venta, el proveedor de pagos y el coste de operar el sistema pueden cambiar la solución. No inventes una previsión de millones de usuarios para justificar una infraestructura que el producto todavía no necesita.
6. Lleva la arquitectura a un proyecto concreto
El siguiente paso es implementar una vertical completa: leer catálogo, confirmar pedido y mostrar el resultado. Documenta una decisión y conserva una prueba de la propiedad más importante. El proyecto demuestra más criterio si explica una limitación real que si enumera muchas tecnologías sin enseñar cómo trabajan juntas.
Puedes revisar las plantillas SaaS para Perú y LATAM y la ficha de minimarket para comparar una base con tu operación. Si quieres practicar el proceso acompañado, el curso de arquitectura de software está orientado a construir y defender decisiones. Para un problema de equipo, revisa la consultoría de arquitectura.
Escrito por
Dambert Muñoz
Staff iOS Architect & AI Architect
Arquitectura de software, productos móviles y formación práctica.