El Problema
Construir un ERP desde cero en 2025 es una decisión audaz. La mayoría de los equipos optan por plataformas existentes — y con razón. Pero cuando tu mercado tiene requisitos de cumplimiento fiscal muy específicos (como el sistema de facturación electrónica DTE de El Salvador), y quieres workflows IA-first desde el día uno, construir te da una precisión que comprar no puede darte.
Esta es la historia de Orbis 8, y lo que aprendí de 6 meses diseñando su arquitectura.
¿Por qué Arquitectura Limpia?
He trabajado en codebases que empezaron siendo "pragmáticos" — llamadas directas a la base de datos en los controladores, lógica de negocio en componentes React, sin separación clara. Funcionan. Hasta que dejan de funcionar.
El momento en que necesitas:
- Cambiar tu ORM
- Agregar una cola de mensajes
- Escribir un test unitario que no requiera una base de datos en ejecución
...te das cuenta de que te has pintado en una esquina.
La Arquitectura Limpia resuelve esto haciendo que las dependencias apunten hacia adentro. Tu lógica de negocio no sabe nada sobre FastAPI, SQLAlchemy ni PostgreSQL. Solo define lo que necesita hacer.
# ✅ Entidad de dominio — Python puro, cero dependencias
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class Factura:
id: str
total: Decimal
tasa_impuesto: Decimal = Decimal("0.13") # IVA 13% El Salvador
@property
def monto_impuesto(self) -> Decimal:
return (self.total * self.tasa_impuesto).quantize(Decimal("0.0001"))
@property
def subtotal(self) -> Decimal:
return self.total - self.monto_impuesto
El Patrón Repository en la Práctica
La interfaz en el dominio, la implementación en la capa de infraestructura:
# domain/interfaces/factura_repository.py
from abc import ABC, abstractmethod
from typing import Optional
from domain.entities.factura import Factura
class IFacturaRepository(ABC):
@abstractmethod
async def buscar_por_id(self, factura_id: str) -> Optional[Factura]:
pass
@abstractmethod
async def guardar(self, factura: Factura) -> Factura:
pass
Esto significa que la capa de servicio puede probarse sin una base de datos. Intercambio el repositorio real por un mock en los tests. Esto no es teórico — nos salvó varias veces cuando el esquema de la base de datos estaba cambiando rápidamente.
Lo que Haría Diferente
1. Empezar con el modelo de eventos
Agregamos Pub/Sub más tarde. Añadir comunicación asíncrona a un sistema síncrono es mucho más difícil que diseñarlo desde el principio. Define tus eventos de dominio el día 1, incluso si los implementas después.
2. MDX para la documentación
Usamos Confluence. No deberíamos. Cada decisión arquitectónica que vivió en Confluence murió cuando la persona que la escribió se fue. Mantén las decisiones cerca del código, en archivos .md en el repositorio.
3. Schemas de Postgres para multi-tenancy, no bases de datos separadas
Usamos aislamiento a nivel de schema (access, billing, inventory schemas). Es más sencillo de gestionar que base de datos por tenant y aún proporciona un aislamiento lógico fuerte.
Conclusión
La Arquitectura Limpia requiere inversión inicial. El retorno es un codebase donde:
- Los desarrolladores senior se incorporan en horas, no días
- Los agentes de IA (como Claude) pueden razonar sobre la estructura del codebase de forma fiable
- Las features pueden añadirse sin tocar módulos no relacionados
La arquitectura sirve al equipo. Cuando el equipo crece, la arquitectura debe doblarse con gracia — no romperse.
Próximo post: Arquitectura Hexagonal en Python — cuándo usarla y cuándo es excesiva.