Elixir e Phoenix: construindo aplicações escaláveis

1. Introdução ao Ecossistema Elixir e Phoenix

1.1. Por que Elixir? A Máquina Virtual BEAM e o modelo de concorrência baseado em atores

Elixir é uma linguagem funcional que roda sobre a Máquina Virtual BEAM (originalmente criada para Erlang). O que torna Elixir excepcional para escalabilidade é seu modelo de concorrência baseado no padrão de atores. Diferente de linguagens tradicionais que usam threads do sistema operacional, Elixir utiliza processos leves (green threads) que consomem pouquíssima memória — cada processo ocupa aproximadamente 2-4 KB. Isso permite que uma única instância do BEAM gerencie milhões de processos simultaneamente sem degradação significativa de desempenho.

1.2. Phoenix como framework web: produtividade, desempenho e tolerância a falhas

Phoenix é o framework web mais popular do ecossistema Elixir. Ele herda todas as vantagens do BEAM e adiciona camadas de produtividade como LiveView (para interfaces reativas sem JavaScript complexo), Ecto (para persistência de dados) e Channels (para comunicação em tempo real). Phoenix é conhecido por sua baixa latência e alta throughput, sendo capaz de servir milhares de requisições por segundo com uso mínimo de recursos.

1.3. Escalabilidade horizontal vs. vertical: como Elixir lida naturalmente com milhões de conexões

Enquanto muitas linguagens exigem arquiteturas complexas para escalar horizontalmente (adicionando mais servidores), Elixir escala verticalmente de forma natural. Um único servidor BEAM pode gerenciar 2 milhões de conexões WebSocket simultâneas com 2 GB de RAM. Quando necessário, a escalabilidade horizontal é alcançada através de distribuição nativa entre nós BEAM, sem necessidade de balanceadores externos complexos.

2. Fundamentos de Concorrência e Escalabilidade no BEAM

2.1. Processos leves (green threads) e comunicação via mensagens assíncronas

No BEAM, cada processo é isolado e se comunica exclusivamente através de mensagens assíncronas. Não há memória compartilhada, eliminando problemas de race conditions e locks. Um exemplo prático:

defmodule MeuModulo do
  def criar_processos(quantidade) do
    1..quantidade
    |> Enum.map(fn id ->
      spawn(fn -> processar_tarefa(id) end)
    end)
  end

  defp processar_tarefa(id) do
    receive do
      {:tarefa, dados} -> IO.puts("Processo #{id} recebeu: #{dados}")
    end
  end
end

# Criando 100.000 processos simultaneamente
MeuModulo.criar_processos(100_000)

2.2. Supervisão e tolerância a falhas: a filosofia “let it crash”

A filosofia central do BEAM é "deixe quebrar" (let it crash). Em vez de tentar prever e tratar todos os erros possíveis, você define árvores de supervisão que reiniciam automaticamente processos que falham. Isso torna sistemas extremamente resilientes:

