Por que eu me divorciei do Laravel Observer?
Em um post anterior no meu blog, sobre a migração para DTOs, eu falei sobre como deixei de depender dos “arrays mágicos” do Laravel em favor de tipagem mais estrita. Esse foi o Passo 1 da minha jornada em direção a um código mais previsível. Isso não foi agora, nem é tão recente, mas acho muito válido documentar essa experiência aqui com vocês.
Quando você começa a usar DTOs e
Actions/, naturalmente o caminho do Observer acaba ficando inviável logo no curto prazo.
Sem mais delongas (eu sei, eu escrevo tanto que acabo enrolando demais), hoje eu preciso falar do Passo 2, e esse aqui doeu mais do que eu esperava: eu parei de usar Observer como muleta.

Por anos eu fui (quer dizer, ainda sou, mas de forma diferente) um Laravel power user. Se a documentação tinha uma feature, eu testava. Observers pareciam um superpoder: Controllers enxutos, models “limpos”, e tudo que é efeito colateral escondido dentro de um UserObserver.
Só que conforme meus projetos deixaram de ser CRUD e viraram domínio de negócio de verdade, eu fui percebendo um padrão bem feio: Observer não é clean code. Observer é lógica invisível. E lógica invisível é o tipo de coisa que te faz perder uma tarde inteira olhando para o arquivo errado, achando que o bug está “no Laravel”, quando na verdade está num Observer esquecido que alguém criou 9 meses atrás.
A ilusão do código “limpo”
O apelo do Observer é óbvio. Você abre o controller e ele parece lindo:
public function store(UserData $userData)
{
// Olha como está "limpo"!
$user = User::create($userData->toArray());
return response()->json($user);
}
Bonito. Só que tem um detalhe: esse “limpo” é só maquiagem. Porque, sem que a pessoa que vai ler esse arquivo daqui a 6 meses saiba, aquela única linha User::create() pode disparar uma reação em cadeia:
- Envia e-mail de boas-vindas
- Cria Customer no Stripe
- Registra atividade
- Notifica um canal no Slack
- Atualiza índice de busca
- Limpa cache
- Passa o cafezinho
E por aí vai.
E aí começa a diversão: você olha pra linha, ela só “cria usuário”. Mas o sistema inteiro está fazendo uma festa escondida.
Isso é o que eu chamo de Ação Fantasmagórica à Distância (spooky action at a distance): você muda o banco de dados aqui, e código é executado em outro lugar que você nem lembrava que existia.

A armadilha da ordem de execução
Outro problema bem comum é a ordem de execução te trair.
Imagine que você tem lógica no evento created que depende de algum relacionamento. Só que se você usa User::create(), o created dispara imediatamente. Muitas vezes antes de você ter a chance de associar coisas como Roles, Teams, Profile, etc.
E aí você começa a ver code smells desse tipo:
public function created(User $user)
{
// Tentando adivinhar se já deu tempo de carregar relação...
if ($user->relationLoaded('team')) {
// ...
}
}
Isso é frágil. E pior: é frágil de um jeito silencioso. Hoje funciona. Amanhã alguém muda um fluxo, coloca um import, usa createQuietly, ou muda a ordem de attach, e pronto, o Observer vira uma roleta. E se uma regra de negócio depende da ordem acidental de eventos do Eloquent, isso não é “arquitetura”, é fé.
O problema real: efeitos colaterais sem dono
No fundo, o que me fez desistir não foi “observer é do mal”. Foi perceber que eu estava colocando regras de negócio num lugar onde:
- Não é óbvio que elas existem
- Não é óbvio quando elas rodam
- Não é óbvio como desativar
- Não é óbvio como testar o fluxo sem chamar metade do sistema junto
Mesmo quando não dá bug, o custo mental está lá. E quando dá bug, a pessoa que está debugando paga a conta.
Chega um momento na vida do dev experiente que o que ele menos quer é dor de cabeça, ele busca controle e previsibilidade.

A solução: explícito é melhor que implícito
Do mesmo jeito que eu troquei Form Requests por DTOs pra deixar os dados explícitos, eu troquei Observers por Services e Actions explícitos.
Sim, isso significa escrever umas linhas a mais.
Só que essas linhas a mais contam uma história. E história é o que o seu “eu do futuro” precisa quando algo quebra. Eu acho que esse é o ponto mais importante, Action quer ser fluxo explícito. Observer quer ser efeito colateral invisível. Os dois juntos viram um sistema onde ninguém sabe qual é a “fonte da verdade” do comportamento.
Aqui está uma versão refatorada do fluxo. Repare que não tem mágica. Você lê e sabe o que acontece:
final class CreateUserAction
{
public function __construct(
private BillingService $billing,
) {}
public function execute(UserData $data): User
{
return DB::transaction(function () use ($data) {
$user = User::create($data->toArray());
DB::afterCommit(function () use ($user) {
$this->billing->createCustomer($user);
});
$user->notify((new WelcomeNotification())->afterCommit());
return $user;
});
}
}
O bônus aqui é que o código vira um painel de controle.
- Quer que import não envie e-mail? Não chama.
- Quer que um admin crie usuário sem Stripe? Não chama.
- Quer testar só o “create user” sem disparar o resto do planeta? Dá pra fazer.
Quando Observers SÃO aceitáveis?
Isso significa que Observer é inútil? Obviamente que não. Eu acredito no uso de Observers quando é uma preocupação técnica, sempre verdadeira, independentemente do contexto, e que não representa regra de negócio.
Exemplos que eu consideraria aceitáveis:
- Gerar UUID para um model
- Limpar chaves de cache
- Atualizar índices de busca (Elasticsearch/Meilisearch), dependendo do caso
Agora, regra de negócio (enviar e-mail, cobrar cartão, atribuir time, provisionar recurso)? Eu evito. Porque regra de negócio precisa ter dono, e precisa estar num lugar onde dá pra enxergar.

Maturidade é previsibilidade
Já cantei a bola antes, quando a gente é júnior, a gente ama ferramenta que faz coisas por nós. A gente ama a mágica. Quando a gente vai ficando mais sênior (ranzinza?), a mágica começa a cobrar juros. Você começa a preferir o código que diz exatamente o que vai acontecer, mesmo que ele seja menos elegante.
Abandonar Observers foi desconfortável no começo. Eu sentia que estava escrevendo “boilerplate”. Só que esse “boilerplate” virou uma coisa muito mais útil: documentação viva. É código que eu consigo ler, entender e debugar sem precisar de um mapa mental do sistema inteiro de eventos. Se você está cansado de efeito colateral quebrando fluxo, dá uma olhada com carinho nos seus Observers.
Principalmente no created.
Principalmente naquele que “só faz uma coisinha”.
E, se fizer sentido, traz isso pra um Service ou Action e deixa o fluxo explícito.