Ir para o conteúdo
Blog Papo Dev

O Controller que virou um monstro (e como a gente parou de criar esses)

De 10 para 800 linhas: o ciclo de vida perigoso de um sistema Laravel mal estruturado. Descubra como aplicamos o Gateway Pattern para isolar integrações complexas e manter o código limpo e sustentável.

6 min de leitura

Fat Controller não é praticidade — é dívida técnica com juros compostos. E quando você adiciona integrações externas no meio disso, a conta chega mais rápido do que você imagina.


Existe um momento muito específico no ciclo de vida de todo sistema Laravel.

Você começa o projeto com boas intenções. O Controller está limpo. Tem dez linhas. Funciona.

Aí vem o cliente com uma nova regra. Depois outra. Depois a integração com o gateway de pagamento. Depois o webhook. Depois o log. Depois a notificação.

Três meses depois você abre o mesmo arquivo e ele tem 800 linhas. Você não consegue mais ler sem perder o fio. Qualquer mudança parece um jogo de campo minado.

Isso tem nome: Fat Controller. E a maioria dos projetos que chegam até nós para "dar uma melhorada" está cheio deles.


O problema não é o tamanho. É o que está dentro.

Um Controller grande não é necessariamente um problema de estética. O problema é o que ele passou a fazer que não deveria:

  • Validar dados

  • Aplicar regras de negócio

  • Chamar APIs externas diretamente

  • Formatar respostas

  • Enviar e-mails

  • Atualizar o banco

Tudo junto. Tudo misturado. Sem separação.

Quando isso acontece, você perde a capacidade de testar, reutilizar e, principalmente, de entender o que o código está fazendo sem ler cada linha do começo ao fim.

Mais grave ainda: quando chega uma mudança — e ela sempre chega — você não sabe exatamente onde mexer sem quebrar outra coisa.

Isso não é praticidade. É dívida técnica com juros compostos.


A saída que a gente usa: Services e Actions

A primeira mudança que fazemos em todo projeto que construímos ou recebemos para evoluir é tirar a lógica do Controller.

O Controller tem uma única responsabilidade: receber uma requisição e devolver uma resposta. Só isso.

A lógica de negócio vai para uma Service. Um caso de uso específico vai para uma Action. Os dados transitam como DTOs tipados — nunca como arrays soltos que você não sabe o que contém.

Na prática, um Controller nosso parece com isso:

class LeadController extends Controller
{
    public function store(StoreLeadRequest $request, LeadService $service): Response
    {
        $lead = $service->create(LeadData::from($request->validated()));
    return Inertia::render('Leads/Show', ['lead' => $lead]);
}

}

Quatro linhas. Sem surpresa. Sem medo de abrir o arquivo.


Mas aí chegou a integração externa. E tudo ficou feio de novo.

Esse é o segundo problema que a gente vê repetir em todo projeto que tem alguma integração com API externa — gateway de pagamento, CRM, ERP, serviço de e-mail, o que for.

A solução mais comum, especialmente quando o projeto já está corrido, é chamar a API diretamente de dentro da Service. Ou pior — do Controller.

// O que a gente vê frequentemente
$response = Http::withToken(config('asaas.key'))
->post('https://api.asaas.com/v3/customers', [
'name' => $data->name,
'cpfCnpj' => $data->document,
]);

Funciona? Funciona. Mas agora sua lógica de negócio está acoplada a uma API externa. Se o Asaas mudar um endpoint, você vai caçar esse código em vários arquivos. Se você quiser testar a Service em isolamento, vai precisar mockar chamadas HTTP. Se você quiser trocar de gateway um dia, o projeto vira uma cirurgia.


A solução: Gateway Pattern

O que a gente faz é simples, mas muda tudo: criar uma camada dedicada para cada integração externa.

Um namespace próprio. Uma responsabilidade única: falar com aquela API específica e devolver dados no formato que o restante da aplicação entende.

A estrutura fica assim:

app/
Gateways/
Asaas/
AsaasGateway.php       ← o contrato (interface)
AsaasClient.php        ← a implementação real
Schemas/
CreateCustomerSchema.php   ← o que enviamos
CustomerResponse.php       ← o que recebemos
Mappers/
CustomerMapper.php         ← transforma resposta em DTO interno

A sua Service fala com o Gateway — não com a API diretamente. O Gateway fala com a API e devolve um DTO interno. A API externa nunca "vaza" para o resto do sistema.

Na prática:

final readonly class FinanceiroService
{
public function __construct(
private AsaasGateway $gateway
) {}

public function registrarCliente(ClienteData $data): Cliente
{
    $schema = new CreateCustomerSchema(
        name: $data->nome,
        cpfCnpj: $data->documento,
    );

    $response = $this->gateway->createCustomer($schema);

    return CustomerMapper::toInternal($response);
}

}

A Service não sabe nada sobre HTTP, sobre tokens, sobre a estrutura de resposta do Asaas. Ela fala com um contrato. O contrato pode ser trocado, mockado em testes, ou substituído por outro gateway amanhã — sem tocar uma linha da lógica de negócio.


O que isso muda na prática

Quando implementamos esse padrão no Omniaflow — um ERP educacional multi-tenant com integração profunda no Asaas para PIX, boletos e conciliação via webhooks — a diferença foi imediata.

Qualquer mudança no gateway fica contida. Os testes das regras financeiras rodam sem fazer uma única chamada HTTP real. E o desenvolvedor que abre o código pela primeira vez consegue entender o fluxo sem precisar caçar onde a API é chamada.

Isso não é over-engineering. É o custo de construir algo que vai durar.


Resumindo para levar

Fat Controller não é um estilo de programar — é uma dívida que você vai pagar mais tarde, com juros, quando o projeto estiver maior e mais difícil de mudar.

Gateway Pattern não é burocracia — é a diferença entre um sistema acoplado a um fornecedor externo e um sistema que controla suas próprias dependências.

As duas coisas têm o mesmo princípio por baixo: separar o que muda do que não muda. Regra de negócio muda. API externa muda. O fluxo principal do sistema deve ser protegido das duas.

É isso que a gente aplica em todo projeto que sai daqui.


Esse post faz parte da série de insights do blog da MC — pensamentos diretos sobre desenvolvimento, arquitetura e o que aprendemos construindo sistemas reais.

Tem um sistema que cresceu além do controle? Fala com a gente.

#Laravel 12 #Clean Code
Compartilhar:
Flavio Moreira

Escrito por

Flavio Moreira

Desenvolvedor e fundador da mktcode. Apaixonado por transformar ideias em produtos digitais de alta performance.