Antes de existir qualquer serviço, o monólito precisa parar de falar com o faturamento de 23 lugares diferentes. A primeira entrega é uma porta: um módulo com uma interface pequena, por onde passam todas as chamadas, com a implementação atual movida para trás dela sem mudar comportamento. Essa técnica, chamada branch by abstraction, é o que torna o desvio possível sem tocar nos chamadores de novo.
// faturamento/porta.js - unico ponto pelo qual o resto do monolito fala com faturamento.
import * as local from './local.js';
import { criarClienteRemoto } from './remoto.js';
import { flags } from '../config/flags.js';
const remoto = criarClienteRemoto({
baseUrl: process.env.FATURAMENTO_URL,
timeoutMs: 800,
});
const implementacaoPara = (tenantId) =>
flags.ativo('faturamento-remoto', { tenantId }) ? remoto : local;
export const faturamento = {
emitirFatura: (tenantId, pedido) => implementacaoPara(tenantId).emitirFatura(tenantId, pedido),
buscarFatura: (tenantId, faturaId) => implementacaoPara(tenantId).buscarFatura(tenantId, faturaId),
listarEmAberto: (tenantId, clienteId) =>
implementacaoPara(tenantId).listarEmAberto(tenantId, clienteId),
};
// faturamento/remoto.js - mesma interface de local.js, falando HTTP com o servico novo.
export function criarClienteRemoto({ baseUrl, timeoutMs }) {
async function chamar(caminho, { method = 'GET', body, idempotencyKey } = {}) {
const resposta = await fetch(new URL(caminho, baseUrl), {
method,
headers: {
'content-type': 'application/json',
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeoutMs),
});
if (resposta.status === 404) return null;
if (!resposta.ok) throw new Error(`faturamento respondeu ${resposta.status} em ${caminho}`);
return resposta.json();
}
return {
emitirFatura: (tenantId, pedido) =>
chamar(`/tenants/${tenantId}/faturas`, {
method: 'POST',
body: pedido,
idempotencyKey: `pedido-${pedido.id}`,
}),
buscarFatura: (tenantId, faturaId) => chamar(`/tenants/${tenantId}/faturas/${faturaId}`),
listarEmAberto: (tenantId, clienteId) =>
chamar(`/tenants/${tenantId}/clientes/${clienteId}/faturas?status=aberta`),
};
}
Três detalhes fazem diferença. A decisão de rota é por tenant, não por requisição, para que um mesmo cliente não veja faturas emitidas por dois sistemas no mesmo dia. A chave de idempotência derivada do pedido garante que um retry depois de timeout não emita fatura duplicada, desde que o serviço novo honre o cabeçalho. E o timeout de 800 ms é explícito: uma chamada que antes era uma função local agora pode esperar a rede, e o orçamento de latência da rota precisa contar com isso.
- Um lint que proíbe importar faturamento/local.js fora de faturamento/ impede que a costura vaze de novo.
- Os testes existentes passam a rodar contra a porta, e servem depois como contrato da implementação remota.
- A interface nasce com as operações que o negócio usa, como emitirFatura e listarEmAberto, e não com o formato das tabelas.