Formato de data por cliente num SaaS multi-tenant
Todo cliente via data na convenção de um país só, e metade deles lia registros operacionais que podiam estar errados por meses.
O que a gente não tinha
O cliente não podia escolher como as datas apareciam para ele.
O produto foi feito para um mercado, e a convenção de data daquele mercado estava escrita no código em vez de guardada como configuração. Não existia um lugar único que decidisse como uma data deve aparecer — a decisão se repetia em centenas de pontos, espalhados por quatro bases de código.
O problema
O produto atendia clientes em vários países, e toda data aparecia no formato de um só.
- 03/07/2026 quer dizer duas coisas diferentes dependendo de onde você está. Metade da base lia datas que podiam estar erradas por meses.
- Eram registros operacionais — datas de consulta, de tratamento e de retorno. Data lida errado não é problema estético; tem consequência no mundo real.
- E travava venda. "Dá para ver as datas do nosso jeito?" aparecia em conversa com prospect, e a resposta era não.
O trabalho era muito maior que trocar uma string de formato. Os formatos estavam escritos em sete linguagens de token diferentes nas quatro stacks — o mesmo formato precisa ser expresso de um jeito para o backend, de outro para duas bibliotecas de front, para um seletor de data, para uma biblioteca de gráfico e para o próprio banco. 132 dessas strings viviam dentro de consultas de agregação no banco, em 66 arquivos: a data era formatada antes de a aplicação sequer vê-la. Os mesmos arquivos que exibiam data também gravavam data, e uma classificação errada corromperia dado gravado em silêncio. Além disso, todo cliente existente tinha que ver absolutamente nenhuma mudança, de forma demonstrável, no dia do lançamento.
Como eu resolvi
- Fiz uma configuração ser a fonte única da verdade, guardada por cliente, e gerei sete tabelas de tradução a partir dela — uma por linguagem de token. Depois disso não sobrou nenhum literal de formato em lugar nenhum do código.
- Classifiquei cada ponto de chamada como exibição ou armazenamento. Valor gravado, payload de API, string lida por máquina e chave de ordenação são intocáveis. Só muda o que um humano lê. Essa regra, aplicada sem exceção, foi o que impediu uma mudança de 515 arquivos de encostar em dado.
- Desenhei a funcionalidade para ser invisível até o cliente optar por ela. Todo fallback devolve o comportamento original, então dava para provar que o cliente existente não era afetado — foi isso que tornou seguro um lançamento único, em vez de liberação em fases.
- Travei as saídas de dados em formatos fixos. Cinco integrações são lidas por máquina de outras empresas. Se uma trocasse de convenção em silêncio, nada daria erro em lugar nenhum: o dado só chegaria errado. Cada uma ficou travada com um comentário dizendo por que nunca pode seguir a configuração do cliente.
- Separei o valor exibido da chave de ordenação. Formatar data dentro de consulta no banco transforma ela em texto, e ordenar depois disso ordena texto. Num formato que começa pelo dia, "ordenar por data" vira silenciosamente "ordenar por dia do mês" — bug que já existia, invisível no formato original, e certo de aparecer com o primeiro cliente internacional.
- Fiz a leitura de entrada também entender o formato, não só a saída, para que quem digita uma data na própria convenção seja lido corretamente.
- Escolhi a arquitetura pragmática em vez da limpa. Um serviço central de contexto era a resposta de manual e a errada para um legado no meio de uma refatoração. Cache por requisição no helper que já existia entregou uns 90% do benefício com uns 10% do raio de impacto.
- Escrevi as regras. Dez regras numeradas de tratamento de data, que viraram a referência do projeto, mais 152 comentários explicando por que uma decisão precisa continuar de pé — mirando em quem mexer nisso depois.
- Reorganizei a entrega em torno do produto, não dos repositórios. Depois de uma etapa de descoberta invalidar várias premissas do desenho original, quebrei o trabalho em 26 planos por área de funcionalidade, cada um atravessando as quatro bases e cada um destravado por um teste humano antes de o próximo começar.
Os resultados
Entregue num único lançamento, quatro pull requests, no mesmo dia — sem liberação em fases e sem precisar de feature flag.
- Escopo
- 4 repositórios, 515 arquivos, +6.737 / −3.796 linhas, 231 commits
- Tempo de construção e teste
- 30 dias, um desenvolvedor
- Rollbacks ou reverts
- 0
- Incidentes de corrupção ou perda de dado
- 0
- Erros de data em produção nos 30 dias seguintes
- 0
- Impacto no cliente existente
- Nenhuma mudança, por desenho
Achados e corrigidos antes de escrever qualquer código da funcionalidade: quatro bugs latentes, invisíveis no formato original, que apareceriam com o primeiro cliente internacional — incluindo o problema de ordenação no banco descrito acima.
Deixado para trás: a aplicação principal de front não tinha executor de teste nem testes quando eu comecei. Hoje tem os dois.