defmodule MeuSupervisor do
  use Supervisor

  def start_link(_opts) do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    children = [
      {MeuGenServer, []},
      {OutroProcesso, []}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

2.3. Gerenciamento de estado imutável e isolamento entre processos

Cada processo no BEAM possui seu próprio estado isolado. Isso significa que um processo nunca pode corromper o estado de outro. O estado é gerenciado através de GenServers ou Agents, que encapsulam dados mutáveis de forma segura:

defmodule Contador do
  use Agent

  def start_link(initial_value \\ 0) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def incrementar do
    Agent.update(__MODULE__, fn state -> state + 1 end)
  end

  def valor_atual do
    Agent.get(__MODULE__, fn state -> state end)
  end
end

3. Estruturando uma Aplicação Phoenix para Escalar

3.1. Arquitetura de contexto (Contexts) e separação de responsabilidades

Phoenix incentiva o uso de Contexts para organizar o código em domínios coesos. Cada Context encapsula um conjunto de funcionalidades relacionadas, facilitando a manutenção e evolução do sistema:

# lib/minha_app/catalogo.ex
defmodule MinhaApp.Catalogo do
  alias MinhaApp.Catalogo.{Produto, Categoria}

  def listar_produtos do
    Repo.all(Produto)
  end

  def criar_produto(attrs) do
    %Produto{}
    |> Produto.changeset(attrs)
    |> Repo.insert()
  end

  def buscar_por_categoria(categoria_id) do
    Repo.get_by(Categoria, id: categoria_id)
    |> Repo.preload(:produtos)
  end
end

3.2. Uso de GenServers e Agents para estado compartilhado escalável

Para estado que precisa ser compartilhado entre múltiplos processos, GenServers oferecem uma abstração robusta com suporte a chamadas síncronas e assíncronas:

defmodule CacheManager do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def put(chave, valor) do
    GenServer.cast(__MODULE__, {:put, chave, valor})
  end

  def get(chave) do
    GenServer.call(__MODULE__, {:get, chave})
  end

  def handle_call({:get, chave}, _from, state) do
    {:reply, Map.get(state, chave), state}
  end

  def handle_cast({:put, chave, valor}, state) do
    {:noreply, Map.put(state, chave, valor)}
  end
end

3.3. Configuração de pools de conexão e balanceamento de carga com Phoenix PubSub

Phoenix PubSub permite comunicação publish-subscribe distribuída entre processos e nós:

# config.exs
config :my_app, MyApp.PubSub,
  adapter: Phoenix.PubSub.PG2,
  name: MyApp.PubSub

# Em um GenServer
Phoenix.PubSub.subscribe(MyApp.PubSub, "notificacoes")

# Em outro processo
Phoenix.PubSub.broadcast(MyApp.PubSub, "notificacoes", %{tipo: :nova_mensagem, dados: "Olá!"})

4. Comunicação em Tempo Real com Phoenix Channels

4.1. WebSockets nativos no Phoenix: como gerenciar milhares de conexões simultâneas

Phoenix Channels abstraem WebSockets e oferecem um modelo baseado em tópicos. Cada conexão é tratada como um processo leve:

# lib/my_app_web/channels/chat_channel.ex
defmodule MyAppWeb.ChatChannel do
  use Phoenix.Channel

  def join("chat:" <> sala, _params, socket) do
    {:ok, assign(socket, :sala, sala)}
  end

  def handle_in("nova_mensagem", %{"texto" => texto}, socket) do
    broadcast!(socket, "mensagem_recebida", %{usuario: socket.assigns.usuario, texto: texto})
    {:noreply, socket}
  end
end

4.2. Tópicos e difusão de mensagens com PubSub distribuído

Os tópicos permitem criar canais de comunicação flexíveis. Um usuário pode se inscrever em múltiplos tópicos simultaneamente:

defmodule MyAppWeb.RoomChannel do
  use Phoenix.Channel

  def join("room:" <> room_id, _params, socket) do
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_out("mensagem", payload, socket) do
    push(socket, "nova_mensagem", payload)
    {:noreply, socket}
  end
end

4.3. Estratégias de backpressure e rate limiting para evitar sobrecarga

Para evitar sobrecarga, implemente rate limiting com ETS ou Redis:

defmodule RateLimiter do
  use GenServer

  def check_rate(key, max_requests, time_window_ms) do
    GenServer.call(__MODULE__, {:check, key, max_requests, time_window_ms})
  end

  def handle_call({:check, key, max, window}, _from, state) do
    now = System.monotonic_time(:millisecond)
    requests = Map.get(state, key, [])

    # Remove requisições antigas
    recent = Enum.filter(requests, fn t -> now - t < window end)

    if length(recent) < max do
      new_state = Map.put(state, key, [now | recent])
      {:reply, :ok, new_state}
    else
      {:reply, {:error, :rate_limit_exceeded}, state}
    end
  end
end

5. Banco de Dados e Persistência Escalável

5.1. Ecto: consultas otimizadas, replicação e sharding de dados

Ecto oferece consultas otimizadas com composição e lazy evaluation:

defmodule MyApp.Produtos do
  import Ecto.Query

  def buscar_produtos_em_promocao(preco_max) do
    Produto
    |> where([p], p.preco < ^preco_max)
    |> where([p], p.em_promocao == true)
    |> order_by([p], desc: p.data_criacao)
    |> limit(50)
    |> Repo.all()
  end

  def buscar_com_filtros(params) do
    Produto
    |> filter_by_categoria(params[:categoria])
    |> filter_by_preco(params[:preco_min], params[:preco_max])
    |> Repo.paginate(page: params[:page], page_size: 20)
  end
end

5.2. Uso de filas assíncronas (Oban, Broadway) para processamento em lote

Oban gerencia filas de trabalho com suporte a retry e agendamento:

defmodule MyApp.Workers.EmailWorker do
  use Oban.Worker, queue: :emails

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id, "template" => template}}) do
    user = Repo.get!(User, user_id)
    MyApp.Mailer.send_email(user, template)
    :ok
  end

  # Agendando um job
  %{user_id: 123, template: "welcome"}
  |> MyApp.Workers.EmailWorker.new()
  |> Oban.insert()

