Skip to content

Filosofía Elixir / OTP aplicada en autowebapp

Los 6 conceptos fundamentales del ecosistema BEAM y cómo aparecen en un frontend Phoenix LiveView stateless

📖 ¿Quieres ver este documento más hermoso? Haz clic aquí para la versión HTML con diseño completo, badges de veredicto y bloques de código resaltados.

Dato clave: autowebapp no tiene base de datos ni estado de dominio propio. Es un frontend Phoenix LiveView stateless: todos los datos viven en microservicios externos a los que pega por HTTP con Req. Esto determina qué conceptos OTP aplican con fuerza, cuáles de forma parcial, y cuáles están deliberadamente ausentes.

ConceptoVeredicto
Let it crash + Árbol de supervisión✅ Infraestructura / ⚠️ Dominio defensivo
Actor model / proceso por usuario✅ Fuerte
Estado e inmutabilidad✅ Sí
Behaviours / programar contra contratos✅ Intensivo
Concurrencia y scheduler del BEAM✅ Fundación / ⚠️ Clustering latente
PubSub / mensajería distribuida✅ Fuerte

1. Let it crash y Árboles de Supervisión

Section titled “1. Let it crash y Árboles de Supervisión”

La idea general: en Erlang/OTP no programas defensivamente contra cada error posible. Dejas que el proceso muera limpiamente y que el supervisor lo reinicie en estado conocido. Un supervision tree define la estructura de recuperación: qué procesos existen, en qué orden se inician, y qué pasa cuando uno falla.

Estrategias principales:

  • :one_for_one — solo el proceso que falló se reinicia.
  • :one_for_all — si uno falla, todos los hermanos se reinician.
  • :rest_for_one — si uno falla, se reinician él y todos los posteriores.

En autowebapp: existe un supervisor :one_for_one real con cinco hijos en orden estricto:

def start(_type, _args) do
children = [
AutowebappWeb.Telemetry,
{DNSCluster, query: Application.get_env(:autowebapp, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Autowebapp.PubSub},
Autowebapp.Shared.Discovery,
AutowebappWeb.Endpoint # siempre el último
]
opts = [strategy: :one_for_one, name: Autowebapp.Supervisor]
Supervisor.start_link(children, opts)
end

📍 lib/autowebapp/application.ex:31–48

Por qué el dominio es defensivo: cada LiveView es la sesión visible de un usuario real. Si muere, ese usuario ve su pantalla romperse. “Let it crash” brilla en workers de fondo reemplazables, no en la interfaz interactiva de alguien. El código de dominio usa tuplas {:ok, _} / {:error, _} y fallbacks seguros — no es un rechazo de la filosofía, es aplicarla correctamente al contexto.


La idea general: cada proceso tiene su propio heap, no comparte memoria, y solo se comunica enviando mensajes a mailboxes de otros procesos. En el BEAM crear un proceso cuesta ~2 KB de RAM y microsegundos — millones coexistiendo es normal.

En autowebapp: cada LiveView conectada es un proceso BEAM. 5.000 pasajeros conectados = 5.000 procesos LiveView independientes. El sistema de notificaciones es el actor model en estado puro:

# 1. Suscripción al montar
def subscribe(rol, id) do
Phoenix.PubSub.subscribe(@pubsub, topic(rol, id))
end
# 2. Broadcast al ocurrir un evento
Phoenix.PubSub.broadcast(@pubsub, topic(rol, id), {:nueva_notificacion, notificacion})
# 3. Recepción en el mailbox
def handle_info({:nueva_notificacion, notif}, socket) do
{:noreply, update(socket, :notificaciones, &[notif | &1])}
end

El pasajero no “consulta” periódicamente — el mensaje llega solo cuando hay algo que decirle. El fallo de cualquier proceso individual no afecta a los demás.


La idea general: en Elixir todos los datos son inmutables — “modificar” un mapa crea uno nuevo compartiendo partes del original. OTP reconcilia esto con el estado mutable a través de procesos guardianes (GenServer, Agent): el proceso custodia un valor que puede reemplazarse, pero como es el único que accede a su propia memoria, no hay condiciones de carrera.

En autowebapp:

a) El Agent de Discovery — inicia con nil y se reemplaza tras el primer fetch HTTP exitoso:

use Agent
def start_link(_opts) do
Agent.start_link(fn -> nil end, name: __MODULE__)
end
def get_config(force_reload \\ false) do
cached = if force_reload, do: nil, else: Agent.get(__MODULE__, & &1)
if cached do
{:ok, cached}
else
case client.fetch_config() do
{:ok, data} ->
case parse_and_validate(data) do
{:ok, config} ->
Agent.update(__MODULE__, fn _ -> config end)
{:ok, config}
end
end
end
end

