Ir para o conteúdo
Blog Papo Dev

Dois padrões que salvam sistemas que precisam crescer: DDD e RBAC

Quando um sistema começa a crescer, dois problemas aparecem quase sempre juntos: a lógica de negócio começa a vazar para lugares onde não deveria estar, e o controle de quem acessa o quê vira um campo minado de condicionais espalhadas pelo código. DDD tático e RBAC não são conceitos acadêmicos — são as duas decisões arquiteturais que mais impactam a capacidade de um sistema de crescer sem se tornar impossível de manter.

7 min de leitura

Todo sistema começa pequeno. E todo sistema pequeno parece não precisar de arquitetura.

Aí ele cresce. O cliente pede uma nova regra. Depois outra. Entra um segundo perfil de usuário. Depois um terceiro. O código que estava "funcionando bem" começa a acumular condicionais, duplicações e aquela sensação crescente de que mexer em qualquer coisa pode quebrar outra.

Dois problemas surgem cedo nesse processo — e quase sempre juntos.

O primeiro: a lógica de negócio está em todo lugar. No Controller, na View, no Model, no middleware, no helper que alguém criou numa sexta-feira.

O segundo: o controle de acesso virou uma colcha de retalhos. if ($user->role === 'admin') repetido em dez lugares diferentes. Permissões hardcoded. Ninguém mais sabe ao certo quem pode fazer o quê.

No CIM Base, resolvemos os dois de forma deliberada desde o início. Aqui está como.


Parte 1 — DDD Tático: tirar a lógica do Controller de verdade

Domain-Driven Design é um tema que assusta. Livros enormes, terminologia pesada, diagramas complexos. Mas o que interessa na prática cotidiana de um sistema Laravel é o DDD tático — um conjunto de padrões concretos que mudam como você organiza o código.

A premissa é simples: o Controller não deve saber como o negócio funciona. Ele só recebe e responde.

O problema que isso resolve

Imagine um Controller que cria um lead:

public function store(Request $request): Response
{
    $request->validate([...]);
$lead = Lead::create([
    'name' => Str::title($request->name),
    'email' => strtolower($request->email),
    'status' => 'novo',
    'origin' => $request->origin ?? 'site',
]);

// notifica o comercial
Mail::to('comercial@empresa.com')->send(new NovoLeadMail($lead));

// registra no log
activity()->on($lead)->log('criado');

return Inertia::render('Leads/Show', ['lead' => $lead]);

}

Funciona. Mas agora essa lógica existe apenas aqui. Se você precisar criar um lead de outro lugar — via importação, via API, via webhook — vai copiar esse código? Vai extrair para um helper? Vai lembrar de fazer tudo isso nas três versões?

E quando a regra mudar — e ela vai mudar — quantos lugares você vai precisar atualizar?

A solução: Service + DTO + Evento

No CIM Base, a criação de um lead passa por uma LeadService que concentra toda essa responsabilidade:

final readonly class LeadService
{
public function create(LeadData $data): Lead
{
return DB::transaction(function () use ($data) {
$lead = Lead::create([
'name'   => Str::title($data->name),
'email'  => strtolower($data->email),
'status' => LeadStatus::Novo,
'origin' => $data->origin ?? LeadOrigin::Site,
]);

        LeadCreatedEvent::dispatch($lead);

        return $lead;
    });
}

}

E o Controller volta para o que é seu:

public function store(StoreLeadRequest $request, LeadService $service): Response
{
$lead = $service->create(LeadData::from($request->validated()));

return Inertia::render('Leads/Show', ['lead' => $lead]);

}

Seis linhas. Sem lógica. Sem surpresa.

O e-mail e o log saem do Controller e vão para um LeadCreatedListener — desacoplado, testável, substituível sem tocar no fluxo principal.

O que muda na prática

Quando você precisa criar um lead via importação de CSV, chama $service->create(). Quando vem de um webhook externo, chama $service->create(). A regra de negócio existe em um lugar só. Muda em um lugar só. É testada em um lugar só.

