Quando falamos sobre performance em software, a primeira pergunta costuma ser bastante previsível:
"Qual linguagem é mais rápida?"
Essa discussão normalmente é acompanhada por gráficos, benchmarks e comparações de tempo de execução entre diferentes linguagens de programação. C++, Rust, Go, Java, C#, Node.js... todas aparecem em rankings que medem quantos milhões de operações por segundo conseguem executar.
Esses estudos são importantes, mas também podem ser enganosos quando analisados isoladamente.
Afinal, uma função extremamente rápida executada em um ambiente controlado não representa, necessariamente, a realidade de um sistema em produção.
O verdadeiro desafio começa quando milhares — ou milhões — de usuários acessam simultaneamente uma aplicação distribuída.
É nesse momento que a definição de performance muda completamente.
Performance não é apenas velocidade
Em engenharia de software, velocidade é apenas uma das dimensões da performance.
Uma aplicação realmente eficiente precisa responder a diversas perguntas ao mesmo tempo.
- Ela continua rápida quando recebe milhares de requisições simultâneas?
- Seu consumo de memória permanece estável?
- O tempo de resposta continua previsível?
- O sistema permanece disponível diante de falhas?
- É capaz de crescer sem reescrever toda a arquitetura?
Essas características representam atributos arquiteturais muito mais relevantes do que simplesmente reduzir alguns milissegundos em uma operação específica.
Como destaca Martin Kleppmann em Designing Data-Intensive Applications:
"A confiabilidade, a escalabilidade e a manutenibilidade são tão importantes quanto a eficiência de processamento quando projetamos sistemas distribuídos."
Em outras palavras, um sistema rápido que deixa de responder sob carga não pode ser considerado um sistema performático.
A performance percebida pelo usuário
Existe um conceito bastante interessante chamado performance percebida.
Do ponto de vista do usuário, pouco importa se uma linguagem consegue executar um algoritmo em 2 ou 5 milissegundos.
O que realmente importa é a experiência entregue.
Imagine dois cenários.
Sistema A
- responde em 20 ms durante 95% do tempo;
- ocasionalmente demora 8 segundos;
- apresenta indisponibilidades durante picos.
Sistema B
- responde consistentemente entre 120 e 180 ms;
- praticamente nunca apresenta interrupções;
- mantém o mesmo comportamento mesmo com milhares de acessos.
Qual deles transmite mais confiança?
A maioria das pessoas escolheria o segundo.
Isso acontece porque consistência gera previsibilidade, e previsibilidade é um dos pilares da boa engenharia de software.
Sistemas reais vivem em ambientes imprevisíveis
Benchmarks normalmente são executados em ambientes extremamente controlados.
Na produção, entretanto, a realidade é completamente diferente.
Todos os dias sistemas enfrentam situações como:
- picos inesperados de acesso;
- indisponibilidade de serviços externos;
- latência de rede;
- contenção de recursos;
- falhas de hardware;
- erros humanos;
- atualizações de infraestrutura;
- degradação gradual de componentes.
Esses fatores fazem parte da rotina operacional.
Ou seja, falhas não são exceções. Elas são inevitáveis.
Essa simples constatação muda completamente a forma como arquitetamos software.
Em vez de perguntar:
"Como evitar qualquer falha?"
passamos a perguntar:
"Como garantir que o sistema continue funcionando mesmo quando uma falha acontecer?"
Essa mudança de perspectiva é um divisor de águas entre sistemas tradicionais e arquiteturas realmente resilientes.
A nova definição de performance
Durante muitos anos, a indústria associou performance exclusivamente ao poder computacional.
Mais CPU.
Mais memória.
Mais servidores.
Hoje sabemos que isso representa apenas uma parte da equação.
Uma definição mais completa poderia ser resumida da seguinte forma:
Performance é a capacidade de um sistema manter tempos de resposta previsíveis, utilizar recursos de forma eficiente, escalar sob carga e permanecer disponível mesmo diante de falhas.
Observe que velocidade continua presente.
Mas ela deixa de ocupar o centro da discussão.
Ela passa a fazer parte de um conjunto muito maior de atributos.
É exatamente aqui que Elixir começa a fazer sentido
Enquanto muitas linguagens evoluíram adicionando mecanismos para lidar com concorrência, paralelismo e tolerância a falhas, Elixir nasceu sobre uma plataforma cuja principal preocupação sempre foi resolver exatamente esses problemas.
Sua proposta nunca foi vencer benchmarks de execução de código.
Seu objetivo sempre foi permitir que sistemas permanecessem funcionando continuamente, mesmo em cenários de alta concorrência e falhas inevitáveis.
Essa filosofia muda completamente a forma de pensar arquitetura.
Em vez de construir aplicações tentando impedir qualquer erro, passamos a construir sistemas preparados para conviver com eles.
É essa mudança de paradigma que torna Elixir uma das linguagens mais interessantes da atualidade — não apenas por sua sintaxe ou produtividade, mas pela maneira como redefine o conceito de performance.
A história do Elixir começa muito antes do Elixir
Quando ouvimos falar em uma nova linguagem de programação, é natural imaginar que ela surgiu para competir com as tecnologias já existentes. Muitas vezes pensamos em novas sintaxes, novos frameworks ou promessas de maior produtividade. No entanto, a história do Elixir segue um caminho diferente. Para compreender por que essa linguagem desperta tanto interesse entre arquitetos de software, é preciso voltar algumas décadas, muito antes de sua criação.
Na década de 1980, a indústria de telecomunicações enfrentava um desafio que hoje parece quase impensável: construir sistemas que simplesmente não pudessem parar.
Uma central telefônica não podia ser reiniciada durante o horário comercial. Atualizações não poderiam interromper chamadas em andamento. Um erro em um único componente não deveria derrubar toda a infraestrutura. Cada segundo de indisponibilidade representava milhares de ligações interrompidas, prejuízos financeiros e perda de confiança em um serviço considerado essencial.
Naquele momento, o desafio não era construir o software mais rápido do mundo.
Era construir o software mais confiável do mundo.
Enquanto boa parte da indústria buscava velocidade de execução, a Ericsson buscava continuidade operacional.
Foi dentro desse contexto que pesquisadores da Ericsson iniciaram o desenvolvimento da linguagem Erlang e, principalmente, da máquina virtual que a executaria: a BEAM.
O objetivo era ambicioso.
Criar uma plataforma capaz de operar continuamente durante meses — ou até anos — sem necessidade de desligamentos programados, suportando milhões de conexões simultâneas e permitindo que falhas individuais fossem tratadas sem comprometer o restante do sistema.
Hoje chamamos essas características de alta disponibilidade, resiliência e tolerância a falhas.
Na época, elas eram simplesmente uma necessidade do negócio.
Um problema diferente exige uma arquitetura diferente
Grande parte das linguagens populares foi construída pensando em aplicações executadas de forma relativamente sequencial, onde a concorrência passou a ganhar importância conforme o hardware evoluiu e os processadores começaram a oferecer múltiplos núcleos.
A consequência foi que muitos recursos de concorrência foram adicionados posteriormente às linguagens.
Vieram então:
- Threads;
- Locks;
- Mutex;
- Semáforos;
- Pools de conexões;
- Promises;
- Futures;
- async/await;
- Bibliotecas para processamento paralelo.
Cada uma dessas soluções resolveu problemas importantes.
Mas também aumentou a complexidade do desenvolvimento de sistemas concorrentes.
Não por acaso, termos como race condition, deadlock, starvation e contenção de recursos tornaram-se parte do vocabulário cotidiano de engenheiros de software.
A BEAM seguiu uma direção completamente diferente.
Em vez de adaptar uma arquitetura existente para suportar concorrência, ela foi concebida desde o primeiro dia com a concorrência como princípio fundamental.
Essa decisão influenciaria todas as escolhas arquiteturais feitas nas décadas seguintes.
Trinta anos de evolução antes da primeira linha de Elixir
Em 2011, o desenvolvedor brasileiro José Valim apresentou ao mundo o Elixir.
À primeira vista, poderia parecer apenas mais uma nova linguagem funcional.
Na prática, entretanto, Elixir representava algo muito maior.
Em vez de criar uma máquina virtual do zero, José Valim tomou uma decisão estratégica: aproveitar toda a maturidade acumulada pela BEAM ao longo de mais de três décadas de uso em ambientes críticos.
Isso significa que Elixir não herdou apenas um ambiente de execução.
Herdou também uma filosofia de engenharia.
Uma filosofia construída em torno de conceitos como:
- isolamento entre processos;
- comunicação por troca de mensagens;
- recuperação automática de falhas;
- atualização de código sem interrupção do serviço;
- escalabilidade horizontal;
- distribuição transparente entre múltiplos nós.
Em outras palavras, Elixir nasceu sobre uma plataforma que já havia sido testada durante décadas em alguns dos ambientes mais exigentes da indústria.
A inovação do Elixir não foi reinventar a roda. Foi tornar uma arquitetura extraordinária mais acessível, moderna e produtiva para uma nova geração de desenvolvedores.
Mais do que uma linguagem, uma filosofia de engenharia
Talvez esse seja o maior equívoco cometido por quem conhece Elixir apenas superficialmente.
Seu diferencial não está na sintaxe elegante, nos pipes (|>) ou nas facilidades oferecidas pelo ecossistema.
Esses elementos contribuem para a experiência do desenvolvedor, mas não explicam por que empresas utilizam a plataforma em aplicações críticas.
O verdadeiro diferencial está na forma como ela encara os problemas da engenharia de software.
Enquanto muitas arquiteturas partem da premissa de que falhas devem ser evitadas a qualquer custo, a filosofia herdada da BEAM assume um princípio mais pragmático:
Falhas são inevitáveis. O papel da arquitetura não é fingir que elas não existem, mas garantir que seus impactos permaneçam isolados.
Essa mudança de perspectiva altera profundamente a forma de construir sistemas distribuídos.
Em vez de criar componentes gigantescos responsáveis por diversas tarefas, a plataforma incentiva a criação de milhares — ou até milhões — de pequenas unidades independentes que cooperam entre si. Se uma delas falhar, as demais continuam operando normalmente.
Essa ideia, que pode parecer simples, é justamente um dos pilares que permitiu à plataforma permanecer relevante por mais de trinta anos.
No próximo capítulo veremos como essa filosofia ganhou forma dentro da BEAM e por que muitos especialistas consideram essa máquina virtual uma das maiores obras da engenharia de software já produzidas.
A verdadeira revolução está na BEAM
Depois de conhecer a origem do Elixir, uma pergunta surge naturalmente:
O que torna essa plataforma tão diferente das demais?
A resposta está em um detalhe que muitos desenvolvedores sequer percebem durante os primeiros contatos com a linguagem.
O maior diferencial do Elixir não é a linguagem em si.
Também não é sua sintaxe funcional.
Nem seus operadores.
Nem mesmo o famoso operador pipe (|>).
A verdadeira revolução acontece abaixo do código que escrevemos.
Ela acontece dentro da BEAM, a máquina virtual responsável por executar aplicações escritas em Erlang e Elixir.
O que é uma máquina virtual?
Quando escrevemos uma aplicação em Elixir, nosso código não conversa diretamente com o sistema operacional.
Existe uma camada intermediária responsável por interpretar, organizar, distribuir e executar tudo o que acontece na aplicação.
De forma simplificada, a arquitetura pode ser representada assim:
Essa organização pode parecer semelhante à de outras plataformas, como a JVM (Java Virtual Machine) ou o CLR (.NET Common Language Runtime). Afinal, todas oferecem uma camada de abstração entre a aplicação e o sistema operacional.
A diferença está no propósito para o qual cada uma delas foi concebida.
Enquanto muitas máquinas virtuais priorizam desempenho computacional e gerenciamento de memória, a BEAM foi projetada para responder a perguntas muito diferentes.
Como executar milhões de atividades simultaneamente?
Como impedir que uma falha local interrompa todo o sistema?
Como atualizar aplicações sem desligá-las?
Como distribuir processamento entre diversos servidores praticamente de forma transparente?
Essas perguntas moldaram toda a arquitetura da plataforma.
A BEAM não tenta ser a mais rápida
Esse talvez seja um dos pontos mais contraintuitivos para quem está acostumado a comparar benchmarks.
A BEAM não foi construída para vencer testes sintéticos de velocidade.
Seu objetivo sempre foi outro.
Ela busca maximizar características como:
- concorrência massiva;
- baixa latência sob carga;
- disponibilidade contínua;
- isolamento entre componentes;
- recuperação automática de falhas;
- escalabilidade distribuída.
Essa decisão faz com que alguns algoritmos puramente computacionais sejam executados mais rapidamente em linguagens como C++, Rust ou Go.
Por outro lado, quando o problema deixa de ser um algoritmo isolado e passa a envolver centenas de milhares de conexões simultâneas, comunicação entre serviços e tolerância a falhas, a BEAM passa a demonstrar seu verdadeiro diferencial.
A pergunta deixa de ser "qual linguagem executa uma função mais rápido?" e passa a ser "qual arquitetura continua funcionando quando tudo fica complexo?".
O scheduler: um dos grandes segredos da plataforma
Um dos componentes mais sofisticados da BEAM é seu scheduler.
Em sistemas tradicionais, múltiplas threads competem constantemente pelo tempo de processamento disponível nos núcleos da CPU. Essa disputa gera trocas de contexto (context switching), contenção de recursos e desperdício de processamento.
A BEAM adota uma estratégia diferente.
Ela mantém diversos schedulers, normalmente um para cada núcleo disponível da máquina, responsáveis por distribuir milhões de processos leves de maneira equilibrada.
Em vez de permitir que um único processo monopolize a CPU, o scheduler concede pequenas fatias de execução para cada um deles, promovendo um modelo cooperativo extremamente eficiente.
Isso oferece duas vantagens importantes.
A primeira é a previsibilidade.
Mesmo sob cargas elevadas, dificilmente um único processo conseguirá bloquear toda a aplicação.
A segunda é o balanceamento automático.
À medida que alguns schedulers ficam sobrecarregados e outros permanecem ociosos, a própria BEAM redistribui processos internamente para utilizar melhor os recursos disponíveis.
Todo esse mecanismo acontece de forma transparente para o desenvolvedor.
Pequenas decisões que produzem grandes resultados
Grande parte da robustez da BEAM não depende de uma única tecnologia revolucionária.
Ela surge da combinação de diversas decisões arquiteturais cuidadosamente integradas.
Entre elas podemos destacar:
- Processos extremamente leves, capazes de existir aos milhões;
- Garbage Collector individual, evitando pausas globais da aplicação;
- Comunicação por mensagens, eliminando memória compartilhada entre processos;
- Escalonamento automático, distribuindo processamento entre todos os núcleos da máquina;
- Distribuição nativa, permitindo que diversos servidores cooperem como se fossem um único sistema;
- Hot Code Upgrade, possibilitando atualizar aplicações em produção sem interromper sua execução.
Cada um desses elementos resolve um problema específico.
Juntos, eles constroem uma plataforma cujo foco não é apenas executar código, mas manter sistemas funcionando continuamente.
Uma arquitetura pensada para décadas
Talvez o aspecto mais impressionante da BEAM seja sua longevidade.
Embora tenha sido concebida para resolver problemas de telecomunicações na década de 1980, seus princípios continuam extremamente atuais.
Hoje vivemos em um cenário dominado por:
- microsserviços;
- computação em nuvem;
- aplicações distribuídas;
- IoT;
- sistemas financeiros em tempo real;
- plataformas de streaming;
- comunicação instantânea.
Curiosamente, todos esses ambientes enfrentam desafios muito semelhantes aos das antigas centrais telefônicas: alta concorrência, disponibilidade contínua e tolerância a falhas.
Isso explica por que uma tecnologia desenvolvida há mais de trinta anos permanece tão relevante.
Ela não foi construída para acompanhar uma tendência tecnológica.
Ela foi construída para resolver um problema que continua existindo.
As melhores arquiteturas não envelhecem rapidamente, porque atacam problemas fundamentais da computação, e não apenas limitações momentâneas de hardware ou linguagem.
No próximo capítulo veremos um dos conceitos mais fascinantes da BEAM: seus processos leves. Eles representam uma mudança radical na forma como pensamos concorrência e ajudam a explicar por que aplicações Elixir conseguem lidar com milhões de atividades simultaneamente de maneira tão eficiente
Processos: o conceito que torna a concorrência quase invisível
Depois de compreender a arquitetura da BEAM, chegamos ao conceito que talvez seja o maior responsável pela reputação do Elixir em sistemas altamente concorrentes.
Não é o pattern matching.
Não é a programação funcional.
Nem mesmo o OTP.
O verdadeiro protagonista é um conceito aparentemente simples:
Os processos da BEAM.
Entretanto, existe um detalhe extremamente importante.
Quando falamos em "processo" dentro do Elixir, não estamos falando do mesmo processo do sistema operacional.
E é justamente essa diferença que muda completamente a maneira como pensamos software.
O problema da concorrência tradicional
Antes de entender como a BEAM resolve a concorrência, vale refletir sobre como normalmente fazemos isso em outras plataformas.
Imagine um servidor web recebendo milhares de requisições simultaneamente.
Uma abordagem bastante comum seria criar uma thread para cada operação.
- Requisição 1 → Thread 1
- Requisição 2 → Thread 2
- Requisição 3 → Thread 3
À primeira vista, parece simples.
O problema aparece quando a quantidade cresce.
Cada thread possui:
- pilha de memória própria;
- contexto de execução;
- gerenciamento pelo sistema operacional;
- custo para criação;
- custo para destruição;
- custo para troca de contexto (context switching).
Quanto maior o número de threads, maior também o custo operacional.
Em determinado momento, o sistema deixa de gastar tempo processando regras de negócio e passa a gastar tempo administrando suas próprias threads.
Em sistemas muito concorrentes, gerenciar concorrência pode consumir mais recursos do que executar a aplicação.
O processo da BEAM é outra categoria
Na BEAM, um processo não possui relação direta com um processo do Linux ou do Windows.
Ele é uma estrutura extremamente leve criada pela própria máquina virtual.
Enquanto um processo do sistema operacional pode consumir megabytes de memória, um processo da BEAM nasce ocupando apenas alguns kilobytes.
Essa diferença muda completamente a escala da aplicação.
Comparando:
- Sistema operacional — gerenciado pelo SO, alto custo de criação, consome mais memória, milhares podem ser caros, compartilha recursos do SO.
- Processo da BEAM — gerenciado pela BEAM, criação extremamente barata, consome poucos KB, milhões são possíveis, vive isolado.
Em vez de pensar:
"Será que posso criar mais uma thread?"
No Elixir a pergunta normalmente é:
"Por que eu ainda não criei outro processo?"
Essa inversão de pensamento é um dos aspectos mais interessantes da plataforma.
Um processo para cada responsabilidade
A filosofia da BEAM incentiva dividir grandes problemas em inúmeras pequenas responsabilidades.
Imagine um sistema de atendimento online.
Em muitas arquiteturas tradicionais poderíamos ter um único serviço responsável por gerenciar todas as conexões.
Na BEAM, é comum encontrarmos algo semelhante a isto:
- Cliente 1 → Processo 1
- Cliente 2 → Processo 2
- Cliente 3 → Processo 3
- ...
- Cliente 1.000.000 → Processo 1.000.000
Cada usuário possui seu próprio processo.
Cada conexão possui seu próprio estado.
Cada operação acontece de forma independente.
Não existe um grande controlador central tentando coordenar tudo.
Essa abordagem reduz significativamente a complexidade do sistema.
Isolamento muda tudo
Talvez a característica mais importante dos processos da BEAM seja o isolamento.
Cada processo possui:
- sua própria memória;
- seu próprio estado;
- sua própria pilha de execução;
- sua própria fila de mensagens (mailbox);
- seu próprio ciclo de vida.
Nenhum processo consegue acessar diretamente a memória de outro.
Isso elimina uma enorme categoria de problemas encontrados em sistemas concorrentes.
Entre eles:
- race conditions;
- corrupção de memória;
- bloqueios acidentais;
- dependências invisíveis entre componentes.
Em vez de compartilhar dados, processos apenas compartilham mensagens.
Essa decisão arquitetural parece pequena.
Na prática, ela muda completamente o comportamento do sistema.
Pequenos processos, grandes sistemas
Existe uma frase bastante conhecida dentro da comunidade Erlang.
"Se um processo está fazendo muitas coisas, provavelmente ele deveria ser dividido em vários processos menores."
Esse princípio segue uma lógica semelhante à encontrada nos microsserviços.
Pequenos componentes.
Pouca responsabilidade.
Baixo acoplamento.
Alta coesão.
A diferença é que, em vez de distribuir responsabilidades entre dezenas de servidores, a BEAM aplica essa filosofia internamente, dentro da própria máquina virtual.
É como se cada aplicação fosse composta por milhares de pequenos serviços cooperando entre si.
Concorrência deixa de ser uma preocupação constante
Uma consequência interessante dessa arquitetura é que o desenvolvedor passa a pensar menos em gerenciamento de concorrência.
Não é necessário decidir cuidadosamente quais objetos devem compartilhar memória.
Nem controlar manualmente múltiplos bloqueios.
Nem criar estratégias complexas de sincronização.
Grande parte dessas preocupações simplesmente desaparece porque a arquitetura impede que elas aconteçam.
A melhor maneira de resolver um problema de concorrência, muitas vezes, é impedir que ele exista.
Essa talvez seja uma das maiores contribuições da BEAM para a engenharia de software moderna.
Ela não oferece apenas ferramentas para lidar com concorrência.
Ela redefine completamente a forma como sistemas concorrentes são construídos.
No próximo capítulo veremos justamente como esses processos conseguem cooperar entre si sem compartilhar memória, utilizando um modelo baseado exclusivamente em troca de mensagens.
Compartilhar memória ou compartilhar mensagens?
Até este ponto, compreendemos que a BEAM utiliza processos extremamente leves e isolados para executar milhões de tarefas simultaneamente. No entanto, ainda existe uma pergunta fundamental:
Se cada processo possui sua própria memória, como eles trabalham juntos?
Essa questão nos leva a uma das maiores diferenças entre a arquitetura da BEAM e a maioria das plataformas modernas.
Não se trata apenas de uma implementação diferente.
Trata-se de uma filosofia completamente diferente de construir software concorrente.
Dois modelos. Dois mundos.
Desde os primeiros sistemas operacionais multitarefa, a forma mais comum de permitir que diferentes partes de uma aplicação cooperassem foi através do compartilhamento de memória.
Diversas threads acessam uma mesma estrutura de dados.
Cada uma lê.
Cada uma escreve.
Cada uma modifica o estado global da aplicação.
Visualmente, esse modelo pode ser representado da seguinte forma:
Thread A │ ▼ Memória Compartilhada ▲ │ Thread B ▲ │ Thread C
À primeira vista, parece uma solução bastante natural.
Se todos precisam acessar os mesmos dados, basta disponibilizá-los em um único lugar.
O problema é que, à medida que mais componentes passam a compartilhar essa memória, surgem novos desafios.
Quem pode escrever primeiro?
Como impedir que duas threads alterem o mesmo dado simultaneamente?
Como garantir que uma leitura não aconteça enquanto outra thread está modificando aquele objeto?
Essas perguntas deram origem a diversos mecanismos de sincronização presentes praticamente em todas as linguagens modernas.
- Mutexes
- Locks
- Semáforos
- Monitores
- Read/Write Locks
- Variáveis atômicas
Todos eles existem para resolver um único problema:
Evitar que o compartilhamento de memória produza inconsistências.
A complexidade cresce silenciosamente
Embora esses mecanismos sejam extremamente importantes, eles também aumentam significativamente a complexidade do sistema.
Quanto maior o número de componentes compartilhando estado, maior tende a ser o esforço necessário para coordená-los.
Consequentemente, começam a surgir problemas conhecidos por qualquer engenheiro de software que já trabalhou com sistemas concorrentes.
- Race Conditions, quando duas execuções alteram o mesmo dado simultaneamente.
- Deadlocks, quando processos ficam esperando indefinidamente uns pelos outros.
- Starvation, quando determinadas tarefas deixam de receber tempo de processamento.
- Contention, quando múltiplas threads disputam constantemente os mesmos recursos.
Esses problemas nem sempre aparecem durante o desenvolvimento.
Na maioria das vezes, eles surgem apenas sob alta concorrência, em produção, tornando sua reprodução extremamente difícil.
Grande parte da complexidade da concorrência tradicional nasce da tentativa de coordenar o acesso ao mesmo estado compartilhado.
A decisão arquitetural da BEAM
A BEAM parte de uma pergunta diferente.
Em vez de perguntar:
"Como sincronizar o acesso à memória compartilhada?"
ela pergunta:
"E se simplesmente não existisse memória compartilhada?"
Essa mudança parece pequena.
Na prática, ela elimina uma categoria inteira de problemas.
Cada processo possui:
- sua própria memória;
- seu próprio estado;
- sua própria fila de mensagens;
- seu próprio ciclo de vida.
Nenhum processo consegue modificar diretamente outro processo.
Não existe acesso direto.
Não existe referência compartilhada.
Não existe escrita concorrente sobre o mesmo objeto.
Comunicação por mensagens
Se processos não compartilham memória, como colaboram?
A resposta é simples.
Eles enviam mensagens.
┌────────────┐ │ Processo A │ └─────┬──────┘ │ │ mensagem ▼ ┌────────────┐ │ Processo B │ └─────┬──────┘ │ │ mensagem ▼ ┌────────────┐ │ Processo C │ └────────────┘
Cada processo possui uma mailbox, uma fila privada onde mensagens são recebidas.
O processo decide quando ler essas mensagens e como reagir a elas.
Isso produz uma consequência extremamente interessante.
O emissor não precisa conhecer a implementação interna do destinatário.
Ele apenas envia uma mensagem.
O receptor decide o que fazer.
Esse desacoplamento torna o sistema muito mais previsível e flexível.
O Actor Model na prática
Esse modelo de comunicação é conhecido como Actor Model, um paradigma proposto por Carl Hewitt na década de 1970 e amplamente adotado pelo ecossistema Erlang/Elixir.
Segundo esse modelo, cada ator (ou processo):
- possui estado privado;
- executa de forma independente;
- comunica-se apenas por mensagens;
- pode criar novos atores;
- pode alterar seu próprio estado;
- nunca acessa diretamente o estado de outro ator.
Essa arquitetura aproxima muito mais o software da forma como organizações humanas funcionam.
Imagine uma empresa.
Um departamento financeiro não altera diretamente os documentos do RH.
Ele envia uma solicitação.
O RH processa a informação e responde.
Cada setor mantém autonomia sobre seus próprios dados.
A comunicação acontece por troca de informações, não por acesso irrestrito aos documentos internos de outro departamento.
A BEAM aplica exatamente essa filosofia aos seus processos.
Pensar em mensagens muda a arquitetura
Quando começamos a desenvolver aplicações nesse modelo, uma mudança interessante acontece.
Deixamos de pensar em perguntas como:
- Qual objeto preciso bloquear?
- Quem possui acesso a essa variável?
- Como sincronizar esse estado?
E passamos a pensar em algo muito mais próximo dos domínios de negócio.
- Que mensagem representa esta ação?
- Quem deve processá-la?
- Quem precisa ser informado do resultado?
Essa mudança reduz significativamente o acoplamento entre componentes e facilita a evolução da arquitetura ao longo do tempo.
No Elixir, o foco deixa de ser "quem possui os dados" e passa a ser "quem é responsável por processar esta mensagem".
Uma frase que resume toda essa filosofia
Existe uma frase frequentemente atribuída a Joe Armstrong que sintetiza perfeitamente esse conceito:
"Don't communicate by sharing memory; share memory by communicating."
Embora originalmente popularizada também pela comunidade da linguagem Go, essa ideia representa um dos pilares fundamentais da arquitetura da BEAM.
Ela resume uma mudança de paradigma que influencia diretamente concorrência, escalabilidade, resiliência e simplicidade arquitetural.
No próximo capítulo veremos como essa filosofia leva naturalmente ao conceito mais famoso do ecossistema Erlang/Elixir: "Let it crash", uma estratégia que transforma falhas em eventos esperados e controlados, em vez de exceções catastróficas.
"Let it crash": por que deixar um processo falhar pode ser a decisão mais segura
Poucas expressões despertam tanta curiosidade — e tantos mal-entendidos — quanto "Let it crash".
Para quem vem de linguagens orientadas a objetos ou de arquiteturas tradicionais, a frase pode soar como uma recomendação irresponsável.
"Como assim deixar o sistema falhar?"
Na realidade, a filosofia do Elixir e da BEAM propõe exatamente o oposto.
Ela não incentiva que sistemas falhem.
Ela incentiva que falhas sejam pequenas, isoladas e rapidamente recuperadas.
Essa diferença é sutil, mas muda completamente a forma como construímos software.
O reflexo natural de todo desenvolvedor
Desde os primeiros contatos com programação, aprendemos que exceções devem ser tratadas.
Escrevemos blocos como:
// Java
try {
processarPagamento();
} catch (Exception e) {
log.error(e);
}
// C#
try
{
ProcessarPedido();
}
catch(Exception ex)
{
// tratamento
}
A ideia parece bastante lógica.
Capturar todos os erros possíveis.
Impedir que a aplicação seja interrompida.
Continuar executando.
Com o passar dos anos, muitos sistemas passam a possuir dezenas — ou centenas — de blocos de tratamento de exceção espalhados pela aplicação.
O problema é que nem sempre sabemos como recuperar um estado inconsistente.
Em muitos casos, apenas escondemos o erro.
O sistema continua funcionando...
Mas agora em um estado desconhecido.
Uma exceção capturada não significa, necessariamente, que o sistema voltou a ser consistente.
Nem todo erro pode ser recuperado
Imagine um processo responsável por calcular a folha de pagamento de uma empresa.
Durante o processamento, uma informação essencial chega corrompida.
A pergunta é simples.
O que fazer?
Algumas possibilidades seriam:
- ignorar o erro;
- tentar continuar;
- utilizar valores padrão;
- registrar um log e seguir executando.
Nenhuma dessas opções garante que o resultado final estará correto.
Em muitos cenários, continuar executando é mais perigoso do que interromper aquela operação.
A filosofia da BEAM parte justamente dessa constatação.
Se um processo entrou em um estado inconsistente e não consegue garantir seu comportamento, talvez a decisão mais segura seja encerrá-lo imediatamente.
Falhar não significa derrubar o sistema
Esse é o ponto mais importante de todo o capítulo.
Quando um processo da BEAM falha:
A aplicação inteira não falha.
Falha apenas aquele processo.
Imagine uma aplicação composta por centenas de milhares de processos independentes.
- Processo A ✅
- Processo B ✅
- Processo C ❌
- Processo D ✅
- Processo E ✅
A falha do Processo C não interrompe os demais.
Os outros continuam executando normalmente.
Isso só é possível porque vimos, no capítulo anterior, que cada processo possui:
- memória própria;
- estado próprio;
- isolamento completo.
Como não existe memória compartilhada, um processo não consegue corromper diretamente outro.
Quem coloca tudo de pé novamente?
Aqui entra um dos maiores diferenciais da plataforma.
Os Supervisors.
Em vez de envolver cada operação em dezenas de blocos de tratamento de erro, a BEAM adota outra estratégia.
Cada processo importante possui alguém responsável por supervisioná-lo.
Quando um processo termina inesperadamente, o Supervisor detecta imediatamente a falha.
Dependendo da estratégia configurada, ele pode simplesmente criar uma nova instância daquele processo.
Supervisor -> Processo C -> Erro -> Reinício automático -> Processo C (novo)
Para o restante do sistema, praticamente nada mudou.
Supervisão em árvore
Uma característica interessante do OTP é que Supervisors também podem possuir Supervisors.
Isso cria uma estrutura hierárquica conhecida como Supervisor Tree.
Supervisor Principal
├── Supervisor Financeiro │ ├── Processo 1 │ ├── Processo 2 │ ├── Supervisor Logística │ ├── Processo 3 │ ├── Processo 4 │ └── Supervisor Atendimento ├── Processo 5 ├── Processo 6
Essa organização lembra bastante a estrutura de uma empresa.
Cada gestor cuida apenas de sua equipe.
Problemas locais são resolvidos localmente.
Somente situações realmente excepcionais sobem para níveis superiores.
Esse modelo reduz significativamente o impacto das falhas.
Falhas deixam de ser eventos extraordinários
Grande parte da engenharia de software tradicional trata erros como eventos excepcionais.
Na filosofia da BEAM acontece exatamente o contrário.
Falhas fazem parte do funcionamento normal do sistema.
Servidores reiniciam.
Redes falham.
Discos apresentam problemas.
Serviços externos ficam indisponíveis.
Usuários enviam dados inválidos.
Tudo isso inevitavelmente acontecerá.
A diferença está em como reagimos.
Em vez de tentar impedir qualquer erro, projetamos aplicações capazes de continuar operando apesar deles.
Resiliência não significa ausência de falhas. Significa capacidade de continuar funcionando mesmo quando elas acontecem.
Por que isso muda a arquitetura?
Quando sabemos que processos podem falhar e ser recriados automaticamente, começamos a construir componentes muito menores.
Cada processo possui uma responsabilidade clara.
Pouco estado.
Pouco acoplamento.
Poucas dependências.
Quanto menor o processo, mais barato é descartá-lo e criar outro.
Essa característica aproxima bastante o software de sistemas biológicos.
Nosso corpo perde milhões de células todos os dias.
Isso não representa um problema.
Novas células surgem continuamente.
O organismo permanece funcionando.
A BEAM aplica exatamente essa filosofia aos sistemas distribuídos.
"Let it crash" não significa negligência
Existe uma interpretação equivocada bastante comum.
Algumas pessoas entendem que "Let it crash" significa não validar entradas, ignorar exceções ou deixar erros acontecerem livremente.
Nada poderia estar mais distante da realidade.
A filosofia incentiva:
- validar dados de entrada;
- tratar erros recuperáveis;
- registrar informações importantes;
- projetar processos pequenos;
- supervisionar componentes críticos.
O que ela evita é manter vivo um processo cujo estado interno já não pode mais ser considerado confiável.
Às vezes, reiniciar é mais seguro do que continuar.
Essa talvez seja uma das maiores diferenças entre arquiteturas orientadas à recuperação manual e arquiteturas orientadas à resiliência.
No próximo capítulo veremos como essa estratégia ganha forma através do OTP, um conjunto de abstrações que transforma conceitos como supervisão, concorrência e distribuição em blocos reutilizáveis para construção de sistemas robustos.
OTP: o framework que redefiniu a engenharia de sistemas distribuídos
Ao estudar Elixir, é comum ouvir frases como:
"O verdadeiro diferencial não é o Elixir. É o OTP."
Para quem está começando, essa afirmação pode parecer exagerada.
Afinal, o que exatamente é o OTP?
Seria um framework?
Uma biblioteca?
Um conjunto de padrões?
A resposta é curiosa.
OTP é um pouco de tudo isso ao mesmo tempo.
Mais do que um conjunto de ferramentas, o Open Telecom Platform (OTP) representa décadas de experiência transformadas em componentes reutilizáveis para construção de sistemas distribuídos, concorrentes e resilientes.
Enquanto muitas linguagens fornecem apenas mecanismos básicos para concorrência, o OTP entrega uma arquitetura completa para organizar aplicações complexas.
O que é o OTP?
Embora o nome Open Telecom Platform remeta ao universo das telecomunicações, hoje ele representa um dos frameworks mais sofisticados da engenharia de software.
Seu objetivo é simples:
Transformar padrões arquiteturais recorrentes em componentes prontos, testados e amplamente utilizados em produção.
Em vez de cada equipe reinventar a maneira de criar processos, supervisioná-los, organizar estados e distribuir mensagens, o OTP oferece essas soluções como blocos reutilizáveis.
Essa abordagem reduz erros, padroniza projetos e torna sistemas mais previsíveis.
A diferença entre uma linguagem e uma plataforma
Grande parte das linguagens oferece apenas recursos para escrever código.
O restante fica sob responsabilidade do desenvolvedor.
Precisamos decidir:
- como iniciar serviços;
- como organizar processos;
- como tratar falhas;
- como reiniciar componentes;
- como compartilhar estado;
- como coordenar concorrência.
No ecossistema Elixir, boa parte dessas decisões já possui padrões consolidados pelo OTP.
Em vez de começar sempre do zero, desenvolvemos aplicações utilizando componentes que já nasceram preparados para ambientes de produção.
Essa talvez seja uma das maiores vantagens da plataforma.
O OTP não fornece apenas APIs. Ele fornece uma maneira de pensar sistemas distribuídos.
Os principais blocos do OTP
Embora o OTP possua dezenas de módulos, alguns componentes aparecem em praticamente qualquer aplicação Elixir.
GenServer
O GenServer é provavelmente o componente mais conhecido do ecossistema.
Ele representa um processo que mantém estado interno e responde a mensagens.
Visualmente:
Cliente
↓
Mensagem
↓
GenServer
↓
Estado interno
Um GenServer pode representar praticamente qualquer entidade da aplicação.
Por exemplo:
- um carrinho de compras;
- uma conexão WebSocket;
- um cache;
- uma fila;
- um dispositivo IoT;
- um jogador conectado a um servidor de jogos.
Cada um possui seu próprio estado.
Cada um processa suas próprias mensagens.
Supervisor
Já conhecemos os Supervisors no capítulo anterior.
Sua responsabilidade é monitorar processos e decidir como agir quando algum deles falha.
Ele não executa regras de negócio.
Seu trabalho é exclusivamente manter a aplicação saudável.
Supervisor
├── GenServer A
├── GenServer B
└── GenServer C
DynamicSupervisor
Nem todos os processos existem desde o início da aplicação.
Imagine um servidor de chat.
Cada novo usuário conectado pode gerar um processo específico.
Esses processos surgem dinamicamente.
Também desaparecem dinamicamente.
Quem administra esse ciclo de vida é o DynamicSupervisor.
Novo Cliente
↓
Novo Processo
↓
DynamicSupervisor
↓
Gerenciamento automático
Task
Nem toda operação precisa manter estado.
Algumas tarefas apenas executam uma ação e terminam.
Por exemplo:
- enviar um e-mail;
- gerar um PDF;
- processar uma imagem;
- consumir uma API externa.
Nesses casos, o OTP oferece o módulo Task, criado justamente para representar trabalhos temporários.
Após concluir sua execução, o processo simplesmente desaparece.
Agent
Em algumas situações precisamos apenas armazenar um pequeno estado compartilhado de maneira simples.
Para isso existe o Agent.
Ele encapsula um estado interno e disponibiliza operações básicas de leitura e atualização.
Embora muitos projetos utilizem GenServer diretamente, Agents continuam sendo uma excelente alternativa para cenários simples.
Pequenos blocos, grandes arquiteturas
O aspecto mais interessante do OTP é perceber que aplicações complexas normalmente surgem da combinação desses poucos componentes.
Imagine um sistema bancário simplificado.
Application
├── Supervisor Principal
│
├── Supervisor Contas
│ ├── Conta 1001 (GenServer)
│ ├── Conta 1002 (GenServer)
│ ├── Conta 1003 (GenServer)
│
├── DynamicSupervisor Sessões
│ ├── Cliente A
│ ├── Cliente B
│
├── Task Supervisor
│ ├── Envio de E-mails
│ ├── Geração de Extratos
│
└── Cache (Agent)
Observe que praticamente toda a arquitetura foi construída utilizando apenas alguns componentes básicos.
Essa simplicidade é intencional.
Convenções reduzem complexidade
Existe uma frase bastante conhecida na engenharia de software.
"Convention over Configuration."
O OTP aplica exatamente essa ideia.
Em vez de permitir dezenas de formas diferentes para resolver o mesmo problema, ele incentiva um pequeno conjunto de padrões bem estabelecidos.
Como consequência:
- projetos tornam-se mais previsíveis;
- novos desenvolvedores aprendem mais rapidamente;
- manutenção fica mais simples;
- arquiteturas tornam-se mais consistentes.
Quando um engenheiro experiente abre um projeto Elixir, normalmente consegue compreender sua estrutura em poucos minutos.
Grande parte dessa previsibilidade vem justamente das convenções estabelecidas pelo OTP.
Por que isso é tão importante?
Ao observar o mercado, percebemos que muitos frameworks surgiram para facilitar o desenvolvimento web.
Outros focaram em ORMs.
Outros em interfaces gráficas.
O OTP seguiu um caminho diferente.
Seu objetivo sempre foi facilitar a construção de sistemas confiáveis.
Essa diferença de propósito explica por que ele permanece relevante após décadas.
Enquanto tecnologias mudam rapidamente, os desafios relacionados à concorrência, disponibilidade e resiliência continuam praticamente os mesmos.
O OTP não resolve apenas problemas da linguagem.
Ele resolve problemas da engenharia de software.
Um dos frameworks mais influentes — e menos conhecidos
Curiosamente, milhares de desenvolvedores utilizam conceitos inspirados no OTP sem sequer perceber.
Estruturas de supervisão.
Workers.
Processos isolados.
Pools de execução.
Sistemas orientados a mensagens.
Todos esses padrões influenciaram diversas tecnologias modernas.
O OTP apenas os reuniu em uma plataforma extremamente consistente.
Talvez esse seja seu maior legado.
Mais do que um framework, o OTP representa décadas de conhecimento acumulado sobre como construir software que continua funcionando quando o mundo real acontece.
No próximo capítulo exploraremos uma confusão bastante comum entre desenvolvedores: concorrência não é paralelismo. Entender essa diferença é fundamental para compreender por que a BEAM consegue escalar tão bem em ambientes multicore e distribuídos.
Capítulo 8 — Concorrência não é paralelismo: uma das confusões mais comuns da engenharia de software
Ao falar sobre Elixir, é comum encontrar afirmações como:
"Elixir é extremamente rápido porque executa tudo em paralelo."
Embora a frase pareça correta, ela mistura dois conceitos que, apesar de relacionados, possuem significados bastante diferentes.
Concorrência e paralelismo não são sinônimos.
Entender essa diferença é essencial para compreender por que a BEAM consegue escalar tão bem e por que ela foi projetada de maneira tão diferente das plataformas tradicionais.
Curiosamente, essa distinção não nasceu com Elixir. Ela é um dos fundamentos da computação moderna e influencia desde sistemas operacionais até arquiteturas distribuídas.
O que é concorrência?
Concorrência diz respeito à capacidade de lidar com várias tarefas ao mesmo tempo, ainda que elas não estejam sendo executadas simultaneamente.
O foco está na organização do trabalho.
Imagine um garçom atendendo quatro mesas.
Ele não consegue conversar com todos os clientes exatamente no mesmo instante.
Mas consegue alternar rapidamente entre eles.
Recebe um pedido.
Anota.
Vai até outra mesa.
Entrega uma bebida.
Retorna.
Recebe outro pedido.
Para quem observa de fora, parece que tudo acontece simultaneamente.
Na prática, ele apenas alterna sua atenção entre diversas tarefas.
Esse é o conceito de concorrência.
Tempo →
Tarefa A ███ ███ ███
Tarefa B ███ ███
Tarefa C ███ ███
Nenhuma tarefa monopoliza completamente o tempo disponível.
O que é paralelismo?
O paralelismo, por outro lado, exige que múltiplas tarefas sejam executadas literalmente ao mesmo tempo.
Isso normalmente depende da existência de múltiplos núcleos de processamento (multi-core).
Imagine agora quatro cozinheiros trabalhando na mesma cozinha.
Cada um prepara um prato simultaneamente.
Todos estão produzindo ao mesmo tempo.
Esse é o paralelismo.
CPU 1 → Tarefa A
CPU 2 → Tarefa B
CPU 3 → Tarefa C
CPU 4 → Tarefa D
Não existe alternância.
Existe execução simultânea.
Concorrência pode existir sem paralelismo
Esse é um detalhe frequentemente ignorado.
Mesmo um computador com apenas um núcleo consegue executar aplicações concorrentes.
Ele faz isso alternando rapidamente entre diferentes tarefas.
Esse comportamento é conhecido como time slicing.
O sistema operacional (ou, no caso da BEAM, o scheduler) distribui pequenas fatias de tempo para cada processo.
A alternância acontece tão rapidamente que o usuário tem a impressão de simultaneidade.
Em outras palavras:
Concorrência é um problema de organização. Paralelismo é um problema de execução física.
A genialidade da BEAM
A BEAM foi projetada para tratar esses dois conceitos de maneira independente.
Primeiro, ela organiza milhões de processos concorrentes.
Depois, distribui esses processos pelos núcleos disponíveis da máquina.
O desenvolvedor normalmente não precisa decidir qual processo será executado em qual núcleo.
Essa responsabilidade pertence ao scheduler.
Ele monitora constantemente a carga da aplicação e redistribui processos quando necessário.
Balanceamento automático
Imagine uma situação onde um núcleo está sobrecarregado enquanto outro permanece praticamente ocioso.
Em muitas plataformas, cabe ao desenvolvedor criar estratégias para equilibrar essa distribuição.
Na BEAM, esse trabalho acontece automaticamente.
Os schedulers cooperam entre si.
Quando um deles acumula muitos processos, parte dessa carga pode ser migrada para outro scheduler.
Isso melhora significativamente a utilização do hardware disponível.
É um mecanismo semelhante ao balanceamento de filas em aeroportos.
Se um guichê está congestionado e outro vazio, faz sentido redistribuir os passageiros.
A BEAM faz exatamente isso com processos.
Por que isso importa?
A consequência prática é enorme.
O desenvolvedor deixa de escrever código pensando em:
- qual thread utilizar;
- qual núcleo executar;
- como distribuir processamento;
- quando migrar tarefas.
Em vez disso, ele apenas cria processos independentes.
A plataforma decide onde e quando executá-los.
A melhor infraestrutura é aquela que permite ao desenvolvedor pensar no problema de negócio, e não na distribuição de CPU.
Concorrência favorece escalabilidade
Outro ponto importante é que aplicações altamente concorrentes costumam escalar melhor.
Isso acontece porque processos independentes podem ser distribuídos de diversas formas.
Primeiro entre os schedulers.
Depois entre diferentes núcleos.
Mais tarde entre diferentes servidores.
Essa evolução acontece quase naturalmente.
O modelo de programação permanece praticamente o mesmo.
A infraestrutura apenas amplia a capacidade disponível.
Um exemplo simples
Imagine um sistema responsável por processar imagens enviadas por usuários.
Cada imagem é independente.
Cada uma pode ser processada separadamente.
Na BEAM, bastaria criar um processo para cada imagem.
Imagem 1 → Processo 1
Imagem 2 → Processo 2
Imagem 3 → Processo 3
...
Imagem N → Processo N
A partir desse ponto, o scheduler decide:
- quais processos executar imediatamente;
- quais aguardarão alguns milissegundos;
- quais serão distribuídos entre diferentes CPUs.
O código permanece praticamente inalterado.
A maior contribuição da concorrência
Existe uma frase bastante conhecida do pesquisador Rob Pike:
"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."
Embora tenha sido popularizada durante apresentações sobre Go, ela descreve perfeitamente a arquitetura da BEAM.
Primeiro organizamos inúmeras atividades independentes.
Depois utilizamos o hardware disponível para executá-las da maneira mais eficiente possível.
Essa separação entre organização e execução é um dos pilares que tornam Elixir tão poderoso para aplicações distribuídas.
No próximo capítulo veremos outro diferencial importante da plataforma: seu Garbage Collector por processo, um mecanismo que elimina um dos maiores problemas enfrentados por aplicações de longa duração.
Garbage Collection por processo: eliminando um dos maiores gargalos das aplicações modernas
Uma das maiores dificuldades enfrentadas por aplicações de longa duração não está relacionada à velocidade do processador, à quantidade de memória ou à capacidade de armazenamento.
Ela está na forma como a memória é gerenciada.
Sempre que uma aplicação cria objetos, estruturas de dados ou variáveis, ela consome memória. Quando essas informações deixam de ser necessárias, alguém precisa liberá-las.
Em linguagens como C, essa responsabilidade pertence ao próprio desenvolvedor.
Em linguagens modernas como Java, C#, Go, JavaScript e Elixir, essa tarefa é realizada automaticamente pelo Garbage Collector (GC).
À primeira vista, todos parecem resolver o mesmo problema.
Entretanto, a forma como a BEAM implementa esse mecanismo representa uma das diferenças mais interessantes de toda a plataforma.
O papel do Garbage Collector
O Garbage Collector possui uma responsabilidade simples.
Recuperar memória que não está mais sendo utilizada pela aplicação.
Sem esse mecanismo, aplicações permaneceriam acumulando objetos inutilizados indefinidamente, consumindo cada vez mais memória até eventualmente esgotarem os recursos disponíveis.
Essa automação revolucionou o desenvolvimento de software.
Os desenvolvedores passaram a concentrar seus esforços nas regras de negócio, em vez de administrar manualmente ponteiros e alocação de memória.
No entanto, toda automação possui um custo.
O problema dos Garbage Collectors tradicionais
Em muitas plataformas, o Garbage Collector trabalha analisando grandes regiões de memória compartilhadas por toda a aplicação.
Durante esse processo, pode ser necessário interromper temporariamente parte da execução para identificar quais objetos ainda estão em uso.
Esse comportamento ficou conhecido como:
Stop-the-World (STW)
Visualmente:
Aplicação
███████████████
↓
Garbage Collection
⛔ Aplicação interrompida
↓
Aplicação continua
Essas pausas podem durar apenas alguns milissegundos.
Em muitos sistemas, isso é praticamente imperceptível.
Mas imagine ambientes como:
- bolsas de valores;
- plataformas financeiras;
- sistemas de monitoramento;
- servidores de jogos;
- aplicações de tempo real;
- telecomunicações.
Mesmo pequenas interrupções podem impactar significativamente a experiência dos usuários.
A decisão da BEAM
A arquitetura da BEAM parte novamente de uma decisão fundamental apresentada nos capítulos anteriores.
Cada processo possui:
- memória própria;
- estado próprio;
- isolamento completo.
Se a memória já é isolada...
Por que executar um Garbage Collector global?
A resposta foi extremamente elegante.
Cada processo possui seu próprio Garbage Collector.
Isso significa que o gerenciamento de memória acontece individualmente.
Não existe um único GC responsável por toda a aplicação.
Existem milhares — ou até milhões — de pequenos Garbage Collectors trabalhando independentemente.
O impacto dessa arquitetura
Imagine uma aplicação contendo cem mil processos.
Um deles começa a consumir muita memória.
Na maioria das plataformas tradicionais, isso pode aumentar significativamente o trabalho do Garbage Collector global.
Na BEAM acontece algo diferente.
Processo A
↓
GC Processo A
↓
Processo A continua
Enquanto isso:
Processo B ✅
Processo C ✅
Processo D ✅
Processo E ✅
Nenhum desses processos precisa esperar.
Cada um continua executando normalmente.
A pausa acontece apenas onde realmente é necessária.
Pausas menores significam latência menor
Esse detalhe possui uma consequência extremamente importante.
Como cada processo geralmente possui pouca memória, seu Garbage Collector também possui pouca memória para analisar.
Consequentemente:
- as pausas tornam-se menores;
- o tempo de recuperação diminui;
- a latência permanece mais previsível;
- a aplicação continua respondendo mesmo sob carga elevada.
Essa característica contribui diretamente para um dos principais objetivos da BEAM:
Manter previsibilidade operacional.
Pequenos processos favorecem pequenos GCs
Nos capítulos anteriores vimos que a filosofia da plataforma incentiva dividir responsabilidades em inúmeros processos pequenos.
Agora fica evidente que essa decisão também beneficia o gerenciamento de memória.
Quanto menor o processo:
- menor seu estado;
- menor sua heap;
- mais rápido seu Garbage Collector;
- menor o impacto sobre a aplicação.
Essa integração entre arquitetura de processos e gerenciamento de memória demonstra como os componentes da BEAM foram concebidos para trabalhar em conjunto.
Nada acontece isoladamente.
A memória deixa de ser um gargalo central
Em aplicações tradicionais, frequentemente pensamos na memória como um recurso compartilhado.
Na BEAM, a perspectiva muda.
Cada processo administra seu próprio espaço.
Cada Garbage Collector atua apenas sobre esse espaço.
Cada decisão permanece local.
Essa descentralização reduz significativamente os efeitos colaterais normalmente associados ao gerenciamento de memória em aplicações de grande porte.
Um exemplo prático
Imagine um servidor de chat com um milhão de usuários conectados.
Cada usuário é representado por um processo.
Se apenas um desses usuários enviar uma enorme quantidade de mensagens, provocando grande utilização de memória, apenas o processo correspondente sofrerá a ação do Garbage Collector.
Os demais novecentos e noventa e nove mil novecentos e noventa e nove usuários continuam sendo atendidos normalmente.
Essa característica é extremamente difícil de reproduzir em arquiteturas baseadas em memória compartilhada.
A soma de pequenas decisões
Quando analisamos isoladamente:
- processos leves;
- isolamento;
- comunicação por mensagens;
- supervisão;
- Garbage Collection individual;
cada elemento parece resolver apenas um problema específico.
Mas, quando combinados, eles produzem uma plataforma capaz de manter milhões de atividades simultâneas com enorme estabilidade.
Essa talvez seja uma das maiores lições da engenharia da BEAM.
Grandes arquiteturas raramente dependem de uma única inovação revolucionária. Elas surgem da combinação consistente de inúmeras boas decisões arquiteturais.
No próximo capítulo conheceremos o Phoenix Framework, responsável por transportar toda essa arquitetura para aplicações web modernas, APIs altamente concorrentes e sistemas em tempo real.
Phoenix: quando toda a arquitetura da BEAM encontra a Web
Até este ponto, exploramos conceitos que acontecem "nos bastidores" do Elixir.
Falamos sobre:
- processos leves;
- comunicação por mensagens;
- supervisão;
- concorrência;
- Garbage Collection por processo;
- OTP.
Todos esses elementos constroem uma plataforma extremamente robusta.
Mas ainda falta responder uma pergunta importante.
Como toda essa arquitetura chega até uma aplicação web?
A resposta está no Phoenix Framework.
Embora frequentemente seja comparado a frameworks como Spring Boot, ASP.NET Core, Django ou Express.js, essa comparação conta apenas parte da história.
Phoenix não é apenas um framework para construir APIs.
Ele foi projetado para explorar ao máximo tudo aquilo que a BEAM oferece.
Muito além de um framework MVC
Grande parte dos frameworks web nasceu para resolver um problema relativamente simples:
Receber uma requisição.
Executar uma regra de negócio.
Consultar um banco de dados.
Responder ao cliente.
Esse modelo continua extremamente eficiente para inúmeras aplicações.
Visualmente:
Cliente
↓
HTTP Request
↓
Controller
↓
Service
↓
Repository
↓
Banco de Dados
↓
HTTP Response
Durante muitos anos esse fluxo foi suficiente.
Entretanto, a web mudou.
Hoje esperamos aplicações capazes de:
- atualizar informações em tempo real;
- sincronizar múltiplos usuários;
- manter conexões permanentes;
- transmitir eventos instantaneamente;
- funcionar como plataformas colaborativas.
Esse novo cenário exige uma arquitetura diferente.
O problema do modelo baseado apenas em requisições
No modelo tradicional, praticamente toda interação segue o mesmo ciclo.
Cliente
↓
Nova requisição
↓
Servidor
↓
Nova resposta
↓
Conexão encerrada
Sempre que algo muda:
Nova requisição.
Novo processamento.
Nova resposta.
Isso funciona muito bem para sistemas CRUD.
Mas imagine aplicações como:
- chats;
- dashboards financeiros;
- monitoramento industrial;
- jogos online;
- plataformas colaborativas;
- rastreamento logístico.
Nesses cenários, esperar que o cliente consulte constantemente o servidor torna-se ineficiente.
Phoenix nasceu para aplicações vivas
A proposta do Phoenix é diferente.
Ele trata conexões como elementos permanentes.
Em vez de abrir e fechar conexões continuamente, é possível manter um canal de comunicação ativo durante toda a interação do usuário.
Visualmente:
Cliente
⇅
Phoenix
⇅
Processos da BEAM
⇅
Sistema
Essa arquitetura reduz significativamente:
- latência;
- quantidade de requisições;
- consumo de banda;
- necessidade de sincronizações constantes.
Mais importante ainda:
Ela aproveita perfeitamente o modelo de milhões de processos independentes oferecido pela BEAM.
Cada conexão pode ser um processo
Aqui percebemos como todos os capítulos anteriores começam a se conectar.
Imagine um servidor de chat.
Em muitas arquiteturas tradicionais, um conjunto relativamente pequeno de threads precisa administrar milhares de conexões simultaneamente.
No Phoenix, uma abordagem bastante comum é representar cada conexão utilizando um processo independente.
Usuário A
↓
Processo A
Usuário B
↓
Processo B
Usuário C
↓
Processo C
Cada conexão possui:
- estado próprio;
- mensagens próprias;
- isolamento completo;
- supervisão automática.
Essa arquitetura reduz drasticamente a complexidade da aplicação.
Phoenix Channels
Um dos recursos mais conhecidos do Phoenix são os Channels.
Eles funcionam como canais de comunicação em tempo real sobre WebSockets.
Imagine um grupo de conversa.
Sala "Arquitetura"
↓
Usuário A
Usuário B
Usuário C
↓
Channel
Quando um usuário envia uma mensagem, o Channel distribui o evento para todos os participantes.
Sem polling.
Sem consultas repetitivas.
Sem múltiplas requisições HTTP.
Tudo acontece praticamente em tempo real.
PubSub: comunicação desacoplada
Outro componente extremamente importante do Phoenix é o sistema de Publish/Subscribe (PubSub).
Em vez de componentes conhecerem diretamente uns aos outros, eles apenas publicam eventos.
Quem tiver interesse nesses eventos poderá recebê-los. Na prática, Phoenix apenas amplia essa filosofia para aplicações web.
Presence
Imagine um sistema semelhante ao WhatsApp.
Como descobrir quais usuários estão online?
Quem entrou?
Quem saiu?
Quem está digitando?
Responder essas perguntas parece simples.
Na prática, manter essas informações sincronizadas entre milhares de usuários costuma ser bastante complexo.
Phoenix oferece isso praticamente pronto através do módulo Presence.
Ele mantém automaticamente o estado das conexões distribuídas entre diferentes nós da aplicação.
LiveView: uma mudança de paradigma
Talvez o recurso mais revolucionário do Phoenix seja o LiveView.
Durante muitos anos, aplicações web seguiram uma arquitetura parecida com esta.
Browser
↓
React / Angular / Vue
↓
API REST
↓
Servidor
Grande parte do estado permanece no navegador.
A interface precisa sincronizar continuamente essas informações com o backend.
LiveView propõe outra abordagem.
Browser
⇅ WebSocket
↓
LiveView
↓
Estado no Servidor
Em vez de enviar páginas inteiras ou grandes objetos JSON, o servidor envia apenas as diferenças necessárias para atualizar a interface.
Essa técnica reduz:
- quantidade de JavaScript;
- complexidade da aplicação;
- inconsistências entre cliente e servidor;
- duplicação de regras de negócio.
Mais importante: A lógica continua protegida no backend.
O efeito combinado
Quando observamos Phoenix isoladamente, ele parece apenas mais um framework web moderno.
Mas quando o analisamos junto com tudo que estudamos até agora, percebemos algo diferente.
Phoenix não existe separado da BEAM.
Ele aproveita diretamente:
- processos leves;
- comunicação por mensagens;
- supervisão;
- OTP;
- Garbage Collection por processo;
- concorrência massiva.
É justamente essa integração que explica sua capacidade de atender centenas de milhares de conexões simultâneas mantendo baixa latência.
Phoenix não substitui a arquitetura da BEAM. Ele a transforma em aplicações web.
O futuro das aplicações web?
Durante muitos anos acreditou-se que aplicações web inevitavelmente se tornariam cada vez mais dependentes de JavaScript complexo no navegador.
Phoenix LiveView mostrou que existe outro caminho.
Um caminho onde:
- o backend volta a ser protagonista;
- a sincronização acontece automaticamente;
- menos código é necessário;
- menos inconsistências surgem;
- mais lógica permanece centralizada.
Talvez essa seja uma das maiores contribuições do ecossistema Elixir para o desenvolvimento web moderno.
No próximo capítulo veremos como tudo isso se traduz em números e casos reais, analisando por que empresas como o Discord adotaram essa arquitetura para atender milhões de usuários simultaneamente.
Quando a teoria encontra a prática: por que empresas como o Discord escolheram Elixir?
Até aqui, exploramos os fundamentos técnicos do ecossistema Elixir.
Falamos sobre:
- processos leves;
- comunicação por mensagens;
- Actor Model;
- OTP;
- supervisão;
- Garbage Collection por processo;
- Phoenix.
Cada conceito, isoladamente, parece resolver um problema específico.
Mas uma pergunta continua em aberto.
Se essa arquitetura é realmente tão eficiente, onde ela está sendo utilizada?
A resposta é simples.
Em sistemas onde falhar não é uma opção.
Tecnologia não é escolhida por moda
Existe uma percepção bastante comum na indústria de software.
Muitas pessoas imaginam que empresas escolhem tecnologias porque elas estão "em alta".
Na prática, organizações de grande porte costumam seguir outro critério.
Custo operacional.
Toda decisão tecnológica possui impacto direto sobre:
- infraestrutura;
- equipe;
- manutenção;
- disponibilidade;
- escalabilidade;
- experiência do usuário.
Quanto maior a plataforma, mais caro se torna um erro arquitetural.
Por isso, dificilmente empresas que atendem milhões de usuários escolhem uma tecnologia apenas porque ela está em evidência.
Grandes empresas normalmente escolhem arquiteturas que reduzem risco operacional, e não apenas tecnologias populares.
O caso do Discord
Um dos exemplos mais conhecidos é o Discord.
Quando a plataforma começou a crescer rapidamente, um dos maiores desafios não era armazenar mensagens.
Era manter milhões de conexões simultâneas ativas.
Usuários entrando.
Saindo.
Digitando.
Entrando em chamadas de voz.
Recebendo notificações.
Tudo isso acontecendo praticamente em tempo real.
Esse cenário exige exatamente aquilo que estudamos ao longo dos capítulos anteriores.
- milhões de processos independentes;
- comunicação baseada em mensagens;
- baixa latência;
- recuperação automática de falhas;
- excelente utilização de CPU.
Segundo a própria equipe de engenharia do Discord, o Elixir permitiu construir uma infraestrutura capaz de atender milhões de conexões WebSocket simultaneamente utilizando uma quantidade relativamente pequena de servidores.
O desafio nunca foi processar mensagens rapidamente. O desafio era manter milhões de pessoas conectadas ao mesmo tempo.
Esse é exatamente o tipo de problema para o qual a BEAM foi projetada décadas atrás.
Escalabilidade não significa apenas adicionar servidores
Existe uma ideia bastante difundida na computação em nuvem.
"Se faltar capacidade, basta adicionar mais máquinas."
Embora isso funcione em muitos casos, essa abordagem possui limites.
Adicionar servidores aumenta:
- custos;
- comunicação entre serviços;
- sincronização;
- observabilidade;
- complexidade operacional.
Uma arquitetura eficiente busca utilizar melhor os recursos existentes antes de simplesmente aumentar a infraestrutura.
É justamente aí que Elixir costuma se destacar.
Sua capacidade de manter milhares — ou milhões — de processos leves permite aproveitar cada servidor de forma extremamente eficiente.
Outros exemplos
Embora o Discord seja frequentemente citado, ele não está sozinho.
Diversas empresas utilizam Elixir em componentes críticos de suas plataformas.
Entre elas podemos destacar:
- Pinterest, em partes de sua infraestrutura de processamento e notificações.
- PepsiCo, em soluções internas de alta disponibilidade.
- Bleacher Report, em sistemas de entrega de notificações em tempo real.
- Moz, em aplicações voltadas para processamento distribuído.
- Heroku, em componentes de sua infraestrutura.
O interessante é perceber que esses projetos possuem algo em comum.
Eles não utilizam Elixir porque precisam realizar cálculos extremamente complexos.
Eles utilizam Elixir porque precisam coordenar milhares ou milhões de atividades simultaneamente de forma confiável.
O padrão aparece
Quando analisamos diferentes casos de sucesso, começamos a perceber um padrão.
Elixir aparece frequentemente em sistemas como:
- plataformas de comunicação;
- chats;
- mensageria;
- streaming;
- monitoramento;
- fintechs;
- IoT;
- sistemas distribuídos;
- aplicações colaborativas;
- processamento de eventos.
Todos esses ambientes compartilham desafios semelhantes.
Alta concorrência.
Baixa latência.
Disponibilidade contínua.
Recuperação automática de falhas.
Esses desafios são muito mais arquiteturais do que algorítmicos.
O que quase nunca vemos
Por outro lado, existem áreas onde Elixir raramente é utilizado.
Por exemplo:
- engines gráficas;
- processamento científico pesado;
- renderização 3D;
- treinamento de modelos de IA;
- desenvolvimento de drivers de hardware;
- sistemas embarcados extremamente restritivos.
Isso não representa uma limitação da linguagem.
Representa simplesmente adequação ao problema.
Assim como não utilizaríamos um caminhão para disputar uma corrida de Fórmula 1, também não utilizaríamos um carro esportivo para transportar cinquenta toneladas de carga.
Cada tecnologia possui um propósito.
A melhor linguagem não é aquela que resolve todos os problemas. É aquela que resolve muito bem o problema certo.
Engenharia é sobre escolhas
Esse talvez seja um dos maiores aprendizados proporcionados pelo estudo do Elixir.
Durante muito tempo discutimos linguagens tentando responder perguntas como:
- Qual é a mais rápida?
- Qual possui melhor sintaxe?
- Qual gera menos código?
Essas perguntas continuam relevantes.
Mas existe uma questão ainda mais importante.
Que tipo de problema essa tecnologia foi criada para resolver?
Quando compreendemos essa resposta, deixamos de escolher ferramentas por preferência pessoal e passamos a escolhê-las por adequação arquitetural.
Essa mudança de perspectiva aproxima o desenvolvimento de software da engenharia.
O legado da BEAM
Curiosamente, boa parte dos conceitos que hoje aparecem em arquiteturas modernas já estavam presentes na BEAM décadas atrás.
- isolamento;
- comunicação por mensagens;
- supervisão;
- distribuição;
- resiliência;
- escalabilidade.
Talvez essa seja a maior demonstração da qualidade de uma arquitetura.
Ela permanece atual mesmo quando o restante da indústria muda.
No próximo capítulo concluiremos nossa jornada refletindo sobre a principal contribuição do Elixir para a engenharia de software moderna: uma nova maneira de pensar sistemas distribuídos, muito além de uma simples linguagem de programação.
Referências
- Discord. Blog de Engenharia sobre escalabilidade e WebSockets.
- Pinterest. Publicações técnicas sobre infraestrutura distribuída.
- Bleacher Report. Artigos sobre uso de Elixir em sistemas de notificações.
- Designing Data-Intensive Applications. O'Reilly Media, 2017.
- Elixir in Action. Manning Publications, 2ª edição, 2019.