📍 lib/autowebapp/shared/discovery.ex:34,48–49 y 66–81

b) Socket assigns: el socket es una struct inmutable. Cada assign/3 devuelve un socket nuevo:

socket =
socket
|> assign(:app_state, app_state)
|> assign(:current_user_id, usuario_id)
|> assign(:app_role, app_role)

📍 lib/autowebapp_web/live_auth.ex:165–169


La idea general: un behaviour define qué funciones deben existir (con @callback) sin implementar ninguna — es una interface nativa verificada en tiempo de compilación. GenServer es el behaviour más fundamental de OTP.

En autowebapp: el patrón dominante es behaviour puro + implementación real (_impl) + mock de test (_mock). Existen 8 behaviours de clientes HTTP: discovery_client, login_client, viaje_client, pasajero_client, conductor_client, pagos_client, notificacion_client, htmls_client.

defmodule Autowebapp.Clients.LoginClient do
@callback fetch_logos(login_url :: String.t()) ::
{:ok, list(map())} | {:error, any()}
@callback valida_existencia_habilitacion_correo(
login_url :: String.t(),
correo :: String.t(),
id_app :: String.t(),
enterprise_id :: integer()
) :: {:ok, map()} | {:error, any()}
end

En producción usa NotificacionClientImpl (HTTP real); en tests, Mox inyecta el mock y valida en tiempo de compilación que implementa exactamente las callbacks del behaviour.

Por qué no hay GenServer de dominio ni GenStage: el estado de negocio vive en los microservicios externos — no hay nada que custodiar localmente. Reconocer cuándo no usar una herramienta de OTP es tan importante como saber cuándo usarla.


La idea general: el BEAM tiene concurrencia nativa única:

  • Procesos ultra-ligeros: ~2 KB de RAM (vs ~8 MB de un thread OS).
  • Scheduler preemptivo por reducciones: cada proceso recibe ~2.000 reducciones antes de pausarse — ningún proceso puede monopolizar la CPU.
  • Múltiples schedulers: tantos como núcleos lógicos, distribuyendo procesos automáticamente.
  • GC por proceso: no existe stop-the-world global.
  • Hot code reload: config_change/3 reemplaza módulos sin detener el sistema.

En autowebapp: la concurrencia masiva es la fundación implícita — simplemente está ahí por usar Phoenix LiveView. El hot reload es estándar:

@impl true
def config_change(changed, _new, removed) do
AutowebappWeb.Endpoint.config_change(changed, removed)
:ok
end

📍 lib/autowebapp/application.ex:55–58

Clustering latente: DNSCluster está en el árbol de supervisión pero con :ignore en desarrollo. La infraestructura para conectar nodos Erlang está lista, en modo standby — solo necesita dns_cluster_query en producción para activarse.


La idea general: Phoenix.PubSub es mensajería publish/subscribe sobre primitivos nativos del BEAM. Procesos se suscriben a topics y cualquier proceso puede hacer broadcast — los suscriptores reciben el mensaje en su mailbox. En cluster Erlang/OTP enruta automáticamente entre nodos. No hay locks ni memoria compartida.

En autowebapp: el sistema de notificaciones combina persistencia HTTP con entrega en tiempo real:

# Topics centralizados — nadie escribe estas strings directamente
def topic("A", id), do: "conductor:#{id}"
def topic("P", id), do: "pasajero:#{id}"
# El patrón dual: primero persiste, luego notifica
def notificar(core_url, attrs) do
client = notificacion_client()
case client.crear(core_url, attrs) do
{:ok, notificacion} ->
rol = Map.get(attrs, :destinatario_rol)
id = Map.get(attrs, :destinatario_id)
Phoenix.PubSub.broadcast(@pubsub, topic(rol, id), {:nueva_notificacion, notificacion})
{:ok, notificacion}
{:error, reason} = error ->
Logger.error("[Notificaciones] Error: #{inspect(reason)}")
error
end
end

📍 lib/autowebapp/shared/notificaciones.ex:62–63 y 105–128

El patrón dual garantiza:

  • Persistencia HTTP primero: la notificación se crea en core-empresas antes de cualquier broadcast. El usuario la verá aunque vuelva más tarde.
  • Broadcast PubSub (best-effort): solo si la persistencia fue exitosa. Si el destinatario está offline, el mensaje se descarta silenciosamente — sin errores, sin reintentos.

autowebapp aprovecha con fuerza la mitad concurrencia y aislamiento de OTP — actor model, procesos por usuario, PubSub, behaviours, Agent — y aplica poco la mitad “let it crash / resiliencia por reinicio”, y eso es correcto.

“La filosofía OTP no es un dogma. Es un conjunto de herramientas: usas las que encajan con el dominio, y entiendes por qué no usas las otras.”