Antes de aprovar um rewrite, eu rodo três medições no legado (e quase sempre elas matam a ideia)
A pergunta "When is a BIG Rewrite the answer?" está no Software Engineering Stack Exchange há mais de uma década, com 80.775 views, score 325 e 25 respostas. Vinte e cinco respostas para uma pergunta que, na maioria dos casos, tem resposta curta: não é.
Eu já peguei uns oito sistemas depois que o fornecedor anterior virou as costas. Em todos eles, alguém do time interno já tinha proposto reescrever do zero. Em dois eu concordei. Nos outros seis, o que resolveu foi medir antes de decidir.
O problema de decidir rewrite no olho é que o argumento nunca chega em número. Chega em "esse código é impossível de manter". Isso pode ser verdade e pode ser só o desconforto de ler código escrito por outra pessoa. Então eu rodo três medições antes de dar opinião. Numa base que eu nunca vi, leva mais ou menos meio dia.
1. Onde a mudança se concentra
Reescrever tudo é caro porque você paga por código que ninguém toca. A primeira medida é descobrir qual fração da base realmente se mexe.
git log --since="18 months ago" --name-only --pretty=format: \
| grep -E '\.(ts|js|py|rb|java)$' \
| sort | uniq -c | sort -rn | head -12
Saída de um sistema que me pediram para reescrever no ano passado, com os caminhos trocados:
412 src/billing/invoice-service.ts
388 src/billing/subscription.ts
201 src/billing/tax.ts
177 src/api/webhooks/payment.ts
94 src/reports/export.ts
61 src/auth/session.ts
58 src/billing/dunning.ts
33 src/api/customers.ts
A base tinha 1.910 arquivos versionados. Os doze primeiros concentravam perto de 60% dos commits de dezoito meses, e cinco deles eram do mesmo módulo de cobrança. Ninguém ali precisava reescrever 1.910 arquivos. Precisava reescrever cobrança.
Depois eu cruzo com quantas pessoas diferentes tocaram o arquivo mais quente:
git log --since="18 months ago" --format='%an' \
-- src/billing/invoice-service.ts | sort -u
Voltaram dois nomes. Dois autores no arquivo que mais muda na empresa inteira, e um deles tinha saído fazia sete meses. Esse número muda a conversa: o buraco no arquivo mais quente da base é bus factor, e rewrite não cura bus factor. Rewrite feito pelas mesmas duas pessoas produz o mesmo gargalo com sintaxe nova.
2. A cobertura mente, o mutation score não
Segunda medida. Aquela base tinha 92% de cobertura de linha, e o time usava esse número como argumento a favor do rewrite: a suíte pega tudo, então dava para trocar o motor com segurança. Aí eu abri os testes.
it('calcula o imposto da fatura', async () => {
const invoice = await service.create({ items, customer })
await service.applyTax(invoice)
// TODO: assert
})
it('nao permite fatura duplicada', async () => {
await service.create({ items, customer, idempotencyKey: 'abc' })
await service.create({ items, customer, idempotencyKey: 'abc' })
expect(true).toBe(true)
})
Teste sem expect passa sempre, cobre linha e garante nada. E não era um caso isolado, era o padrão da pasta. Rodei mutation testing para ter o número real:
--------------------|---------|--------|----------|-----------
File | % score | killed | survived | timeout
--------------------|---------|--------|----------|-----------
All files | 41.30 | 317 | 451 | 12
billing/ | 22.10 | 48 | 169 | 3
auth/ | 78.40 | 109 | 30 | 1
reports/ | 55.70 | 71 | 55 | 2
--------------------|---------|--------|----------|-----------
Cobertura 92, mutation score 41. No módulo de cobrança, 22.
Essa medida não decide entre reescrever e refatorar. Ela decide se você tem direito de encostar em qualquer coisa. Com 22% de mutation score no módulo que mais muda, refactor e rewrite são igualmente cegos: você não tem como saber se quebrou a regra de imposto de 2019 que só aparece em três clientes antigos. O primeiro trabalho vira escrever teste de caracterização em cima do comportamento atual, inclusive nas partes em que o comportamento atual está errado.
3. O que o banco está fazendo enquanto você discute arquitetura
Terceira. Em quase todo sistema que me pediram para reescrever "porque não escala", o gargalo estava a um EXPLAIN de distância.
EXPLAIN ANALYZE
SELECT i.id, i.total, c.name
FROM invoices i
JOIN customers c ON c.id = i.customer_id
WHERE i.status = 'open' AND i.due_date < now()
ORDER BY i.due_date
LIMIT 50;
Limit (actual time=4128.902..4128.919 rows=50 loops=1)
-> Sort (actual time=4128.899..4128.908 rows=50 loops=1)
Sort Key: i.due_date
Sort Method: top-N heapsort Memory: 32kB
-> Nested Loop (actual time=0.412..4093.771 rows=43991 loops=1)
-> Seq Scan on invoices i (actual time=0.038..612.447 rows=43991 loops=1)
Filter: ((status = 'open') AND (due_date < now()))
Rows Removed by Filter: 1284509
-> Index Scan using customers_pkey on customers c
(actual time=0.061..0.062 rows=1 loops=43991)
Planning Time: 0.244 ms
Execution Time: 4129.061 ms
Seq scan descartando 1,28 milhão de linhas e um index scan repetido 43.991 vezes, uma vez por fatura. Um índice parcial em (due_date) com filtro de status e a troca do loop por um join carregado de uma vez levaram a execução para 21 ms. Isso não é rewrite, é uma tarde de trabalho.
A parte incômoda: o time tinha passado seis semanas discutindo se migrava para outro banco.
O ritmo do resgate
Minha analogia de sempre é médica, porque ela segura expectativa melhor que qualquer roadmap. Estancar o sangramento é rápido, dá para fazer na primeira semana. Descobrir a origem do sangramento demora mais, porque exige instrumentar o que ninguém instrumentou. Tratar a doença é remédio de meses, e nesse período o produto continua tendo que entregar feature.
Quem decide rewrite completo geralmente está tentando pular as duas primeiras etapas e comprar a terceira de uma vez. Nunca vi essa compra sair pelo preço anunciado.
As duas vezes em que eu disse sim
A primeira foi um runtime sem caminho de upgrade. Versão da linguagem fora de suporte, CVE aberta no framework, e a biblioteca de pagamento nova exigindo uma versão que quebrava metade das dependências transitivas. Ali a decisão saiu da minha mão, porque o custo de ficar era jurídico antes de ser técnico.
A segunda foi mudança de domínio. Um sistema single-tenant, com o identificador da empresa espalhado por gambiarra em vez de existir no modelo, depois que o negócio passou a operar 40 e poucas filiais com dado isolado. Reescrever o núcleo saiu mais barato que enfiar tenancy em 300 queries.
Nos dois casos o que rodou foi strangler fig: rota por rota atrás de um proxy, os dois sistemas no ar ao mesmo tempo, corte de tráfego por percentual. Big bang com data marcada eu só vi funcionar em apresentação de slide.
Onde eu posso estar errado
Tudo isso vale para sistema com histórico de git decente e usuário pagante em produção. Se a sua base tem seis meses, três devs e o produto ainda está procurando encaixe, jogar fora pode ser mais barato do que medir. Não sei dizer o corte exato. Chuto alguma coisa perto de doze meses de produção, e chuto sem número por trás.
Pergunta honesta para quem passou por isso: alguém aqui fez big rewrite big bang que deu certo? Se deu, queria entender o que tinha de diferente. Congelaram feature durante o período, o sistema antigo ficou no ar em paralelo, ou o time era grande o bastante para tocar os dois?
A versão para quem precisa levar essa decisão ao board está no blog da Revin: https://revin.com.br/pt/blog/conversa-dificil-como-o-cto-comunica-que-o-produto-precisa-de-rewrite