Escalar uma aplicação em Elixir não é uma questão de apertar um botão. É um conjunto de decisões arquiteturais, de código e de infraestrutura que precisam ser tomadas de forma consistente ao longo do ciclo de vida do produto. A boa notícia é que a BEAM já oferece a fundação — o que você precisa é não desperdiçá-la com anti-patterns no código.
Este guia reúne as práticas mais relevantes — o que evitar, o que implementar e como configurar a infraestrutura — para times que constroem produtos sérios com Elixir e Phoenix.
O que não fazer para escalar (e por quê esses erros são tão comuns)
Antes de falar sobre o que fazer, é mais eficiente endereçar o que desfaz o potencial do Elixir. Esses erros costumam surgir quando desenvolvedores vindos de Rails ou Django trazem hábitos de outras linguagens sem adaptar ao paradigma do Phoenix.
Erro 1: lógica de negócio no controller
Esse é provavelmente o anti-pattern mais comum em aplicações Phoenix. O Phoenix é construído em torno do conceito de contextos — módulos que encapsulam a lógica de negócio de um domínio específico. Quando queries Ecto e regras de negócio vazam para o controller, a modularidade e testabilidade desaparecem.
O padrão errado:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
import Ecto.Query, warn: false
alias MyApp.Accounts.User
alias MyApp.Repo
def index(conn, %{} = params) do
users = User |> where(name: params["name"]) |> Repo.all()
render(conn, "index.json", users: users)
end
end
O padrão correto — o controller apenas filtra parâmetros e delega ao contexto:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyApp.Accounts
action_fallback MyAppWeb.FallbackController
def index(conn, %{} = params) do
users = params |> permitted_filter_params() |> Accounts.list_users()
render(conn, "index.json", users: users)
end
defp permitted_filter_params(%{} = params) do
Map.take(params, [:name, :email, :status])
end
end
À medida que o contexto cresce, divida-o em submódulos:
MyApp.Accounts
MyApp.Accounts.Configuration
MyApp.Accounts.Billing
Cada submódulo fica em seu próprio arquivo .ex, mantendo hierarquia clara e facilitando testes unitários por escopo.
Erro 2: “Enum hell” em grandes conjuntos de dados
O módulo Enum do Elixir é poderoso — Enum.filter/2 e Enum.map/2 executam incrivelmente rápido mesmo para 10 milhões de entradas. Mas encadear múltiplas iterações sobre grandes conjuntos em memória é um gargalo desnecessário:
# Padrão a evitar
permitted_params
|> Enum.map(&SomeModule.transform/1)
|> Enum.filter(&SomeModule.validate/1)
|> Enum.map(&SomeModule.convert/1)
|> Enum.sort(&SomeModule.compare/1)
A solução é transferir essas operações para o banco de dados via Ecto Query, onde índices e o planner do PostgreSQL fazem o trabalho pesado com muito mais eficiência. Quando precisar iterar em memória, faça isso sobre listas já paginadas — nunca sobre o conjunto completo.
Erro 3: deploy on-premise sem motivo técnico justificado
Infraestrutura on-premise raramente é justificável para aplicações web em estágio de escala. Cloud oferece backups automáticos, scaling configurável, ferramentas consolidadas de deploy e, na maioria dos cenários, segurança superior à que um time interno consegue garantir para uma máquina física dedicada. A complexidade de automatizar deploy em on-premise geralmente não compensa.
Práticas de código que fazem diferença em escala
Aplicações Umbrella para separar domínios
Quando o projeto cresce e começa a misturar domínios distintos, aplicações Umbrella oferecem uma separação inicial organizada sem precisar criar microsserviços imediatamente. Uma Umbrella é um mono-repo com múltiplos projetos Elixir que compartilham dependências mas têm releases separáveis.
mix new my_app --umbrella
A estrutura resultante permite separar o domínio do servidor HTTP:
my_app/
apps/
my_app/ # lógica de negócio
my_app_web/ # controllers, routers, LiveView
Atenção: se os projetos dentro da Umbrella precisarem de versões diferentes das mesmas dependências, isso é um sinal de que você tem serviços genuinamente distintos — e projetos separados fazem mais sentido.
Paginação sempre — sem exceção
Nenhum endpoint de API deve retornar todos os registros de uma tabela de uma vez. Em sistemas com qualquer volume significativo de dados, essa chamada simplesmente não chega ou trava o servidor.
Use bibliotecas como Paginator ou Scrivener para retornar sempre fatias paginadas:
# Com Paginator
def list_users(params) do
User
|> order_by([u], u.inserted_at)
|> Repo.paginate(params)
end
O front-end precisa ser capaz de consumir paginação — uma listagem completa sem tratamento sofre do mesmo problema no lado do cliente.
IDs não sequenciais (UUIDv4)
Usar IDs sequenciais (1, 2, 3…) em entidades expostas via API revela informação operacional sensível: quantidade de usuários, frequência de criação de registros, volume do sistema. Um ID como 2c9695b7-f899-4849-9d20-203a65e44474 não diz nada.
Para habilitar UUIDv4 por padrão em todas as entidades:
# config/config.exs
config :my_app,
ecto_repos: [MyApp.Repo],
generators: [binary_id: true]
O schema gerado fica:
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users" do
# ...
end
E a migration correspondente:
def change do
create table(:users, primary_key: false) do
add :id, :binary_id, primary_key: true
timestamps()
end
end
Em bancos distribuídos (multi-shard), UUIDs têm vantagem adicional: permitem inserção em múltiplos nós sem coordenação central, pois cada nó gera seus próprios identificadores únicos sem conflito.
Trafegar menos dados sempre que possível
A compressão gzip é um ganho imediato e trivial de configurar:
# lib/my_app_web/endpoint.ex
plug Plug.Static,
at: "/",
from: :my_app_web,
gzip: true,
only: ~w(css fonts images js)
# config/prod.exs
config :my_app_web, MyAppWeb.Endpoint,
http: [port: {:system, "PORT"}, compress: true]
Para reduzir dados trafegados nas respostas de API, GraphQL permite que o cliente especifique exatamente quais campos precisa — eliminando o problema de over-fetching em endpoints REST convencionais. Para comunicação entre serviços internos, gRPC reduz o payload ainda mais via serialização binária.
Delegar cálculos computacionalmente pesados
Conversão de vídeo, geração de relatórios complexos, processamento de imagens em lote — essas tarefas não devem viver na sua aplicação monolítica. O BEAM é excepcional para I/O e concorrência, não para CPU-bound tasks intensivas.
Estratégias práticas:
- Serviços auxiliares em Node.js ou Python para processamento específico
- Funções serverless (AWS Lambda, Google Cloud Functions) para tarefas de alta demanda episódica — não importa quantas requisições simultâneas cheguem, uma nova instância é criada automaticamente para cada uma
Tarefas assíncronas: nenhum servidor deve ficar parado esperando
Enviar e-mail de boas-vindas, processar uma integração pós-cadastro, gerar um relatório — nenhuma dessas operações deve bloquear a resposta HTTP. Todo processamento não instantâneo deve ser desacoplado do request-response:
O Elixir oferece opções nativas (GenServer, Task) e bibliotecas maduras como Oban para filas de jobs persistidos no banco, com retry automático, scheduling e monitoramento. Para integrações externas, Exq + Redis é outra alternativa consolidada.
Infraestrutura: o que configurar para aguentar carga
Cache, APM, CDN e WAF
Quatro ferramentas que aparecem em qualquer sistema escalado sério:
Cache (Redis, Memcached): para dados de acesso frequente e custo alto de recomputação. Disponível como serviço em todas as clouds principais.
APM (DataDog, New Relic, Elastic APM): essencial para identificar onde os requests demoram, quais endpoints geram mais erros e onde está o gargalo. Você não consegue otimizar o que não consegue medir.
CDN (Cloudflare, AWS CloudFront): fundamental para servir assets (imagens, CSS, JS, vídeos) com baixa latência globalmente. Instala pontos de presença próximos aos usuários finais.
WAF (Web Application Firewall): protege contra DDoS, bots maliciosos e tentativas de exploração. Disponível como serviço via Cloudflare, AWS WAF, Google Cloud Armor. Um sistema que escala também vira alvo — WAF é a primeira linha de defesa.
Scaling vertical e horizontal com Kubernetes
Scaling vertical aumenta os recursos (CPU, RAM) das máquinas que rodam sua aplicação. Em Kubernetes, isso é configurável por cluster com autoscaling vertical baseado em consumo.
Scaling horizontal aumenta o número de instâncias do seu serviço. O Kubernetes oferece o Horizontal Pod Autoscaler (HPA), que escala o número de pods automaticamente com base em métricas de CPU, memória ou métricas customizadas:
kubectl autoscale deployment my_app \
--cpu-percent=70 \
--min=2 \
--max=10
Tipo | O que escala | Melhor para |
|---|---|---|
Vertical | Poder de processamento de um nó | Picos previsíveis, baixa complexidade operacional |
Horizontal | Número de instâncias | Alta disponibilidade, distribuição de carga, zero downtime |
Para aplicações Elixir com clustering (múltiplos nós BEAM se comunicando), o scaling horizontal tem uma camada adicional: configurar o libcluster para que os nós se descubram automaticamente em topologias Kubernetes.
Construindo uma API com Phoenix 1.7+: o ponto de partida
Se você está começando um projeto de API REST com Phoenix, o caminho mais rápido é:
# Criar projeto sem HTML, com IDs binários
mix phx.new my_app --no-html --binary-id
# Gerar CRUD JSON
mix phx.gen.json Accounts User users email:string
# Criar banco e rodar migrations
mix ecto.create && mix ecto.migrate
O mix phx.gen.json gera automaticamente controller, schema, views e testes para o CRUD. Com –binary-id, todos os schemas nascem com UUIDv4 por padrão.
Autenticação JWT com Guardian
Para proteger endpoints com tokens JWT:
# mix.exs
{:bcrypt_elixir, "~> 3.0"},
{:guardian, "~> 2.3"}
O Guardian define dois módulos centrais:
# lib/my_app/accounts/guardian.ex
defmodule MyApp.Accounts.Guardian do
use Guardian, otp_app: :my_app
alias MyApp.Accounts
def subject_for_token(user, _claims), do: {:ok, to_string(user.id)}
def resource_from_claims(%{"sub" => id}) do
user = Accounts.get_user!(id)
{:ok, user}
rescue
Ecto.NoResultsError -> {:error, :resource_not_found}
end
end
No router, separe rotas públicas (criação de conta, login) de rotas protegidas com EnsureAuthenticated:
# Rotas públicas
scope "/api", MyAppWeb do
pipe_through [:api, :auth]
post "/users", UserController, :create
post "/login", SessionController, :login
end
# Rotas protegidas
scope "/api", MyAppWeb do
pipe_through [:api, :auth, :ensure_auth]
resources "/users", UserController, except: [:new, :edit]
end
Cobertura de testes com ExCoveralls
# mix.exs
{:excoveralls, "~> 0.16", only: :test}
Para gerar relatório HTML de cobertura:
mix coveralls.html
O relatório em cover/excoveralls.html mostra exatamente quais linhas e branches foram exercidos pelos testes — essencial para identificar partes críticas do código sem cobertura.
Escalar é um processo, não um estado
O que faz uma aplicação Elixir escalar não é um checklist fixo — é a capacidade do time de identificar gargalos e atacar o ponto certo no momento certo. Código bem arquitetado (contextos claros, sem Enum hell, sem lógica no controller), infraestrutura bem configurada (paginação, gzip, cache) e observabilidade (APM) formam a base.
Na Ateliware, construímos produtos em Elixir onde escalabilidade e custo de manutenção são requisitos reais — não hipóteses acadêmicas. Se o seu time está enfrentando gargalos em uma aplicação existente ou projetando a arquitetura de um novo produto, o Discovery guiado por IA pode ajudar a identificar os pontos críticos e definir o caminho técnico mais eficiente.