Isso não é elegância por elegância — é a diferença entre uma refatoração de uma hora e uma investigação de dois dias.


Parte 2 — RBAC: controle de acesso que escala com a equipe

RBAC significa Role-Based Access Control — controle de acesso baseado em papéis. A ideia é que permissões não são atribuídas diretamente a usuários, mas a roles (perfis), e usuários recebem roles.

Parece óbvio. Mas a maioria dos sistemas não faz isso — faz outra coisa disfarçada de RBAC.

O falso RBAC que todo sistema tem

// Em algum Controller
if ($user->role === 'admin') {
// pode fazer X
}

// Em outro lugar if ($user->type !== 'viewer') { // pode fazer Y }

// Em outro if (in_array($user->role, ['admin', 'manager'])) { // pode fazer Z }

Isso parece controle de acesso. Mas é uma armadilha.

Quando você precisa criar um novo perfil — digamos, supervisor, que pode fazer X e Z mas não Y — você vai caçar todas as condicionais do sistema? E quando as regras mudarem?

Além disso, esse código está espalhado. Não existe um lugar centralizado onde você consegue ver o que cada perfil pode fazer.

O RBAC real com Spatie Laravel Permission

No CIM Base, usamos spatie/laravel-permission. A lógica fica assim:

Permissões são declaradas de forma semântica:

// Permissões
'leads.view'
'leads.create'
'leads.edit'
'leads.delete'
'leads.assign'

Roles agrupam permissões:

$admin = Role::create(['name' => 'admin']);
$admin->givePermissionTo(['leads.view', 'leads.create', 'leads.edit', 'leads.delete', 'leads.assign']);

$comercial = Role::create(['name' => 'comercial']); $comercial->givePermissionTo(['leads.view', 'leads.create', 'leads.edit']);

$suporte = Role::create(['name' => 'suporte']); $suporte->givePermissionTo(['leads.view']);

E a verificação no código se torna declarativa:

// No Controller ou Policy
$this->authorize('leads.edit');

// No frontend (via Inertia) v-if="$page.props.auth.permissions.includes('leads.edit')"

O que muda na prática

Criar um novo perfil? Você cria uma role e atribui permissões existentes. Nenhuma linha de código de verificação muda.

Mudar o que um perfil pode fazer? Você ajusta as permissões da role. Nenhum if precisa ser caçado.

E o melhor: como as roles e permissões vivem no banco de dados, o próprio administrador do sistema consegue gerenciar isso pelo painel — sem abrir um ticket para o dev.

No CIM Base, o gestor da CIM adiciona um novo colaborador, atribui a role correta, e pronto. O sistema sabe exatamente o que aquele usuário pode ver e fazer.


DDD e RBAC juntos: por que eles se complementam

Os dois padrões partem do mesmo princípio: responsabilidades claras, fronteiras definidas.

DDD tático diz que a lógica de negócio tem um lugar certo para viver — e não é o Controller.

RBAC diz que o controle de acesso tem um lugar certo para viver — e não é um if espalhado pelo código.

Quando os dois estão presentes, o sistema ganha algo valioso: previsibilidade. Você sabe onde a regra está. Você sabe quem pode executá-la. E quando precisar mudar — e vai precisar — você sabe exatamente onde mexer.


Resumindo para levar

DDD tático não é para sistemas complexos — é para sistemas que vão crescer. Começar com Services e DTOs não é over-engineering, é o custo de não ter que reescrever tudo quando o segundo cliente aparecer.

RBAC não é para sistemas com muitos usuários — é para sistemas com mais de dois perfis. Se você tem admin e usuário comum, já precisa de RBAC. Tudo o que vier depois é só atribuição de permissões.

Os dois custam um pouco mais no início. E poupam muito mais ao longo do caminho.

É essa a conta que a gente faz antes de escrever a primeira linha.


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.

Está construindo algo que vai crescer? Fala com a gente.

#Laravel 12 #DDD #RBAC #Clean Code #PHP
Compartilhar:
Flavio Moreira

Escrito por

Flavio Moreira

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