5.3. Cache distribuído com Redis e integração com Phoenix

Redis oferece cache rápido e distribuído:

defmodule MyApp.Cache do
  use GenServer

  def get(key) do
    case Redix.command(:redix, ["GET", key]) do
      {:ok, nil} -> {:miss, nil}
      {:ok, value} -> {:hit, value}
    end
  end

  def set(key, value, ttl \\ 3600) do
    Redix.command(:redix, ["SETEX", key, ttl, value])
  end

  def invalidate(pattern) do
    {:ok, keys} = Redix.command(:redix, ["KEYS", pattern])
    Enum.each(keys, fn key ->
      Redix.command(:redix, ["DEL", key])
    end)
  end
end

6. Deploy, Monitoramento e Observabilidade

6.1. Implantação em clusters distribuídos

Elixir suporta deploy em diversas plataformas. Um exemplo de configuração para Fly.io:

# fly.toml
app = "my-app"

[build]
  builder = "elixir:1.16"

[http_service]
  internal_port = 4000
  force_https = true

[[services]]
  protocol = "tcp"
  internal_port = 4000

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]

6.2. Métricas e tracing com Telemetry, Prometheus e Grafana

Telemetry é o sistema de métricas nativo do Elixir:

# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
  use Supervisor

  def start_link(_opts) do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    children = [
      {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp metrics do
    [
      counter("http.request.count"),
      last_value("http.request.latency"),
      distribution("http.request.duration")
    ]
  end
end

6.3. Logs estruturados e debugging remoto em produção

Logger configurado para JSON estruturado:

# config/prod.exs
config :logger, :console,
  format: "$metadata[$level] $message\n",
  metadata: [:request_id, :user_id]

config :logger_json, :backend,
  formatter: LoggerJSON.Formatters.GoogleCloud,
  level: :info

7. Casos de Uso Reais e Padrões Avançados

7.1. Aplicações de chat, streaming e jogos multiplayer em tempo real

Aplicações de chat são implementadas naturalmente com Phoenix Channels:

defmodule MyAppWeb.ChatLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, "chat")
    end

    {:ok, assign(socket, mensagens: [], texto: "")}
  end

  def handle_event("enviar", %{"texto" => texto}, socket) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "chat", %{usuario: socket.assigns.current_user, texto: texto})
    {:noreply, assign(socket, texto: "")}
  end

  def handle_info(%{usuario: usuario, texto: texto}, socket) do
    {:noreply, update(socket, :mensagens, fn msgs -> msgs ++ [%{usuario: usuario, texto: texto}] end)}
  end
end

7.2. Integração com sistemas legados e microserviços via RabbitMQ ou Kafka

Broadway é ideal para processamento de streams de dados:

