Equipes que desenvolvem aplicativos SaaS com Django tendem a repetir um conjunto de deslizes técnicos que parecem inofensivos no início e se tornam caros em produção. Esses equívocos raramente geram erros imediatos; eles se acumulam silenciosamente, afetam desempenho, vazam dados ou criam instabilidade sob carga real.
Ao mapear esses padrões, torna-se possível preveni-los com práticas simples, porém sistemáticas. As seções a seguir apresentam dez erros frequentes, por que acontecem e formas objetivas de corrigi-los. O foco recai em conceitos fundamentais de arquitetura, concorrência, isolamento entre clientes e integrações externas. Cada correção privilegia uma estrutura mais clara de responsabilidades e um caminho de execução previsível.
Erro 1: Regra de negócio dentro de views
Colocar a lógica de negócio diretamente na view mistura validação, acesso a dados, integração externa e orquestração de tarefas no mesmo lugar. Essa mistura dificulta testes, duplica regras e torna mudanças arriscadas. Um indício recorrente é a necessidade de montar todo o stack HTTP para validar uma única regra de negócio. O efeito prático é a propagação de bugs e acoplamento excessivo entre camadas.
A solução é concentrar a lógica em uma camada de serviço, deixando a view como mera orquestradora de entrada e saída. Esse serviço pode ser reutilizado por comandos de gerenciamento e tarefas assíncronas. Testes passam a validar o comportamento de forma direta, sem depender do ciclo completo de requisição. O fluxo de execução fica rastreável e previsível.
O exemplo a seguir mostra primeiro a lógica embutida na view e, em seguida, a delegação correta a um serviço.
# Antes — regra de negócio espalhada na view
class VisualizacaoAssinatura(APIView):
def post(self, requisicao):
plano = requisicao.data.get('plano')
cliente = stripe.Customer.create(email=requisicao.user.email)
assinatura = stripe.Subscription.create(customer=cliente.id, ...)
requisicao.user.tenant.plan = plano
requisicao.user.tenant.save()
enviar_email_confirmacao(requisicao.user)
return Response({'status': 'assinado'})
# Depois — view delega para um serviço testável
class VisualizacaoAssinatura(APIView):
def post(self, requisicao):
resultado = ServicoAssinatura.criar(
inquilino=requisicao.user.tenant,
plano=requisicao.data.get('plano'),
)
return Response(resultado)
class ServicoAssinatura:
@staticmethod
def criar(inquilino, plano):
cliente = stripe.Customer.create(email=inquilino.usuario.email)
stripe.Subscription.create(customer=cliente.id, ...)
inquilino.plan = plano
inquilino.save()
enviar_email_confirmacao(inquilino.usuario)
return {'status': 'assinado'}
Erro 2: Falta de escopo de inquilino (tenant) em QuerySets
Em um SaaS multi-inquilino, todo acesso a dados específicos deve ser filtrado pelo inquilino atual. A ausência desse escopo expõe dados entre clientes e cria brechas de segurança. Outro erro sutil é confiar no identificador de inquilino recebido do cliente, o que permite consultas cruzadas. O problema só aparece quando há dados reais e usuários curiosos.
O padrão seguro é centralizar o filtro do inquilino na camada de acesso, preferencialmente em uma classe base de viewset. Essa abordagem garante que cada consulta considere o inquilino autenticado, sem depender da entrada do cliente. A medida reduz risco de vazamento e simplifica auditoria. O comportamento torna-se consistente em toda a aplicação.
Os trechos a seguir mostram primeiro consultas sem escopo e depois a forma correta com filtro pelo inquilino autenticado.
# Incorreto — sem escopo de inquilino
def get_queryset(self):
return Projeto.objects.all()
# Incorreto — confia no dado do cliente
def get_queryset(self):
inquilino_id = self.request.data.get('inquilino_id')
return Projeto.objects.filter(inquilino_id=inquilino_id)
# Correto — usa o inquilino autenticado
def get_queryset(self):
return Projeto.objects.filter(inquilino=self.request.user.tenant)
Erro 3: Consultas N+1 em serializers
O problema de N+1 consultas ocorre quando cada item serializado aciona novas idas ao banco. Em Django REST Framework, campos calculados em serializers mascaram o custo até que o volume de dados cresça. O resultado são centenas de consultas extras para uma única página. O desempenho degrada de forma não linear.
O remédio é preparar o queryset com joins e pré-buscas, evitando que o serializer toque o banco novamente. Métodos como select_related e prefetch_related carregam relacionamentos de forma eficiente. A serialização então opera sobre dados em memória, com uso previsível. Essa abordagem estabiliza a latência sob carga.
O exemplo abaixo mostra a origem da N+1 e a correção com pré-carregamento no queryset.
# Incorreto — acessa relações por item
class PedidoSerializer(serializers.ModelSerializer):
nome_cliente = serializers.SerializerMethodField()
itens = serializers.SerializerMethodField()
def get_nome_cliente(self, obj):
return obj.cliente.nome # consulta por pedido
def get_itens(self, obj):
return [p.nome for p in obj.produtos.all()] # consulta por pedido
# Correto — pré-carrega no queryset da view
class PedidoViewSet(viewsets.ModelViewSet):
queryset = Pedido.objects.all()
def get_queryset(self):
return (
Pedido.objects
.select_related('cliente')
.prefetch_related('produtos')
)
Erro 4: Tarefas Celery que recebem instâncias de modelos
Enviar uma instância de modelo para uma fila Celery parece prático, mas introduz dados obsoletos. A serialização ocorre no momento do enqueue e a desserialização no worker, que pode rodar minutos depois. Nesse intervalo, o registro pode ter mudado ou sido removido. O efeito é processamento com informações antigas sem alertas claros.
A prática segura é enviar apenas o identificador e buscar o registro mais recente dentro da tarefa. Essa estratégia mantém a tarefa alinhada ao estado atual do banco. A abordagem também reduz o volume de dados trafegando na fila. O comportamento fica determinístico e fácil de depurar.
O trecho a seguir ilustra o antipadrão e, em seguida, a forma correta com busca interna.
# Incorreto — envia instância inteira
processar_pedido.delay(pedido)
# Correto — envia apenas o ID e busca dentro da tarefa
processar_pedido.delay(pedido.id)
@shared_task
def processar_pedido(pedido_id):
pedido = Pedido.objects.get(id=pedido_id)
# processa com dados atualizados
Erro 5: Sinais post_save com regra de negócio
Usar sinais para acoplar ações a eventos de modelo parece elegante, porém espalha regras por caminhos implícitos. Um post_save pode disparar em testes, no admin, em migrações e em comandos, exigindo inúmeras guardas. O rastreamento do que acontece em cada salvamento torna-se difícil. Mudanças em fluxo de cadastro passam a exigir conhecimento de vários arquivos de sinal.
Concentrar a regra em um serviço explícito reduz surpresas e facilita teste. A criação de entidades relacionadas, integração com provedores externos e envio de e-mails ficam em um único método. O controle de quando executar a lógica é intencional, sem disparos laterais. A manutenção torna-se mais previsível.
O exemplo abaixo mostra a remoção da regra do sinal e sua migração para um serviço de usuários.
# Incorreto — muita regra em um sinal
@receiver(post_save, sender=Usuario)
def ao_criar_usuario(sender, instance, created, **kwargs):
if created:
stripe.Customer.create(email=instance.email)
enviar_email_boas_vindas(instance)
EstadoOnboarding.objects.create(usuario=instance)
criar_espaco_padrao(instance)
# Correto — serviço explícito chamado pelo fluxo de cadastro
class ServicoUsuario:
@staticmethod
def criar(dados):
usuario = Usuario.objects.create(**dados)
stripe.Customer.create(email=usuario.email)
enviar_email_boas_vindas(usuario)
EstadoOnboarding.objects.create(usuario=usuario)
criar_espaco_padrao(usuario)
return usuario
Erro 6: Ausência de select_for_update em escritas concorrentes
Condições de corrida aparecem quando duas requisições validam um limite ao mesmo tempo e ambas passam. A leitura de contagem seguida da criação sem bloqueio resulta em ultrapassagem de cotas. Em ambientes paralelos, isso ocorre esporadicamente e é difícil de reproduzir. O impacto surge como dados incoerentes e regras violadas.
O uso de transação com select_for_update bloqueia a linha relevante até o commit. A segunda requisição aguarda, lê o estado atualizado e respeita o limite. Essa técnica é essencial ao aplicar regras de cota, estoque e saldo. O custo do bloqueio é menor que o custo de corrigir dados corrompidos.
O código a seguir demonstra o cenário de corrida e a correção com bloqueio de linha.
# Incorreto — sujeito a condição de corrida
def adicionar_membro(inquilino, usuario):
total = MembroEquipe.objects.filter(inquilino=inquilino).count()
if total >= inquilino.plano.limite_membros:
raise LimiteExcedido()
MembroEquipe.objects.create(inquilino=inquilino, usuario=usuario)
# Correto — transação e bloqueio da linha do inquilino
from django.db import transaction
def adicionar_membro(inquilino, usuario):
with transaction.atomic():
inquilino_bloqueado = (
Inquilino.objects.select_for_update().get(id=inquilino.id)
)
total = MembroEquipe.objects.filter(inquilino=inquilino_bloqueado).count()
if total >= inquilino_bloqueado.plano.limite_membros:
raise LimiteExcedido()
MembroEquipe.objects.create(inquilino=inquilino_bloqueado, usuario=usuario)
Erro 7: Enfileirar tarefas Celery antes do commit
Agendar uma tarefa dentro de uma transação ativa pode dispará-la antes do registro existir no banco. O worker tenta buscar o objeto e recebe inexistência intermitente. Esse defeito depende da velocidade do worker e raramente aparece em ambientes locais. Em produção, sob carga, torna-se fonte de erros difíceis de rastrear.
O gancho transaction.on_commit garante que o agendamento ocorra somente após o commit bem-sucedido. Com isso, a tarefa sempre encontra os dados persistidos. O comportamento deixa de ser temporalmente frágil. A operação fica correta mesmo com workers rápidos.
O exemplo a seguir contrasta o agendamento precoce com o uso do gancho pós-commit.
# Incorreto — agenda antes do commit
from django.db import transaction
def criar_pedido(dados, inquilino):
with transaction.atomic():
pedido = Pedido.objects.create(inquilino=inquilino, **dados)
processar_pedido.delay(pedido.id) # pode rodar cedo demais
# Correto — agenda após o commit
def criar_pedido(dados, inquilino):
with transaction.atomic():
pedido = Pedido.objects.create(inquilino=inquilino, **dados)
transaction.on_commit(lambda: processar_pedido.delay(pedido.id))
Erro 8: Regras de plano codificadas no código
Verificações como “plano pró ou empresarial” espalhadas pelo código envelhecem mal. A adição de um novo plano ou mudança de benefícios exige buscas e edições manuais. Pontos esquecidos viram falhas de autorização em funcionalidades pagas. O resultado é suporte reativo e bugs no dia do lançamento.
Uma abordagem robusta é modelar recursos do plano no banco e consultá-los por chave. A lógica passa a perguntar “tem o recurso X?” em vez de “é o plano Y?”. A criação ou ajuste de planos vira operação de dados, sem alteração de código. A consistência cresce e o risco de regressão cai.
O trecho a seguir apresenta a verificação frágil por slug e a alternativa baseada em recurso.
# Incorreto — checagem por nome do plano
def exportar(requisicao):
if requisicao.user.tenant.plan not in ('pro', 'enterprise'):
return Response({'erro': 'Upgrade necessário'}, status=403)
# lógica de exportação
# Correto — checagem por recurso de plano
class ServicoAssinatura:
@staticmethod
def tem_recurso(inquilino, chave_recurso):
return RecursoPlano.objects.filter(
plano=inquilino.plan, chave=chave_recurso, ativo=True
).exists()
def exportar(requisicao):
if not ServicoAssinatura.tem_recurso(requisicao.user.tenant, 'exportacao_csv'):
return Response({'erro': 'Upgrade necessário'}, status=403)
# lógica de exportação
Erro 9: Configurações de DEBUG vazando em produção
O problema não se limita a manter DEBUG ativo; configurações condicionais a DEBUG podem mascarar comportamentos críticos. Um backend de e-mail de console sob a bandeira de DEBUG interrompe entregas reais se um ambiente for configurado incorretamente. O incidente passa despercebido até que usuários relatem falhas. O risco é alto para notificações de segurança e cobrança.
A prática segura é separar arquivos de configuração por ambiente sem condicionais. Produção define explicitamente o backend real e dependências necessárias. Desenvolvimento usa ferramentas e atalhos locais sem chance de escapar para produção. O caminho de execução das configurações torna-se inequívoco.
Os trechos abaixo ilustram a armadilha condicional e a organização por ambientes.
# Incorreto — comportamento muda com DEBUG
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Correto — arquivos de settings por ambiente
# settings/producao.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# settings/desenvolvimento.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Erro 10: Ausência de timeout em requisições HTTP externas
Chamadas externas sem timeout podem bloquear workers indefinidamente quando o provedor degrada. Em cenários de incidente, o bloqueio se multiplica até esgotar processos do servidor de aplicação. O efeito cascata derruba o serviço principal por dependências lentas. A recuperação exige reinícios e redução de carga.
Definir tempos de conexão e leitura limita o impacto de degradações. Timeouts explícitos viram requisito de revisão de código para toda chamada externa. Valores razoáveis protegem o sistema sem gerar falsos positivos. O comportamento degradado passa a ser controlado, não catastrófico.
O exemplo abaixo mostra uma chamada sem proteção e a alternativa com tempos separados de conexão e leitura.
# Incorreto — sem prazos definidos
resposta = requests.post(
'https://api.exemplo.com/v1/acao',
data=payload,
headers=cabecalhos,
)
# Correto — tempo de conexão e de leitura explícitos
resposta = requests.post(
'https://api.exemplo.com/v1/acao',
data=payload,
headers=cabecalhos,
timeout=(5, 30), # 5s para conectar, 30s para ler
)
O padrão por trás dos dez erros
Todos os problemas compartilham uma mesma raiz: algo que funciona em desenvolvimento e falha no mundo real. Vazamento entre inquilinos aparece quando existe dado sensível; N+1 explode quando há volume; tarefas assíncronas tropeçam no tempo de transação; integrações externas travam quando parceiros estão lentos. A produção expõe caminhos implícitos, acoplamentos e suposições otimistas.
Isolar regras em serviços, aplicar escopo de inquilino por padrão, usar pré-carregamento de relações, respeitar transações, bloquear linhas críticas, projetar planos por recursos e impor timeouts são práticas que tornam o comportamento previsível. Comportamentos previsíveis facilitam testes e observabilidade. O resultado é um SaaS mais estável, seguro e econômico. A prevenção custa menos do que corrigir sob pressão.