Em 1973, Carl Hewitt e seus colaboradores no MIT — Peter Bishop e Richard Steiger — publicaram um artigo de doze páginas com título ambicioso: A Universal Modular Actor Formalism for Artificial Intelligence. Hewitt buscava um modelo computacional capaz de lidar com sistemas inteligentes distribuídos: agentes que se comunicam, decidem localmente, e cooperam sem coordenação central. A inspiração veio de três fontes — o lambda calculus de Church (1936), as mensagens em Smalltalk-72 de Alan Kay, e a noção emergente de processos cooperantes de Dijkstra. O resultado foi o actor model: uma teoria de computação onde a unidade primária é o actor, uma entidade que recebe mensagens, pode criar outros actors, alterar o próprio comportamento, e enviar mensagens em resposta.
Por uma década e meia, o modelo permaneceu como construção acadêmica. A virada veio em 1986, quando Joe Armstrong e equipe no Computer Science Lab da Ericsson, na Suécia, começaram a projetar Erlang para construir sistemas de telefonia tolerantes a falhas. A demanda era extrema: centrais telefônicas que atendessem milhões de chamadas com uptime medido em "five nines" (99,999% — cinco minutos de downtime por ano), com atualizações de software sem desligar o serviço, com falhas de hardware tratadas em milissegundos sem perder ligações em curso. Threads, locks e exception handling tradicionais não bastavam. Armstrong escolheu o actor model — não por elegância acadêmica, mas porque era a única abstração que parecia escalar para o requisito.
Erlang foi liberada como open source em 1998 e virou base de sistemas que a maior parte do mundo usa sem saber: WhatsApp (suportando bilhões de mensagens por dia em 2026 com pequena equipe), RabbitMQ (broker de mensagens onipresente), Discord (originalmente, em Elixir/BEAM), CouchDB, Klarna. Akka (2009) trouxe o modelo para JVM. Orleans (2011, Microsoft Research) criou a noção de "virtual actors" para .NET, usado em Halo, Skype, e Azure. Pony (2015) explorou type system que garante ausência de data race em tempo de compilação. Em 2026, actor model é a fundação de virtualmente todo sistema massivamente distribuído tolerante a falhas em produção.
Este conceito mostra a teoria de Hewitt, sua materialização prática em Erlang/Akka/Orleans, o pensamento de "let it crash" e supervisão hierárquica como inversão do reflexo defensivo, e o contraste com CSP (visto no conceito anterior). É denso porque o modelo exige reorientação mental — quem vem de OOP tradicional precisa internalizar que actors não são objetos com locks, são processos com identidade.
O actor — três regras essenciais
Hewitt formalizou o actor com três axiomas. Um actor, em resposta a uma mensagem, pode:
- Enviar mensagens a actors cujos endereços ele conhece. O envio é assíncrono e fire-and-forget — o emissor não espera resposta a menos que combine isso explicitamente.
- Criar novos actors, ganhando o endereço do recém-criado para enviar mensagens a ele.
- Mudar o próprio comportamento para a próxima mensagem. O comportamento é tipicamente expresso como uma função ou método que processa mensagens; mudar o comportamento é trocar a função.
Três axiomas curtos, consequências profundas. Como o actor processa uma mensagem por vez, não há concorrência interna — qualquer estado dentro do actor é manipulado sequencialmente. Não precisa de locks. Como mensagens são assíncronas, o emissor não bloqueia esperando processamento. Como actors têm identidade (endereço), a comunicação é dirigida — você manda mensagem para esse actor, não para um canal compartilhado.
Compare com CSP. No conceito anterior, channels eram cidadãos primários e processos eram anônimos do ponto de vista do canal. No actor model, actors são cidadãos primários e canais não existem — cada actor tem seu próprio mailbox privado. Você não manda mensagem para "o canal de pedidos", manda para "o actor do pedido 12345". Essa diferença parece sutil; tem consequências profundas em design e em como o sistema escala em distribuição.
Em actor model, identidade é central. Cada actor é uma unidade lógica autônoma com estado próprio e endereço observável. Pensar em actors é pensar em "quem faz o quê, e quem fala com quem" — não em "quais dados estão compartilhados". O modelo mental é mais próximo de microsserviços (cada serviço com identidade, com fronteira) do que de objetos OOP (que tipicamente compartilham heap).
Mailbox — onde mensagens esperam
Cada actor tem um mailbox: uma fila privada de mensagens recebidas mas ainda não processadas. Quando outro actor envia mensagem, ela vai para o mailbox do destinatário e o emissor segue seu trabalho. Em algum momento, o actor destinatário processa a mensagem (chamando seu método de tratamento atual), termina, e pega a próxima do mailbox.
A natureza FIFO do mailbox parece simples, mas há decisões de design importantes. Mailbox bounded oferece backpressure natural — quando enche, novos sends bloqueiam ou falham. Mailbox unbounded aceita qualquer quantidade, mas pode crescer indefinidamente se o actor não der vencimento ao volume, eventualmente esgotando memória. Akka oferece ambos como mailbox types configuráveis. Erlang tradicionalmente usa unbounded, com a contraparte de que a programação cuidadosa é necessária para evitar overload.
Algumas implementações oferecem priority mailbox:
mensagens marcadas como prioritárias pulam para frente da fila.
É útil em sistemas onde mensagens de controle (shutdown, status
check) precisam ser processadas antes de mensagens de trabalho
em backlog. Erlang implementa isso com receive
seletivo — você pode pegar mensagens da fila por padrão, não
necessariamente em ordem.
Erlang — actor model em produção desde 1986
Erlang é a referência canônica de actor model em produção. A sintaxe parece estranha para quem vem de C-family, mas os conceitos são limpos. Veja um actor contador minimal:
%% contador.erl
-module(contador).
-export([start/0, incrementar/1, ler/1]).
start() ->
spawn(fun() -> loop(0) end).
loop(N) ->
receive
incrementar -> loop(N + 1);
{ler, From} -> From ! {valor, N}, loop(N);
parar -> ok
end.
incrementar(Pid) -> Pid ! incrementar.
ler(Pid) -> Pid ! {ler, self()},
receive {valor, V} -> V end.
Pontos a notar. spawn cria um novo actor (em Erlang
chamado de process, mas é o que outras linguagens
chamam de actor). ! é o operador de envio de
mensagem (Pid ! mensagem envia para o processo
identificado pelo PID). receive aguarda mensagens
do mailbox, fazendo pattern matching para decidir qual cláusula
executa. loop(N + 1) é a forma idiomática de
"mudar o estado": chamada recursiva com novo estado, sem
mutação de variável. Estado é capturado em parâmetro de função,
não em campo de objeto.
A BEAM (a VM de Erlang) implementa preempção de actors com precisão notável. Cada actor tem um budget de "reductions" (instruções) e é preempted quando excede. Na prática, isso significa que um actor em loop apertado não bloqueia outros — coisa que goroutines em Go até 1.14 não garantiam. A BEAM também faz garbage collection por actor (não global), tornando GC pauses imperceptíveis em sistemas com milhões de processos. Esses detalhes de implementação são parte do que torna Erlang capaz de sustentar 99,999% uptime.
"Let it crash" — a inversão do reflexo defensivo
Programadores treinados em Java, C# e Python carregam um reflexo:
defenda contra falhas dentro da função. try/catch
em todo lugar, validações em entrada, branchings para tratar
casos exóticos. O resultado é código onde 60% das linhas tratam
casos que talvez nunca aconteçam, e que esconde a lógica de
negócio entre verificações. Joe Armstrong, no livro
Programming Erlang, articulou a alternativa:
"let it crash".
A ideia é simples e radical. Dentro de um actor, você não tenta tratar todas as possíveis falhas. Você assume os caminhos esperados e escreve código limpo. Quando algo inesperado acontece — input mal-formado, conexão perdida, NPE — o actor crasha. Não tenta se recuperar. Outro actor (o supervisor) detecta o crash e decide o que fazer: reiniciar o actor crashed, escalar para um supervisor superior, ou desligar a árvore inteira.
Por que isso funciona? Três razões. Primeira: dividir responsabilidades. Lógica de negócio fica no actor; lógica de falha fica no supervisor. Cada um faz uma coisa bem. Segunda: o estado do actor é resetado no restart — sem state corrompido pelo erro. Terceira: o sistema todo, em vez de uma instância individual, vira o foco de robustez. Você não tenta fazer cada processo perfeito; tenta fazer o sistema tolerante a processos imperfeitos.
%% supervisor genérico em OTP
init(_Args) ->
SupFlags = #{strategy => one_for_one,
intensity => 5, period => 10},
ChildSpecs = [
#{id => contador,
start => {contador, start, []},
restart => permanent,
shutdown => 5000,
type => worker}
],
{ok, {SupFlags, ChildSpecs}}.
O supervisor declara: "se o actor 'contador' crashar, reinicie
ele". Estratégia one_for_one diz "reinicie só o
crashed"; alternativas são one_for_all (reinicie
todos os filhos) e rest_for_one (reinicie o
crashed e os subsequentes). intensity e
period definem limite: se mais que 5 restarts em
10 segundos, escala para o pai. Toda a árvore de processos do
sistema vira uma supervision tree, onde cada nível
tem política definida de tratamento de falha.
Comparação com defensive programming tradicional
Considere processar uma mensagem em um servidor. Em estilo defensivo (Java tradicional):
void processar(Mensagem m) {
try {
if (m == null) return;
if (m.getCorpo() == null) return;
try {
var resultado = parser.parse(m.getCorpo());
if (resultado != null) {
processador.processar(resultado);
}
} catch (ParseException e) {
logger.error("parse falhou", e);
return;
} catch (ProcessamentoException e) {
logger.error("processamento falhou", e);
return;
}
} catch (Throwable t) {
logger.error("inesperado", t);
}
}
Em estilo "let it crash" (Erlang/Akka):
processar(Mensagem) ->
Resultado = parser:parse(Mensagem#mensagem.corpo),
processador:processar(Resultado).
%% Se algo crasha, supervisor reinicia o actor.
%% Sem try, sem if, sem null check.
A versão Erlang é uma fração do tamanho. Quando algo dá errado, o supervisor reinicia o actor — provavelmente o problema foi uma mensagem corrompida, e a próxima vai funcionar. Se o problema persistir (5 crashes em 10s), o supervisor escala. O sistema, no agregado, é mais robusto que a versão defensiva, porque a versão defensiva pode esconder bugs ao "tratá-los" silenciosamente.
"Let it crash" não significa "deixe tudo cair". Significa "deixe o supervisor decidir o que fazer com a falha — não tente prever cada caso dentro do actor". Em sistemas onde você controla a topologia de processos (e tem hot reload, como em Erlang), a estratégia funciona. Em sistemas onde crashar custa caro (perde dados, perde conexão de cliente), ainda há lugar para defensive programming — mas como exceção, não como regra.
Akka — actor model na JVM
Em 2009, Jonas Bonér criou Akka, uma biblioteca para Scala que depois ganhou suporte a Java. O nome vem do mountain Akka na Lapônia sueca (Bonér é sueco) e da homenagem implícita a Erlang. Akka rodava em Lightbend (a empresa de Bonér, antes Typesafe) e virou o veículo de actor model para o ecossistema JVM. Em 2022, Lightbend mudou a licença de Akka 2.7+ para Business Source License, motivando o fork comunitário Apache Pekko (2023) — que mantém compatibilidade e licença Apache 2.0. Em 2026, ambos estão em uso.
// Scala — Akka Typed (estilo moderno)
object Contador {
sealed trait Mensagem
case object Incrementar extends Mensagem
case class Ler(reply: ActorRef[Int]) extends Mensagem
def apply(): Behavior[Mensagem] = comportamento(0)
private def comportamento(n: Int): Behavior[Mensagem] =
Behaviors.receive { (ctx, msg) =>
msg match {
case Incrementar => comportamento(n + 1)
case Ler(reply) =>
reply ! n
Behaviors.same
}
}
}
Akka Typed (introduzido em Akka 2.6, 2019) traz tipagem
estática para mensagens — quem usa o actor sabe pelo tipo
ActorRef[Mensagem] exatamente que mensagens pode
enviar. Isso ataca uma das críticas históricas a actor model:
o "any actor can send any message" gerava erros em runtime
que poderiam ser detectados em compile time. A integração
com o sistema de tipos de Scala/Java fez com que actor model
em JVM ganhasse o melhor de dois mundos.
Orleans — virtual actors da Microsoft
Em 2011, a Microsoft Research lançou Orleans, com uma proposta original: virtual actors. Em modelos tradicionais (Erlang, Akka), você cria um actor explicitamente, gerencia seu ciclo de vida, decide quando destruir. Em Orleans, actors ("grains") são tratados como conceitualmente sempre existentes — você os referencia por ID e o runtime resolve onde eles vivem (em qual nó do cluster), criando-os sob demanda e desativando quando ficam idle.
A vantagem é simplicidade conceitual: você não precisa pensar em "este actor existe?" — você sempre pode mandar mensagem para qualquer ID. Orleans alimenta sistemas em escala massiva: Halo 4 e 5 (matchmaking, presença de jogadores), Skype messaging, e Azure Digital Twins. O modelo é particularmente adequado para entidades de negócio com ID estável (usuários, pedidos, dispositivos IoT).
// C# — Orleans
public interface IContadorGrain : IGrainWithStringKey {
Task IncrementarAsync();
Task<int> LerAsync();
}
public class ContadorGrain : Grain, IContadorGrain {
private int n;
public Task IncrementarAsync() {
n++;
return Task.CompletedTask;
}
public Task<int> LerAsync() => Task.FromResult(n);
}
// uso
var grain = grainFactory.GetGrain<IContadorGrain>("usuario:1234");
await grain.IncrementarAsync();
var v = await grain.LerAsync();
O grain "usuario:1234" sempre existe conceitualmente. Da primeira vez que você manda mensagem, Orleans aloca em algum silo do cluster. Se o silo cair, Orleans recria o grain em outro silo (com estado restaurado de storage, se persistido). Essa transparência é especialmente poderosa em sistemas distribuídos — você programa como se fosse local, e Orleans cuida do resto.
Pony — type system contra data races
Pony, criada por Sylvan Clebsch em 2015, é a tentativa mais ambiciosa de garantir ausência de data races em compile time via sistema de tipos. Pony usa reference capabilities — cada referência a um objeto carrega permissões (read-only, read-write, isolated, sendable) que o compilador verifica. Você não consegue compilar um programa Pony onde duas threads possam escrever no mesmo objeto sem coordenação.
Em produção, Pony é nicho — adoção pequena, ecossistema limitado. Mas conceitualmente é importante: prova que actor model + type system rico podem dar garantias estáticas que modelos tradicionais não dão. Em 2026, ideias de Pony influenciam Rust async (que tem semelhanças no tratamento de ownership/borrowing) e algumas propostas em desenvolvimento para Swift e Kotlin.
Tell vs Ask — dois patterns de mensagem
Em quase todo framework de actor, há duas formas de enviar mensagem com semânticas distintas:
Tell (fire-and-forget)
actor ! mensagem em Erlang/Akka,
actor.tell(msg) em alguns dialetos. A mensagem é
colocada no mailbox e o emissor segue. É o padrão idiomático
e o que actor model espera por design — mensagens assíncronas,
sem retorno direto. Se você precisa de resposta, o actor
destinatário inclui um reply-to nas mensagens e
envia uma mensagem de volta.
Ask (request-response)
actor ? mensagem em Akka, ou via método utilitário.
Espera por uma mensagem de retorno como Future/Promise. É
conveniente, mas perigoso: bloqueia (ou suspende) o emissor; se
o destinatário travar ou demorar, o emissor sofre. Em sistemas
onde latência é crítica, "ask" tipicamente vem com timeout. Em
sistemas distribuídos com falhas reais, "ask" deve ser usado
com moderação — Joe Armstrong, em palestras, advertia
explicitamente contra abuso.
A regra prática é: prefira "tell" (assíncrono) por padrão; use "ask" apenas quando a semântica do problema for genuinamente request-response e você puder absorver a latência. APIs externas, queries em outros serviços, esses são candidatos legítimos. Comunicação interna entre actors do mesmo serviço tipicamente fica melhor com tell + reply-to explícito.
Distribuição transparente — actors locais e remotos
Uma das vantagens estruturais do actor model é
location transparency: do ponto de vista do código que
envia mensagem, não importa se o actor destinatário está no
mesmo processo, em outro processo na mesma máquina, ou em
outra máquina no cluster. A interface é a mesma:
endereço ! mensagem. O framework cuida de
serialização, transporte, reconexão.
Erlang implementa isso desde sempre: nodes Erlang se conectam ao cluster via TCP, e enviar mensagem para um Pid em outro node funciona transparentemente. Akka tem Akka Cluster e Akka Remote. Orleans tem silos coordenados. A consequência prática é que escalar um sistema vira (em primeira aproximação) adicionar mais máquinas — actors migram, replicam, são acessados de qualquer lugar.
A "primeira aproximação" é importante. Distribuição introduz latência de rede, falhas parciais, e a sutileza de que actors remotos podem desaparecer. CAP theorem aplica. Em sistema Erlang real, há detalhes (split brain, network partitions, consenso) que precisam de cuidado. Mas o ponto é que actor model expõe distribuição como variação de localidade, não como novo paradigma — uma propriedade que poucos modelos de concorrência oferecem com a mesma elegância.
O mesmo padrão nas três linguagens
Para concretizar, considere um actor que mantém um estado contábil (saldo) e processa mensagens de incremento, decremento e leitura. Em ambientes onde actor model é nativo, o código fica curto; em ambientes onde precisa biblioteca, fica mais verboso.
using Akka.Actor;
public abstract record SaldoMsg;
public sealed record Depositar(decimal Valor) : SaldoMsg;
public sealed record Sacar(decimal Valor) : SaldoMsg;
public sealed record LerSaldo(IActorRef Reply) : SaldoMsg;
public class SaldoActor : ReceiveActor {
private decimal saldo;
public SaldoActor() {
Receive<Depositar>(m => saldo += m.Valor);
Receive<Sacar>(m => saldo -= m.Valor);
Receive<LerSaldo>(m => m.Reply.Tell(saldo));
}
}
// uso
var system = ActorSystem.Create("banco");
var contador = system.ActorOf(Props.Create<SaldoActor>());
contador.Tell(new Depositar(100m));
contador.Tell(new Sacar(30m));
// pedir saldo via ask:
var saldo = await contador.Ask<decimal>(new LerSaldo(ActorRefs.NoSender));
Akka.NET (mantido por Petabridge) é port idiomático de Akka
para .NET. Mensagens como record sealed são
padrão moderno. Note que actor processa mensagens uma por
vez — não há lock no saldo. Orleans seria
ainda mais idiomático em .NET para sistemas distribuídos
em produção.
import pykka
class SaldoActor(pykka.ThreadingActor):
def __init__(self):
super().__init__()
self.saldo = 0
def on_receive(self, msg):
match msg:
case ("depositar", valor):
self.saldo += valor
case ("sacar", valor):
self.saldo -= valor
case ("ler",):
return self.saldo
# uso
ref = SaldoActor.start()
ref.tell(("depositar", 100))
ref.tell(("sacar", 30))
saldo_future = ref.ask(("ler",))
print(saldo_future.get()) # 70
ref.stop()
Pykka oferece actor model em Python sobre threading (também tem Greenlet/asyncio). O actor processa mensagens uma por vez na sua thread interna; estado fica seguro sem lock. Para sistemas distribuídos em Python, Thespian é alternativa. Note que Python não tem actor model nativo — essas bibliotecas o reproduzem com qualidade.
package main
import "fmt"
type cmd interface{ tipo() string }
type depositar struct{ valor float64 }
func (depositar) tipo() string { return "dep" }
type sacar struct{ valor float64 }
func (sacar) tipo() string { return "sac" }
type ler struct{ reply chan float64 }
func (ler) tipo() string { return "ler" }
func saldoActor(in <-chan cmd) {
saldo := 0.0
for c := range in {
switch m := c.(type) {
case depositar: saldo += m.valor
case sacar: saldo -= m.valor
case ler: m.reply <- saldo
}
}
}
func main() {
in := make(chan cmd, 100)
go saldoActor(in)
in <- depositar{100}
in <- sacar{30}
reply := make(chan float64)
in <- ler{reply}
fmt.Println(<-reply) // 70
}
Go não tem actor nativo, mas o padrão "goroutine + channel de comandos" reproduz a semântica de actor: estado interno isolado, mensagens processadas uma por vez. Para sistemas maiores, ProtoActor-Go ou Hollywood oferecem framework completo (com supervisor, distribuição), mas o caseiro cobre boa parte dos casos.
Quando actor model brilha — e quando outros vencem
Actor model é especialmente adequado em três cenários. Primeiro: sistemas com identidade de longa vida — usuários online em jogo, dispositivos IoT em fleet, sessões em messenger. Cada identidade tem estado, e o estado evolui no tempo independente dos outros. Segundo: sistemas que exigem alta tolerância a falhas com supervisão hierárquica — telecom, sistemas de negociação, sistemas embarcados críticos. Terceiro: sistemas distribuídos onde location transparency simplifica escalabilidade — clusters massivos onde nodes vêm e vão.
Actor model é menos natural em três cenários. Primeiro: pipelines de processamento de dados — fluxo é estrutura mais adequada que identidade, e CSP costuma expressar melhor. Segundo: aplicações com estado pequeno e altamente compartilhado — um cache, um contador global, locks tradicionais ou estruturas lock-free são mais simples. Terceiro: APIs CRUD simples sem requisitos de robustez extrema — Spring Boot ou ASP.NET tradicional resolvem com menos cerimônia.
Quando você combina os modelos do módulo até aqui, vê-se um espectro: async/await (cooperativo, single-thread) → CSP (processos sequenciais, canais como primitivos) → actor model (entidades com identidade, mensagens assíncronas) → systems distribuídos (microsserviços com identidade e mensageria de rede). Cada modelo amplia o anterior em um eixo: actor model adiciona identidade explícita ao CSP, e sistemas distribuídos adicionam falhas e latência reais ao actor model. Não é coincidência que arquiteturas baseadas em actor model (Erlang, Akka Cluster, Orleans) sejam mais fáceis de migrar para distribuído do que arquiteturas thread-based — eles já tinham os primitivos certos.
Programadores acostumados a OOP frequentemente criam um actor por classe, replicando a granularidade dos objetos. O resultado é um sistema com milhões de actors triviais, todos trocando mensagens, e overhead que mata desempenho. Actor model funciona melhor com granularidade coarser: actor por entidade de negócio (usuário, pedido, dispositivo) — não actor por estrutura de dados. Joe Armstrong recomendava: "se você precisa pensar duas vezes sobre criar um actor, provavelmente não precisa".
Como praticar
- Implemente um actor caseiro em Go ou Python. Sem framework. Goroutine ou thread + channel/queue de mensagens, processadas em sequência. Estado privado da função. Adicione "tell" e "ask". Compare a complexidade com equivalente usando mutex.
- Construa um sistema de chat com actors. Cada usuário é um actor; salas são actors; mensagens fluem como tells entre eles. Use Akka.NET, Pykka, ou home-built. Adicione supervisor que reinicia actors quando falham. Force uma falha (mensagem mal-formada) e observe o restart.
- Compare tratamento de falha entre estilos. Mesmo problema (parser de mensagens recebidas via socket) em duas versões: defensive programming tradicional (try/catch em todo lugar) e let-it-crash com supervisor. Submeta entradas patológicas a ambos. Compare quantidade de código, comportamento sob carga ruim, e quão fácil é adicionar tratamento para um novo tipo de erro.
Referências para aprofundar
- livro Programming Erlang — Joe Armstrong (2ª ed., 2013).
- livro Designing for Scalability with Erlang/OTP — Francesco Cesarini & Steve Vinoski (2016).
- livro Akka in Action — Raymond Roestenburg, Rob Bakker, Rob Williams (2ª ed., 2024).
- livro Reactive Messaging Patterns with the Actor Model — Vaughn Vernon (2015).
- paper A Universal Modular Actor Formalism for Artificial Intelligence — Hewitt, Bishop, Steiger (1973).
- paper Making reliable distributed systems in the presence of software errors — Joe Armstrong (PhD thesis, 2003).
- paper Orleans: Distributed Virtual Actors for Programmability and Scalability — Microsoft Research (2014).
- artigo Why is your Akka actor still receiving messages? — Petabridge blog.
- docs Erlang/OTP Documentation.
- docs Microsoft Orleans Documentation.
- docs Pony Tutorial.
- vídeo The mess we're in — Joe Armstrong (Strange Loop, 2014).