---
id: "ADR-002"
title: "Modelo multi-tenant con FX"
date: "2026-04-12"
status: "active"
superseded_by: ""
tags: ["multi-tenant", "fx", "dependency-injection", "flags", "function-types", "interfaces"]
summary: "En el contexto de White Label, una plataforma que corre para múltiples marcas
(Jumbo, Disco, Prezunic, Santa Isabel, entre otras) cada una con proveedores, flujos
y reglas distintas, frente a la necesidad de soportar esas diferencias sin condicionales
dispersos en runtime, decidimos usar un modelo de un binario por tenant donde el tenant
se inyecta como primer nodo del grafo FX y las variaciones se modelan con la jerarquía
flag booleano → function type → interface, para lograr que las diferencias entre marcas
se resuelvan en tiempo de construcción y el código de negocio sea agnóstico al tenant,
aceptando que agregar una nueva marca requiere configurar los flags y reglas correspondientes."
---

## Contexto

White Label corre para múltiples marcas de retail en distintos países: Jumbo (Chile,
Colombia, Argentina), Disco (Argentina), Prezunic (Brasil), Santa Isabel (Chile), entre
otras. Cada marca tiene particularidades: distintos proveedores de autenticación, distintos
flujos de validación, distintos mercados con regulaciones propias.

La pregunta de diseño es cómo modelar esas diferencias sin que el código se convierta en
una acumulación de `if tenant == "prezunic"` dispersos por todos los casos de uso, y sin
duplicar lógica entre servicios que hacen esencialmente lo mismo.

La respuesta está en cómo se construye el sistema al arrancar, no en cómo se comporta
mientras corre.

## Alternativas consideradas

### Opción A — Condicionales en runtime por tenant
Detectar el tenant en cada request y bifurcar el comportamiento con condicionales.

**Por qué se descartó:** Los condicionales dispersos son difíciles de razonar, testear y
mantener. Agregar una nueva marca requiere buscar todos los puntos donde hay lógica
condicional y agregar una rama más. El riesgo de olvidar un lugar es alto. A medida
que crecen las marcas, el código se vuelve imposible de entender.

### Opción B — Configuración dinámica consultada en cada request
Un mapa de configuración por tenant consultado en cada invocación.

**Por qué se descartó:** Introduce latencia y complejidad en el hot path. Los errores de
configuración (tenant no encontrado, configuración malformada) se descubren en runtime
durante el tráfico real. No permite que el compilador valide que el comportamiento de
un tenant está completo.

### Opción C — Un microservicio por tenant
Deployar instancias completamente separadas sin código compartido.

**Por qué se descartó:** Imposible de mantener. Los bugs se arreglan N veces, las features
se implementan N veces. El crecimiento de marcas escala linealmente el costo de mantenimiento.
Es exactamente el problema que White Label existe para resolver.

### Opción elegida — Un binario por tenant con inyección FX en tiempo de construcción
El tenant se inyecta al arrancar como primer nodo del grafo FX. Todo el código de negocio
es agnóstico al tenant porque recibe comportamientos ya configurados, no el tenant en sí.

## Decisión

We will use a one-binary-per-tenant model where the tenant is injected as the first node
of the FX dependency graph, and behavioral differences are modeled using a hierarchy of
three mechanisms in order of increasing complexity.

### Un deployment, un tenant

Cada instancia del servicio corre para una sola marca. El tenant se especifica al arrancar:

```bash
main server --tenant=br-prezunic
main server --tenant=co-jumbo
main server --tenant=ar-disco
```

El flag `--tenant` es validado por Cobra usando `config.NewTenant()` y provisto al grafo
FX como `config.TenantValue`. El tenant **no llega por variable de entorno ni por archivo
de configuración** — solo por este flag CLI obligatorio.

Ese valor se inyecta como el primer nodo del grafo FX. A partir de ahí, todos los módulos
que necesiten comportarse distinto según la marca reciben ese valor como dependencia y
resuelven su configuración en tiempo de construcción.

```go
app := fx.New(
    fx.Provide(func() (config.TenantValue, error) {
        return config.NewTenant(tenant)
    }),
    common.Module,
    platform.Module,
    health.Module,
    // dominios del servicio...
)
```

**No hay detección de tenant en runtime.** No hay un mapa de tenants que se consulta
en cada request. Las decisiones están tomadas desde el momento en que el proceso arranca.

### Jerarquía de mecanismos para modelar variaciones

