Inicio / Elixir / Phoenix Framework: Web en Tiempo Real / Contextos y Arquitectura

Contextos y Arquitectura

Bounded contexts, generators, separación de dominios.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Contextos y Arquitectura en Phoenix

Introducción

Los contextos en Phoenix son módulos que agrupan funcionalidad relacionada y exponen una API pública clara. Actúan como fronteras (boundaries) entre distintas áreas del dominio de negocio, promoviendo la separación de responsabilidades.

¿Qué Son los Contexts?

Un contexto es un módulo Elixir que encapsula la lógica de un dominio específico:

defmodule MyApp.Catalogo do
  alias MyApp.Repo
  alias MyApp.Catalogo.Producto

  def list_productos do
    Repo.all(Producto)
  end

  def get_producto!(id), do: Repo.get!(Producto, id)

  def create_producto(attrs \\ %{}) do
    %Producto{}
    |> Producto.changeset(attrs)
    |> Repo.insert()
  end

  def update_producto(%Producto{} = producto, attrs) do
    producto
    |> Producto.changeset(attrs)
    |> Repo.update()
  end

  def delete_producto(%Producto{} = producto) do
    Repo.delete(producto)
  end
end

mix phx.gen.context

Phoenix genera contextos automáticamente con generadores:

# Genera contexto, schema y migración
mix phx.gen.context Catalogo Producto productos nombre:string precio:decimal stock:integer

# Genera contexto con HTML completo
mix phx.gen.html Catalogo Producto productos nombre:string precio:decimal

# Genera contexto con API JSON
mix phx.gen.json Catalogo Producto productos nombre:string precio:decimal

Esto crea la estructura:

# lib/my_app/catalogo.ex          -> Contexto (API pública)
# lib/my_app/catalogo/producto.ex  -> Schema Ecto
# priv/repo/migrations/xxx.exs    -> Migración

Diseño de Fronteras (Boundary Design)

Las fronteras definen cómo interactúan los contextos entre sí:

# Contexto Cuentas - maneja usuarios y autenticación
defmodule MyApp.Cuentas do
  def get_usuario!(id), do: Repo.get!(Usuario, id)
  def autenticar(email, password), do: # ...
end

# Contexto Pedidos - maneja órdenes y compras
defmodule MyApp.Pedidos do
  alias MyApp.Cuentas

  def crear_pedido(%{usuario_id: usuario_id} = attrs) do
    usuario = Cuentas.get_usuario!(usuario_id)

    %Pedido{}
    |> Pedido.changeset(attrs)
    |> Ecto.Changeset.put_assoc(:usuario, usuario)
    |> Repo.insert()
  end
end

# Contexto Notificaciones - envía emails y alertas
defmodule MyApp.Notificaciones do
  def notificar_pedido_creado(pedido) do
    pedido
    |> MyApp.Emails.confirmacion_pedido()
    |> MyApp.Mailer.deliver()
  end
end

Separación de Responsabilidades

Cada contexto debe tener una responsabilidad única y bien definida:

# MAL: Todo junto en un solo módulo
defmodule MyApp.Todo do
  def crear_usuario(attrs), do: # ...
  def crear_producto(attrs), do: # ...
  def crear_pedido(attrs), do: # ...
  def enviar_email(usuario), do: # ...
end

# BIEN: Separado por dominio
defmodule MyApp.Cuentas do
  # Solo usuarios, roles, autenticación
end

defmodule MyApp.Catalogo do
  # Solo productos, categorías, inventario
end

defmodule MyApp.Pedidos do
  # Solo órdenes, items, pagos
end

API Pública del Contexto

La API pública del contexto debe ser clara, documentada y estable:

defmodule MyApp.Blog do
  @moduledoc """
  Contexto del Blog. Gestiona artículos, comentarios y etiquetas.
  """

  alias MyApp.Blog.{Articulo, Comentario}

  @doc "Lista artículos publicados con paginación."
  def list_articulos_publicados(opts \\ []) do
    page = Keyword.get(opts, :page, 1)
    per_page = Keyword.get(opts, :per_page, 10)

    Articulo
    |> where([a], a.publicado == true)
    |> order_by([a], desc: a.published_at)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> Repo.all()
  end

  @doc "Crea un artículo asociado a un autor."
  def create_articulo(autor, attrs) do
    %Articulo{autor_id: autor.id}
    |> Articulo.changeset(attrs)
    |> Repo.insert()
  end

  @doc "Agrega un comentario a un artículo."
  def agregar_comentario(articulo, attrs) do
    %Comentario{articulo_id: articulo.id}
    |> Comentario.changeset(attrs)
    |> Repo.insert()
  end
end

Refactoring a Contexts

Cuando el código crece, debemos extraer contextos nuevos:

# Antes: MyApp.Cuentas manejaba usuarios Y notificaciones
defmodule MyApp.Cuentas do
  def registrar_usuario(attrs) do
    with {:ok, usuario} <- crear_usuario(attrs) do
      enviar_email_bienvenida(usuario)
      {:ok, usuario}
    end
  end
end

# Después: Extraemos notificaciones a su propio contexto
defmodule MyApp.Cuentas do
  def registrar_usuario(attrs) do
    Multi.new()
    |> Multi.insert(:usuario, Usuario.changeset(%Usuario{}, attrs))
    |> Multi.run(:perfil, fn _repo, %{usuario: u} ->
      crear_perfil_default(u)
    end)
    |> Repo.transaction()
  end
end

defmodule MyApp.Notificaciones do
  def bienvenida(usuario) do
    usuario
    |> MyApp.Emails.bienvenida()
    |> MyApp.Mailer.deliver_later()
  end
end

Los controladores orquestan los contextos:

def create(conn, %{"usuario" => params}) do
  with {:ok, %{usuario: usuario}} <- Cuentas.registrar_usuario(params) do
    Notificaciones.bienvenida(usuario)
    render(conn, :show, usuario: usuario)
  end
end

Resumen

Los contextos en Phoenix son la herramienta principal para organizar la lógica de negocio. Definen fronteras claras entre dominios, exponen APIs públicas estables y promueven la separación de responsabilidades. El generador mix phx.gen.context facilita su creación, y el refactoring a contextos es esencial conforme la aplicación crece.

🔒

Ejercicio práctico disponible

Contextos de dominio

Desbloquear ejercicios
// Contextos de dominio
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes