Destino Dev

Blog

Como apresentar refatorações de código legado em entrevistas técnicas

Por John Pires · 10 de março de 2026

Cenario pratico do dia a dia

O que é um código legado no contexto da entrevista

Você será questionado sobre impactos reais: manutenção, bugs frequentes, acoplamento e riscos. Em entrevistas técnicas, 'código legado' não é apenas código antigo — é qualquer base onde mudanças geram efeitos colaterais inesperados e onde falta teste ou documentação. Mostrar que você entende esse contexto é mais valioso do que decorar padrões.

Como os entrevistadores querem ver sua abordagem

Recrutadores e engenheiros buscam clareza: identificação do problema, análise de trade-offs, testes, pequenas mudanças incrementais e comunicação. Eles querem ver que você prioriza segurança (não quebrar produção), que sabe dividir um refactor em passos reversíveis e que tem critérios de sucesso mensuráveis.

Na prática, você deve demonstrar: leitura rápida do código, identificação de responsabilidades mal distribuídas, hipóteses de regressão e mitigação com testes e deploy incremental. Seja específico sobre riscos e decisões.

Exemplos genéricos não bastam aqui. Use um exemplo concreto: uma função que valida pedido, calcula total e persiste tudo junto. Explique por que isto é um problema — dificulta testes, impossibilita reuso e torna deploys arriscados.

Antes de sugerir mudanças, explique o comportamento atual, as falhas observadas e as métricas que importam: tempo médio de deploy, taxa de rollback, cobertura de teste.

Arquitetura da solucao

Princípios para decomposição segura

Adote princípios simples e comunicáveis: SRP (single responsibility), testes por função, interfaces claras e isolamento de efeitos colaterais. Em entrevistas, prefira padrões que você pode explicar em 30-60 segundos e demonstrar com código curto.

Estratégia incremental e contrato público

Proponha uma arquitetura que permita dois modos de operação: manter compatibilidade com a API atual e introduzir funções internas fáceis de testar. Use adapters e wrappers pequenos para proteger consumidores e desencadear rollback rápido se necessário.

Um fluxo razoável para apresentar: identificar função-alvo, extrair validação, extrair cálculo, criar testes unitários, trocar implementação interna e, por fim, remover código antigo com um commit separado. Isso mostra disciplina e cuidado com riscos.

  • Extração pequena e comprovada: um commit por responsabilidade
  • Testes antes de alterar lógica (onde possível)
  • Documentação curta no PR com antes/depois e mitigação de riscos

Codigo completo com explicacoes

Contexto do exemplo

Vou usar um exemplo de backend em Node.js: uma função 'processOrder' que recebe um pedido, valida, calcula total com descontos, atualiza estoque e grava no banco. Esse é um caso comum de função com muitas responsabilidades.

Exemplo 1 — Código legado (funcional)

O código abaixo representa a função original encontrada no legado. Ele é intencionalmente monolítico e imprimível com node. Antes do bloco, explique: este arquivo processa pedidos e mistura validação, cálculo e persistência.

// legacy-process-order.js
// O que faz: processa um pedido inteiro — valida, calcula e persiste
function processOrder(order) {
  // validações simples inline
  if (!order || !order.items || order.items.length === 0) {
    throw new Error('pedido invalido');
  }

  // calcula total com desconto aleatório (lógica misturada)
  let total = 0;
  for (const it of order.items) {
    if (!it.price || !it.qty) {
      throw new Error('item invalido');
    }
    total += it.price * it.qty;
  }

  // desconto por quantidade (lógica de negocio dentro)
  if (order.items.length > 3) {
    total = total * 0.9; // 10% desconto
  }

  // atualiza estoque (simulado) e persiste
  // side effects diretos e sem isolamento
  for (const it of order.items) {
    // imaginando chamada ao banco/estoque
    console.log('decrement stock for', it.sku);
  }

  // grava order (simulado)
  console.log('persist order with total', total);
  return { success: true, total };
}

// Exemplo de uso
const example = { items: [ { sku: 'A1', price: 10, qty: 2 }, { sku: 'B2', price: 5, qty: 1 } ] };
console.log(processOrder(example));

Explicacao depois do bloco: esse código funciona mas é difícil de testar partes isoladas — validação, cálculo e persistência são acopladas. Em entrevista, destaque isso: pontos de dor, como cobertura e risco de regressão.

Exemplo 2 — Versão refatorada com testes simples

Agora, extraí validação e cálculo para funções puras e isolei efeitos colaterais. Antes do bloco: este arquivo mostra a decomposição com funções exportáveis e um mock de persistência.

// refactor-process-order.js
// O que faz: mesma funcionalidade, com responsabilidades separadas
function validateOrder(order) {
  if (!order || !order.items || order.items.length === 0) {
    throw new Error('pedido invalido');
  }
  for (const it of order.items) {
    if (!it.price || !it.qty) {
      throw new Error('item invalido');
    }
  }
  return true;
}

function calculateTotal(order) {
  let total = 0;
  for (const it of order.items) {
    total += it.price * it.qty;
  }
  if (order.items.length > 3) {
    total = total * 0.9;
  }
  return total;
}

async function persistOrder(order, total, dbClient) {
  // dbClient.save é um mockable side-effect
  await dbClient.save({ order, total });
  return true;
}

async function processOrderRefactored(order, dbClient) {
  validateOrder(order);
  const total = calculateTotal(order);
  await persistOrder(order, total, dbClient);
  return { success: true, total };
}

// Mock de dbClient para demonstracao
const mockDb = { save: async (payload) => console.log('saved', payload.total) };

// Uso
const example = { items: [ { sku: 'A1', price: 10, qty: 2 }, { sku: 'B2', price: 5, qty: 1 } ] };
processOrderRefactored(example, mockDb).then(r => console.log(r));

Explicacao depois do bloco: agora você pode testar calculateTotal sem tocar banco, validar inputs isoladamente e mockar persistOrder em testes de integração. Em entrevista, cite que você faria commits separados: extrair funções puras, adicionar testes, then adaptar callers.

Exemplo 3 — Teste rápido e benchmark

Antes do bloco: mostre um benchmark simples comparando a versão legacy e a refatorada em execução repetida. Isso é útil para discutir trade-offs de performance e justificar mudanças.

// benchmark.js
// O que faz: compara tempo de execução entre legacy e refactor em loops
const { performance } = require('perf_hooks');

// Implementacoes simples (copiadas/inlined para comparacao)
function legacyCalc(order) {
  let total = 0;
  for (const it of order.items) {
    total += it.price * it.qty;
  }
  if (order.items.length > 3) total = total * 0.9;
  return total;
}

function refactorCalc(order) {
  // mesma logica em funcao separada — overhead de chamada de funcao
  return (function calc(o) {
    let t = 0;
    for (const i of o.items) t += i.price * i.qty;
    if (o.items.length > 3) t = t * 0.9;
    return t;
  })(order);
}

const order = { items: new Array(10).fill(0).map((_,i)=>({ sku: 's'+i, price: 10+i, qty: 1 })) };

function bench(fn, iter=100000) {
  const start = performance.now();
  let s = 0;
  for (let i=0;i<iter;i++) s += fn(order);
  const end = performance.now();
  return { ms: end-start, sum: s };
}

console.log('legacy', bench(legacyCalc));
console.log('refactor', bench(refactorCalc));

Explicacao depois do bloco: o benchmark simples mostrará se a decomposição impõe overhead relevante. Em sistemas I/O-bound, overhead de chamadas internas é irrelevante. Em entrevistas, use esse argumento para justificar ou não uma micro-otimização.

Tratamento de erros e edge cases

Erros comuns ao apresentar refactors

Um erro frequente é falar alto demais sobre padrões sem mostrar provas. Outro é alterar too much at once: grandes commits que misturam refatoração com features novas. Isso leva a rollbacks e menos confiança do time. Em entrevista, proponha commits pequenos e reversíveis.

Armadilhas técnicas e como mitigá-las

Armadilhas comuns: assumir que testes existentes cobrem casos, remover comportamento legado com impacto sutíl (por exemplo, rounding), e ignorar código que expõe contratos implícitos aos clientes. Mitigue criando testes que documentem o comportamento atual antes de mudar.

Inclua no seu roteiro de apresentação: como você testa alterações, quais mocks usa, que logs ou métricas adicionaria temporariamente para validar em produção e como faria rollback.

  • Teste antes: unidade para lógica pura, integração para persistência
  • Commits pequenos: um compromisso por responsabilidade extraída
  • Documente o contrato: quais entradas e saídas manteve compatíveis
Ao descrever erros, sempre entregue como ação: 'para evitar X eu adiciono teste Y e um commit separado que isola side-effects'.

Performance e otimizacao

Medindo impacto real

Performance é sobre métricas: latency, throughput e custo. Em entrevistas, não diga apenas 'é mais rápido' — mostre números simples. Use microbenchmarks para validar overheads e monitore em staging para ver impacto sob carga real.

Quando não otimizar

Não otimize prematuramente. Se refactor melhora legibilidade e testabilidade, e o sistema é I/O-bound, aceitar um pequeno overhead de CPU é razoável. Explique trade-offs: manutenção vs latência. Tenha exemplos concretos: 'desacoplei cálculo da persistência e isso adicionou 2 chamadas de função — overhead insignificante comparado ao chamado ao banco'.

Demonstre que você sabe medir e justificar: use ferramentas simples (console.time, perf_hooks em Node, benchmarks locais) e dados de produção quando possível. Um bom argumento é 'benchmark local mostrou 3% mais latência em CPU-bound, mas cobertura aumentou 40% e tempo de entrega caiu'.

Conclusao pratica

Roteiro prático para apresentar em entrevista

Estruture sua resposta em 5 passos curtos: (1) contexto e evidências do problema; (2) objetivo do refactor; (3) mudanças concretas (commits pequenos); (4) testes e mitigação de riscos; (5) métricas e trade-offs. Repita esse roteiro em voz curta durante a entrevista.

Checklist rápido para levar na cabeça

Tenha sempre preparado: um snippet do código atual e refatorado, um teste que demonstra comportamento, um benchmark simples e um exemplo de mensagem de PR. Isso mostra que você não apenas entende patterns, mas sabe aplicá-los com disciplina.

Em entrevistas técnicas, seu diferencial é a clareza: dividir problemas, priorizar segurança e provar resultados. Mostre commits pequenos, testes que documentam o comportamento antigo e novos, e números quando possível. Essas práticas reduzem risco e demonstram maturidade técnica.

Feche sua apresentação com uma nota prática: o que outra pessoa no time precisaria saber para revisar o PR e qual rollback plan você deixaria pronto. Isso revela que você pensa além do código.

Use este artigo como script: pratique explicando cada bloco de código do exemplo em 2 minutos. Se dominar esse fluxo, você terá uma resposta técnica concreta e memorável.

Assuntos: entrevistas, refatoracao, codigo-legado, Carreira, Git

Leia também