El criterio para elegir cuál usar: empezar por el más simple y subir de nivel solo cuando
la complejidad lo justifica.

#### Nivel 1 — Flag booleano

Un flag activa o desactiva una feature o un comportamiento completo. Se resuelve una sola
vez en el constructor del Core.

```go
// En el constructor del Core
useOAuth := false
switch tenant {
case config.BR_PREZUNIC:
    useOAuth = true
}

// En el caso de uso
if c.UseOAuth {
    return c.SignInOAuth(ctx, username, password)
}
return c.SignInVtex(ctx, username, password)
```

Usar cuando: la diferencia es binaria (hacer X vs. no hacer X, o hacer X vs. hacer Y).

#### Nivel 2 — Function type

Una function type modela una regla que siempre aplica pero que cada tenant implementa
diferente. Se define como un tipo de función con firma clara. Las implementaciones
específicas de un tenant viven en `core/<tenant>/`.

```go
// Definición
type UsernameValidator func(string) error
type UsernameSanitizer func(string) string

// En el constructor del Core
sanitizer := rule.EmptySanitizer        // default
validator := rule.EmailValidator        // default
switch tenant {
case config.BR_PREZUNIC:
    sanitizer = prezunic.CpfSanitizer
    validator = prezunic.CpfValidator
}

// En el caso de uso — no sabe qué implementación está usando
username = c.usernameSanitizer(username)
if err := c.usernameValidator(username); err != nil {
    return nil, err
}
```

Usar cuando: la regla siempre se ejecuta pero la lógica varía por tenant.

#### Nivel 3 — Interface

Una interface se usa cuando la regla tiene dependencias de plataforma distintas según
el tenant (una función no alcanza porque necesita clientes externos distintos).

```go
// Definición de la interface
type SellerValidator interface {
    ValidateSellers(ctx context.Context, req *v1.ValidateSellersRequest) (*v1.ValidateSellersResponse, error)
}

// Selección en el módulo FX (no en el Core)
var Module = fx.Module("sellervalidator",
    fx.Provide(func(tenant config.TenantValue, cms *cms.CmsClient, vtex *vtex.VtexClient) SellerValidator {
        switch tenant {
        case config.AR_JUMBO, config.AR_DISCO:
            return NewSellerValidatorSimulation(cms, vtex)
        default:
            return NewSellerValidatorRegion(vtex)
        }
    }),
)
```

La selección de qué implementación usar vive en el **módulo FX**, no en el Core.
El Core recibe la interface y la invoca sin saber cuál implementación está usando.

Usar cuando: la regla requiere dependencias de plataforma distintas por tenant.

### Regla de nomenclatura: features, no tenants

Las reglas y flags se nombran en términos de la feature, **nunca en términos del tenant**.

```go
// ✓ Correcto
UseOAuth
UsernameValidator
SellerValidator

// ✗ Incorrecto
IsPrezunic
IsArgentina
JumboSellerLogic
```

Una regla bien nombrada puede ser adoptada por cualquier tenant futuro sin cambiar
su definición. El tenant que la usa hoy es un detalle de configuración, no parte del diseño.

## Consecuencias

**Positivas:**
- El código de negocio (Core) es agnóstico al tenant: no tiene condicionales de marca.
- Los errores de configuración se descubren al arrancar el proceso, no durante tráfico real.
- Agregar una nueva marca es configurar flags y rules en los constructores existentes,
  sin tocar la lógica de negocio.
- La jerarquía flag → function type → interface tiene complejidad creciente y explícita:
  se sube de nivel solo cuando hay una razón concreta.

**Negativas:**
- El modelo requiere que quien diseña el Core piense en términos de features, no tenants.
  Es un esfuerzo consciente que puede no ser natural al principio.
- Las implementaciones de rules específicas por tenant (`core/<tenant>/`) proliferan
  a medida que crece la cantidad de marcas.

**Riesgos y mitigaciones:**
- **Condicionales de tenant en el Core** → Violación de este ADR. Si se detecta un
  `if tenant == "algo"` en lógica de negocio, debe refactorizarse a flag o function type.
- **Subir de nivel innecesariamente** (usar interface cuando un flag alcanza) → La revisión
  de código es el control. El criterio es siempre empezar por el nivel más simple.

## ADRs relacionados

- ADR-000: Arquitectura en cuatro capas (el constructor del Core es donde se resuelven
  los flags y function types; el módulo FX del dominio es donde se seleccionan las interfaces)