defmodule MyApp.BroadwayPipeline do
  use Broadway

  alias Broadway.Message

  def start_link(_opts) do
    Broadway.start_link(__MODULE__,
      name: __MODULE__,
      producer: [
        module: {BroadwayRabbitMQ.Producer, queue: "orders"},
        concurrency: 10
      ],
      processors: [
        default: [concurrency: 50]
      ],
      batchers: [
        default: [batch_size: 100, batch_timeout: 2000]
      ]
    )
  end

  def handle_message(_, message, _) do
    message
    |> Message.update_data(fn data -> process_order(data) end)
  end

  def handle_batch(_, messages, _, _) do
    Repo.insert_all(Order, Enum.map(messages, & &1.data))
    messages
  end
end

7.3. Testes de carga e simulação de milhões de conexões

defmodule MyAppWeb.LoadTest do
  use Phoenix.ChannelTest

  @endpoint MyAppWeb.Endpoint

  test "simula 10000 conexões simultâneas" do
    connections = 
      1..10_000
      |> Enum.map(fn _ ->
        Task.async(fn ->
          {:ok, _, socket} = socket(MyAppWeb.UserSocket, %{user_id: :rand.uniform(1000)})
          |> subscribe_and_join(MyAppWeb.RoomChannel, "room:lobby")

          push(socket, "nova_mensagem", %{texto: "Hello"})

          assert_push "mensagem_recebida", _
          :ok
        end)
      end)

    results = Task.await_many(connections, :infinity)
    assert Enum.all?(results, &(&1 == :ok))
  end
end

Referências

) — Guia prático sobre processos, mensagens e árvores de supervisão
- Oban: Job Processing for Elixir — Documentação oficial para filas assíncronas com suporte a retry e agendamento
- Broadway: Data Processing Pipelines — Framework para processamento de streams e integração com RabbitMQ, Kafka e outros
- Telemetry: Métricas e Instrumentação — Sistema nativo de métricas para Elixir, compatível com Prometheus e Grafana
- Phoenix PubSub: Comunicação Distribuída — Documentação sobre difusão de mensagens em clusters
- Fly.io: Deploy de Aplicações Elixir — Guia de implantação em plataforma gerenciada com suporte nativo a BEAM
- Tsung: Teste de Carga Distribuído — Ferramenta para simular milhões de conexões simultâneas
- Ecto: Consultas e Migrations — Documentação oficial do banco de dados para Phoenix
- Redix: Cliente Redis para Elixir — Biblioteca para cache distribuído e filas
- Kubernetes para Elixir — Guia de orquestração de clusters BEAM em contêineres

Conclusão: O Caminho para Aplicações Escaláveis com Elixir e Phoenix

Construir aplicações escaláveis não é apenas uma questão de tecnologia, mas de arquitetura e mentalidade. Elixir e Phoenix oferecem um ecossistema maduro que abraça a concorrência desde o nível mais baixo — com processos leves na BEAM — até o mais alto, com ferramentas como Phoenix PubSub, Oban e Broadway. A filosofia "let it crash" combinada com árvores de supervisão garante tolerância a falhas sem complexidade adicional, enquanto o modelo de atores elimina problemas clássicos de concorrência, como race conditions e deadlocks.

Ao longo deste artigo, exploramos desde os fundamentos de concorrência até casos de uso reais, como chat em tempo real, processamento de filas e integração com sistemas legados. Cada camada — banco de dados, cache, mensageria e deploy — foi projetada para escalar horizontalmente, permitindo que sua aplicação lide com milhões de conexões simultâneas sem perda de desempenho.

O segredo do sucesso está em adotar os padrões certos: Contexts para organização, GenServers para estado compartilhado, PubSub para comunicação em tempo real e filas para tarefas assíncronas. Com as ferramentas certas e uma arquitetura bem planejada, Elixir e Phoenix permitem que você construa sistemas que não apenas escalam, mas também permanecem resilientes e fáceis de manter.

Agora é sua vez: experimente, implemente um protótipo, teste com milhares de conexões e veja como a BEAM transforma a maneira como você pensa sobre escalabilidade. O futuro das aplicações web é distribuído, tolerante a falhas e, acima de tudo, escalável — e Elixir está na vanguarda dessa revolução.