Hace tres años, alguien en una reunión de producto dijo la frase que precede a la mayoría de los proyectos de plataforma accidentales: “solo necesitamos reutilizar los componentes de formulario entre productos”. Esa frase inició un esfuerzo arquitectónico de meses que tocó cuatro equipos de producto, redefinió los pipelines de despliegue y cambió nuestra manera de pensar sobre la infraestructura compartida en un entorno bancario regulado.
Esta es la versión honesta de lo que ocurrió.
El problema que realmente estábamos resolviendo
La iniciativa OneBank en Scotiabank CCAU buscaba ofrecer a los clientes una experiencia digital unificada para solicitar cualquiera de cuatro productos financieros: préstamos, tarjetas de crédito, cuentas de ahorro y seguros. Cada producto tenía su propio equipo, su propio ritmo de releases, sus propios requisitos regulatorios y su propia historia técnica. Lo que compartían era el cliente — y la obligación regulatoria de recopilar esencialmente la misma información de esencialmente la misma forma.
El problema superficial era la duplicación. Cada equipo había construido su propio flujo de onboarding:
Cuatro implementaciones independientes de la misma lógica de onboarding. Cada una con sus propios bugs, sus propias interpretaciones de compliance, su propia versión de “cómo hacemos la validación de fechas”. Esto no fue un fallo técnico — los equipos habían construido soluciones razonables a sus problemas inmediatos. El fallo fue organizacional: no existía ningún mecanismo para el aprendizaje compartido, las correcciones compartidas ni la gobernanza compartida.
El problema más profundo, el que realmente necesitaba solución, es que el onboarding bancario es un dominio regulatorio, no solo un problema de UI. Cada campo es potencialmente significativo desde el punto de vista legal. Las reglas de validación son requisitos de compliance, no sugerencias. El audit trail no es opcional. Cualquier plataforma construida para unificar esto no podía limitarse a compartir componentes — tenía que compartir restricciones.
Fase 1: Qué intentamos primero, y por qué falló
El primer paso natural fue una biblioteca de componentes compartida. Un <Button>, un <Input>, un <Modal>. Compartidos entre los cuatro productos. Esto es lo que todos los artículos sobre design systems recomiendan construir, y es correcto hasta cierto punto.
meses después:
| Producto | Versión de la biblioteca |
|---|---|
| Préstamos | 3.4.1 |
| Tarjetas | 2.8.0 |
| Cuentas | 4.1.0 |
| Seguros | 2.7.3 |
Cuatro productos, cuatro versiones distintas, ninguna actualizada. El equipo de Seguros estaba dos versiones mayores por detrás porque un cambio de la v3 rompió su orquestación de formularios personalizada. El equipo de Tarjetas había hecho cherry-pick de un fix de la v3.2 copiando el código fuente directamente en su codebase — lo que significaba que ya no recibían futuros fixes para ese componente.
Esto es normal. No he visto ninguna biblioteca de UI compartida a gran escala que no termine aquí. La razón no es deuda técnica ni pereza. Es que una biblioteca de UI compartida sin un modelo de validación compartido no resuelve el problema real. Los equipos seguían siendo responsables de su propia lógica de validación, su propio contexto regulatorio, su propia estrategia de auditoría. La biblioteca compartida reducía la duplicación visual, pero no tocaba la capa de compliance — que era donde vivía la mayor parte de la complejidad.
Habíamos construido la capa compartida incorrecta.
El punto de inflexión arquitectónico
La intuición que cambió el diseño vino de una sesión de debugging fallida. Un validador de compliance informó que los productos Préstamos y Tarjetas estaban interpretando el mismo campo regulatorio de manera diferente — uno lo trataba como obligatorio, el otro como condicional según el tipo de persona. Ambas interpretaciones eran defendibles. Ninguna era canónica.
La causa raíz: la lógica de validación vivía en cuatro lugares, mantenida por cuatro equipos, sin ningún schema compartido. No existía una única definición de cómo era una solicitud de onboarding válida. Había cuatro definiciones aproximadas, suficientemente alineadas para pasar el testing, suficientemente divergentes para generar preguntas de compliance.
La solución no eran más componentes. Era una capa de schema que existiera antes que los componentes.
Reestructuramos toda la biblioteca en tres capas:
P -->|"extends — solo capa visual"| Co
Co -->|orquesta| F
F --> L & C & A & I
L & C & A & I --> BFF
BFF --> API
La separación clave: la evolución visual y los contratos estructurales tienen diferentes tasas de cambio, y diferente impacto downstream. Acoplarlos en un único paquete obliga a los consumidores a tratar cada actualización de diseño como un potencial breaking change. Separarlos permite que el design system evolucione sin generar trabajo de migración en los equipos de producto.
La capa @onebank/forms es donde vive la arquitectura más significativa. Es propietaria del schema de validación, el resolver regulatorio y el modelo de audit log. No tiene dependencia de React — es importable en Node.js, testeable sin navegador, y versionable de forma independiente de las capas UI superiores.
Orquestación de formularios dirigida por schemas
El núcleo de la capa de forms es SolicitanteSchema, un schema Zod usando z.discriminatedUnion sobre tipoPersona:
const SolicitanteSchema = z.discriminatedUnion('tipoPersona', [
z.object({
tipoPersona: z.literal('NATURAL'),
rut: RutSchema,
nombre: z.string().min(2).max(100),
apellidoPaterno: z.string().min(2).max(100),
fechaNacimiento: FechaNacimientoSchema,
// campos de persona natural
}),
z.object({
tipoPersona: z.literal('JURIDICA'),
rutEmpresa: RutSchema,
razonSocial: z.string().min(2).max(200),
representanteLegal: RepresentanteLegalSchema,
// campos de persona jurídica
}),
]);
TypeScript estrecha el tipo automáticamente en ramas condicionales. Un componente que renderiza fechaNacimiento solo se ejecuta cuando tipoPersona === 'NATURAL' — y TypeScript lo hace cumplir sin checks en runtime. Esto eliminó una clase entera de bugs: campos renderizados para el tipo de persona incorrecto.
La función de configuración regulatoria fue una decisión arquitectónica más significativa:
function resolverConfigFormulario(
estado: EstadoFormulario,
contextoRegulatoria: ContextoRegulatoria
): ConfigFormulario {
// Función pura — sin hooks, sin efectos secundarios, sin React
const esEmpleadoDependiente = estado.tipoPersona === 'NATURAL'
&& estado.tipoEmpleo === 'DEPENDIENTE';
return {
campos: {
liquidacionSueldo: {
requerido: esEmpleadoDependiente && contextoRegulatoria.productoRequiereComprobante,
visible: esEmpleadoDependiente,
},
// ... otras configuraciones de campos
},
};
}
Es una función pura. No sabe nada de React. No tiene efectos secundarios. Recibe el estado actual del formulario y el contexto regulatorio y devuelve un objeto de configuración. Las reglas de negocio son testeables en completo aislamiento:
it('requiere comprobante de sueldo para empleado dependiente que solicita préstamos', () => {
const config = resolverConfigFormulario(
{ tipoPersona: 'NATURAL', tipoEmpleo: 'DEPENDIENTE' },
{ productoRequiereComprobante: true }
);
expect(config.campos.liquidacionSueldo.requerido).toBe(true);
});
Sin mocks, sin DOM, sin React Testing Library. El test corre en menos de 2 segundos. Este era el prerrequisito para un testing de compliance rápido — antes de esta arquitectura, testear reglas regulatorias requería levantar un entorno completo de navegador.
La capa BFF: complejidad oculta
El flujo de onboarding involucra mucho más que un formulario y un botón de enviar. Detrás del BFF:
U->>App: envía formulario de onboarding
App->>Forms: resolveFormConfig(estado, contextoReg)
Forms-->>App: FormConfig (validada, restricciones por campo)
App->>Forms: SolicitanteSchema.parse(formData)
Forms-->>App: datos validados o ZodError
App->>BFF: POST /onboarding/productCode
BFF->>Auth: validateToken + extractClaims
Auth-->>BFF: claims (userId, sessionId)
BFF->>BFF: rateLimitCheck + fraudSignalAppend
BFF->>CB: POST /v2/applications
CB-->>BFF: applicationId + estado PENDIENTE
BFF-->>App: applicationId + nextStep
App->>Forms: logEvento(EventoFormulario)
Forms->>AuditLog: persist(evento, versionFormulario)
AuditLog-->>Forms: confirmado
Subestimamos la complejidad del BFF de manera significativa. El gateway Node.js necesitó manejar:
Varianza en autenticación: Distintos productos tenían distintos modelos de sesión. El equipo de Préstamos había implementado su propia validación JWT; el equipo de Tarjetas usaba un auth service compartido. Normalizar esto en el límite del BFF tomó más tiempo que toda la capa @onebank/primitives.
Rate limiting en el límite de la aplicación: Core Banking tenía rate limits que no documentamos completamente hasta que los alcanzamos en staging. El BFF necesitó implementar rate limiting por token bucket por cliente y por producto — no rate limiting global, que habría permitido que un spike en un producto degradara los demás.
Complejidad de orquestación de requests: Algunos productos requerían llamadas de pre-validación a servicios externos (consultas de crédito DICOM, verificaciones regulatorias CMF) antes de enviar a Core Banking. Estas no podían hacerse en el cliente — requerían coordinación server-side. El BFF se convirtió en la capa de orquestación para las cadenas de validación previas al envío.
La lección: la complejidad del BFF crece de forma no lineal con el número de dependencias upstream. Habíamos definido el alcance del BFF como “solo autenticación y enrutamiento”. Para cuando estuvo estable en producción, también manejaba circuit breaking, agregación de requests, normalización de errores y lógica de reintentos downstream.
El contexto regulatorio como preocupación arquitectónica de primer nivel
La regulación financiera chilena (SBIF, ahora CMF tras la fusión de 2019) impone requisitos específicos sobre qué datos deben recopilarse, en qué orden, con qué validación. Estos requisitos varían según la categoría de producto, el segmento de cliente y los umbrales del monto solicitado.
Cometimos un error al principio: tratar los requisitos regulatorios como datos de configuración que los equipos de producto gestionarían. En la práctica, la configuración regulatoria requiere revisión legal, no solo configuración del developer. La distinción importa arquitectónicamente — significa que el contexto regulatorio no puede editarse libremente en una app de producto sin un proceso de revisión.
El objeto ContextoRegulatoria en la capa de forms se convirtió en un tipo sellado — construido solo por un servicio de configuración regulatoria con un flujo de revisión explícito, no por código arbitrario de producto. Esta restricción la añadimos después de descubrir que un error de configuración había causado requisitos incorrectos de campos en un entorno de staging.
// No exportado — construido solo por RegulatoryConfigService
type ContextoRegulatoria = {
readonly productoRequiereComprobante: boolean;
readonly montoRequiereDocumentacionAdicional: boolean;
readonly segmentoRequiereVerificacion: SegmentoVerificacion;
readonly _brand: 'RegulatoryConfigService'; // nominal type brand
};
El brand impide la construcción arbitraria. Una app de producto no puede crear un ContextoRegulatoria — debe solicitarlo al servicio que posee la configuración regulatoria y su historial de revisión.
CI/CD: builds independientes por paquete
El monorepo usa Jenkins con pipelines de build independientes por paquete. Esta fue una decisión deliberada que tardó tres iteraciones en quedar bien.
Decisiones clave en este pipeline:
Semver basado en changesets: Cada PR que modifica un paquete debe incluir un archivo changeset declarando el tipo de cambio. Esto elimina la ambigüedad del “¿quién bumpeó esta versión?” y genera changelogs precisos. La automatización es estricta: un PR sin changeset falla en CI si toca archivos fuente del paquete.
Opt-in explícito del consumer: Cuando un paquete publica, no dispara automáticamente actualizaciones en los consumers. Las apps consumer eligen cuándo actualizar. Esto parece que causaría deriva — y sí genera algo de lag — pero evita que un cambio en la biblioteca fuerce un release de emergencia en el sprint de un equipo de producto. Los equipos actualizan según su propio ritmo.
Gate de cobertura del 95% en lógica de negocio: El requisito de cobertura aplica específicamente a la lógica de negocio de @onebank/forms — resolverConfigFormulario y los validadores de reglas. No aplica globalmente. Los requisitos globales de cobertura crean incentivos perversos para escribir tests triviales para código de UI y omitir tests complejos para lógica de negocio. Hacemos el gate específico para la capa que más importa.
El vacío de observabilidad que no vimos venir
Seis meses después del lanzamiento en producción, una auditoría de compliance preguntó: “¿Con qué frecuencia un cliente comienza la solicitud de préstamo y no la completa, y en qué paso?” No podíamos responder esa pregunta. Teníamos tasas de envío de solicitudes. No teníamos datos de abandono por paso.
El modelo de audit log (EventoFormulario) existía desde el principio — pero fue diseñado alrededor de solicitudes enviadas, no de sesiones en progreso. Retrofitamos el tracking de eventos por paso nueve meses después del lanzamiento, lo que requirió una migración de schema y un esfuerzo de coordinación entre los cuatro equipos de producto.
La lección dura: los requisitos de observabilidad para software regulado no son opcionales y no son lo mismo que los requisitos de monitoreo de aplicaciones. El monitoreo de aplicaciones te dice cuándo las cosas fallan. La observabilidad regulatoria te dice qué pasó antes, durante y después de cada interacción del cliente — y debe retenerse en una forma reconstruible bajo demanda.
Deberíamos haber diseñado el schema de EventoFormulario con requisitos de observabilidad regulatoria desde el principio, no con requisitos generales de logging. La distinción cambia qué registras, cómo lo almacenas, cuánto tiempo lo retienes y quién puede consultarlo.
La experiencia del developer como métrica arquitectónica
Uno de los indicadores más claros de que una arquitectura está sana: ¿cuánto tarda un engineer nuevo en hacer su primer cambio real?
Cuando empezamos la plataforma, la respuesta era aproximadamente tres semanas. Un developer nuevo en un equipo de producto necesitaba entender la implementación de formularios existente, el enfoque de validación, el contrato de API del BFF, el pipeline de despliegue y la infraestructura de testing. Nada de esto era consistente entre equipos.
Después de que la arquitectura de 3 capas se estabilizó, establecimos un benchmark específico: un developer nuevo debería ser capaz de producir una página de formulario compliant, on-brand y correctamente validada en cuatro horas. Este número forzó decisiones que de otro modo no habríamos tomado:
-
El paquete
@onebank/formsnecesitaba un comando CLI de scaffold (npx create-onebank-form) que generara un formulario correctamente estructurado con las importaciones de schema correctas, la llamada al resolver correcta y el hookup del audit log correcto. Sin el scaffold, los developers nuevos pasaban horas deduciendo el patrón correcto leyendo código existente. -
El storybook necesitaba documentación completa de cada estado de formulario — no solo el happy path, sino los casos edge regulatorios, los estados de error y los estados de carga. Los developers no pueden implementar algo que no pueden ver.
-
Los errores de TypeScript necesitaban ser accionables. Un error genérico de “Property ‘X’ does not exist” en una unión discriminada no es útil. Añadimos mensajes de error personalizados para los patrones de uso incorrecto más comunes.
El benchmark de cuatro horas también expuso un gap que no habíamos anticipado: la documentación del BFF era interna al equipo de plataforma. Los developers de equipos de producto no podían descubrir fácilmente el contrato de API para el endpoint de onboarding de su producto. Añadimos una spec OpenAPI generada desde los tipos TypeScript del BFF — que también se convirtió en la fuente de verdad para la generación de mock servers en tests de integración.
Qué hicimos mal
El schema Postgres compartido: En los primeros seis meses, los eventos de auditoría de los cuatro productos escribían a una tabla compartida con un discriminador productCode. Esto estuvo bien inicialmente y se convirtió en un problema cuando el equipo de Seguros necesitó un schema de auditoría significativamente diferente para su contexto regulatorio. Migramos a schemas de auditoría por producto en el mes ocho — antes habría sido más sencillo.
La estrategia de codemods: Cuando introdujimos breaking changes en @onebank/primitives, prometimos codemods automatizados que manejarían el 80% del trabajo de migración. La cifra del 80% era precisa. Pero subestimamos el esfuerzo requerido para el 20% restante — que involucraba casos donde los equipos habían extendido las APIs de primitivos de formas no estándar. Documenta y haz cumplir los puntos de extensión pronto; hacer codemods retroactivos para patrones de extensión no documentados es significativamente más difícil.
La propiedad de la configuración regulatoria: Permitimos a los equipos de producto gestionar su propia configuración regulatoria durante los primeros ocho meses. Esto parecía empoderador y reducía los cuellos de botella del equipo de plataforma. También resultó en un incidente near-miss de compliance donde un equipo había configurado como opcional un campo que era legalmente requerido para su categoría de producto. El tipo sellado ContextoRegulatoria vino después de este incidente, no antes.
Omitir la revisión regulatoria en la definición de done: Cada historia que cambiaba el comportamiento de formularios necesitaba una revisión de compliance antes de ir a staging. No lo formalizamos en el pipeline de CI/CD — era un requisito verbal. Tres veces en el primer año, un cambio llegó a staging sin revisión de compliance porque el developer no sabía que el requisito aplicaba a su cambio. Añadimos un check de CI que detecta cambios de comportamiento de formularios y bloquea la promoción a staging hasta que se aplique un tag de compliance del responsable de compliance del equipo.
Lecciones para sistemas de onboarding enterprise
La capa de compliance es una preocupación arquitectónica de primer nivel, no una capa de configuración. En sectores regulados, las entidades que definen qué es válido no son las mismas que escriben el código. La arquitectura debe codificar esta separación explícitamente — de lo contrario, el compliance se convierte en “algo que los devs configuran” y la gobernanza en “algo que pasa en code review”.
El DX es una métrica de fiabilidad. La fricción en el desarrollo se compone en bugs en producción. Si un developer tiene que leer tres componentes de ejemplo distintos para entender cómo añadir un campo de formulario, a veces se equivocará. El benchmark de cuatro horas fue una fuerza impulsora para la claridad.
La gobernanza de versiones es tan importante como la gestión de versiones. El semver le dice a los consumers qué cambió. La gobernanza le dice a los consumers qué se les permite cambiar y cuándo. Una biblioteca compartida sin decisiones explícitas de gobernanza — quién puede aprobar breaking changes, qué tiempo de aviso se requiere, cómo se apoyan las migraciones de consumers — derivará hacia el caos.
Los requisitos de observabilidad en sistemas regulados no son opcionales y no son lo mismo que el monitoreo general. Diseña tu modelo de auditoría antes de diseñar tu UI. Los requisitos regulatorios suelen ser más claros que los requisitos de UX, y restringen el sistema más.
Las plataformas compartidas requieren estructuras de ownership compartidas. Los cuatro equipos de producto que consumían los paquetes @onebank tenían representantes en el proceso de revisión de diseño de plataforma. Esto no era opcional — era la forma en que detectábamos problemas antes de que se convirtieran en incidentes de producción. El documento de arquitectura que nadie revisó es el que lamentarás.
Tres años después, la plataforma soporta cuatro productos y un quinto en desarrollo. La tasa de automatización de migración supera el 80%, el benchmark de onboarding se mantiene consistentemente bajo las cuatro horas, y las preguntas de auditoría de compliance se responden con consultas de log en lugar de investigaciones de ingeniería. La arquitectura que hace posible esto no es ingeniosa — es explícita sobre lo que posee, clara sobre sus límites y honesta sobre sus restricciones.
Eso es generalmente lo que parece una buena arquitectura enterprise.