O problema
Um sistema fiscal raramente atende uma empresa só. Escritório de contabilidade opera dezenas de clientes; grupo com matriz e filiais precisa que cada CNPJ veja apenas o que é seu. Nesse contexto, vazamento de dado entre empresas não é um defeito de interface — é problema fiscal, com consequência legal.
Esse requisito foi o que decidiu a arquitetura.
Monólito modular, não microsserviços
Oito módulos com fronteira explícita — IdentityAccess, Organizations,
Invoicing, TaxReporting, Certificates, FiscalProviders,
Notifications, Auditing — cada um com o próprio DbContext. A decisão
está registrada em ADR-0001, com o motivo: o custo operacional de rede,
observabilidade e transação distribuída não se paga num sistema que ainda não tem
escala para justificá-lo, e a fronteira de módulo bem feita permite extrair um
serviço depois, se a necessidade aparecer.
Isolamento de tenant
A separação por empresa é feita em camadas, e não numa só:
- A aplicação filtra por organização em toda consulta.
- A autorização confere o vínculo do usuário com a organização antes de servir.
- No banco, cada tabela tem policy de Row Level Security escrita à mão em migração — o Prisma e o EF Core não expressam policy, então isso vive em SQL dentro do histórico de migração, senão vira desvio silencioso.
Sobre a terceira camada, o estado atual, sem maquiagem: as policies existem no banco, mas a conexão da aplicação ainda usa uma role capaz de contorná-las, o que as deixa inertes. Está registrado na spec 012, com as perguntas em aberto e uma ordem de execução em que a troca da conexão só acontece depois de o interceptador existir.
Preferi documentar isso a entregar meia trava. RLS pela metade é pior que RLS nenhuma: dá a sensação de proteção e derruba a aplicação inteira no dia em que a role muda.
Erro esperado não é exceção
Regra de negócio violada devolve Result<T>, não lança (ADR-0003). Nota fiscal
recusada pela SEFAZ, certificado expirado e CNPJ inválido são resultados
previstos do domínio: modelá-los como exceção esconde o caminho de erro de quem
lê o código e transforma controle de fluxo em captura genérica algumas camadas
acima.
Valores fiscais no núcleo compartilhado
CNPJ, percentual e base de cálculo são value objects no SharedKernel
(ADR-0004), com validação num lugar só. Dois detalhes que essa escolha resolveu:
CNPJ alfanumérico, que entra em vigor e quebra validação por dígito (ADR-0007), e
numeração de RPS, que tem regra própria por município (ADR-0005).
Um defeito que mostra por que precisão fiscal exige cuidado
O value object de percentual arredondava assim:
FiscalMath.RoundRate(value / 100m) * 100mA coluna é numeric(7,4) e guarda o percentual na forma humana — 4 casas
decimais disponíveis. Dividir por 100 antes de arredondar gastava duas dessas
casas no arredondamento e multiplicava o resultado de volta: 5,1234% era gravado
como 5,1200%.
Num sistema que apura imposto sobre milhares de notas, erro na quarta casa não fica na quarta casa. Comprovei o comportamento antes de mexer, corrigi para arredondar o valor direto e cobri com 18 testes — o arquivo de teste que a própria documentação do projeto exigia e que não existia.
Qualidade imposta pelo compilador
TreatWarningsAsErrors com analisadores Roslyn e .editorconfig. Aviso não
vira dívida técnica porque não compila. Custa disciplina no começo e economiza a
discussão sobre "depois a gente limpa".
Desenvolvimento guiado por especificação
Cada bloco de trabalho tem uma spec numerada com critério de aceite, e cada decisão de arquitetura tem um ADR. O que isso muda na prática: quando uma decisão é revista, o registro antigo não é apagado — ele é marcado como substituído, e o novo cita o gatilho. Fica legível por que o sistema é como é.
Stack
.NET 9 e C# no backend, PostgreSQL, Angular 22 no frontend com componentes standalone e sem zone.js, Docker Compose para subir tudo.