Blog
Projects
Linkedin
Contact

Adapter Pattern na prática: importando extratos OFX, Excel e PDF com um único contrato em TypeScript

11 de outubro de 2026

Todo sistema financeiro chega no mesmo ponto: o usuário quer importar o extrato do banco. Um banco exporta OFX, outro manda uma planilha Excel, e o terceiro só oferece PDF. A transação é a mesma, mas cada arquivo descreve ela de um jeito diferente.
Neste artigo eu mostro como o Adapter, um padrão estrutural do catálogo GoF (Gang of Four), resolve esse problema com uma ideia simples: o sistema define um contrato, e cada formato ganha um tradutor que cumpre esse contrato. As regras de negócio não precisam saber de onde a transação veio.
Este conteúdo nasceu de uma apresentação que fiz na faculdade (TADS, UDESC) sobre padrões de projeto.

O problema

Olha como a mesma saída de R$ 150,00 aparece em cada formato:
Sem um padrão, o serviço de importação vira uma sequência de if/else por formato. Pior: cada formato novo obriga a mexer nas regras centrais do sistema, justamente o código que deveria ser o mais estável.

O conceito

O Adapter faz uma interface incompatível funcionar onde o sistema espera outra. São quatro peças:
O Client usa o Target, o Adapter cumpre o Target e traduz o Adaptee. Todo formato termina no mesmo lugar: uma lista de Transaction.

1. Defina o que o sistema entende

Antes de qualquer adaptador, vem o modelo de domínio. Repare que ele não menciona OFX, Excel nem PDF.
transaction.ts
type TransactionType = "CREDIT" | "DEBIT";

interface Transaction {
  date: string;
  description: string;
  amount: number;
  type: TransactionType;
  externalId?: string;
}
Uma decisão importante: amount guarda sempre o valor absoluto, e type diz se é crédito ou débito. Isso é uma convenção do domínio, não do padrão, mas é ela que permite que três formatos tão diferentes produzam exatamente o mesmo objeto.

2. Defina a regra que todo adaptador cumpre

transaction-file-adapter.ts (Target)
interface TransactionFileAdapter {
  parse(content: Buffer): Promise<Transaction[]>;
}
É o contrato inteiro: recebe o conteúdo do arquivo e devolve transações normalizadas. Qualquer coisa que cumpra isso pode ser importada.

3. Um adaptador por formato

O adaptador de OFX mapeia campo a campo: DTPOSTED → date, MEMO → description, TRNAMT → amount, o sinal de TRNAMT vira type, e FITID → externalId.
ofx.adapter.ts
class OfxAdapter implements TransactionFileAdapter {
  async parse(content: Buffer): Promise<Transaction[]> {
    const entries = await parseOfxFile(content);

    return entries.map((entry) => ({
      date: normalizeOfxDate(entry.DTPOSTED),
      description: entry.MEMO,
      amount: Math.abs(Number(entry.TRNAMT)),
      type: Number(entry.TRNAMT) < 0 ? "DEBIT" : "CREDIT",
      externalId: entry.FITID,
    }));
  }
}
O de Excel lê as colunas da planilha e trata a vírgula decimal:
excel.adapter.ts
class ExcelAdapter implements TransactionFileAdapter {
  async parse(content: Buffer): Promise<Transaction[]> {
    const rows = await parseExcelFile(content);

    return rows.map((row) => ({
      date: normalizeExcelDate(row["Data"]),
      description: row["Histórico"],
      amount: Math.abs(parseMoney(row["Valor"])),
      type: normalizeTransactionType(row["Tipo"]),
    }));
  }
}
E o de PDF primeiro extrai o texto e depois interpreta as linhas do extrato:
pdf.adapter.ts
class PdfAdapter implements TransactionFileAdapter {
  async parse(content: Buffer): Promise<Transaction[]> {
    const text = await extractPdfText(content);
    const entries = parseStatementLines(text);

    return entries.map((entry) => ({
      date: normalizePdfDate(entry.date),
      description: entry.description,
      amount: Math.abs(parseMoney(entry.amount)),
      type: parseMoney(entry.amount) < 0 ? "DEBIT" : "CREDIT",
    }));
  }
}
Três entradas completamente diferentes, a mesma saída: Transaction[].

4. Um serviço que não conhece arquivos

Validação, deduplicação e persistência ficam no serviço, fora dos adaptadores. O detalhe que faz tudo funcionar: ele recebe a interface, não uma classe concreta. Por isso aceita qualquer adaptador.
transaction-import-service.ts
class TransactionImportService {
  async import(
    content: Buffer,
    adapter: TransactionFileAdapter,
  ): Promise<Transaction[]> {
    const transactions = await adapter.parse(content);
    this.validate(transactions);
    // deduplicação, regras de negócio, persistência
    return transactions;
  }
}

5. Escolhendo o adaptador

Para selecionar o adaptador pela extensão do arquivo, um objeto simples resolve. Não é preciso uma Factory completa:
adapter-resolver.ts
const adapters: Record<string, TransactionFileAdapter> = {
  ofx: new OfxAdapter(),
  xlsx: new ExcelAdapter(),
  pdf: new PdfAdapter(),
};
import-file.ts
async function importFile(ext: string, content: Buffer) {
  const adapter = adapters[ext.toLowerCase()];

  if (!adapter) {
    throw new Error("Formato não suportado");
  }

  const service = new TransactionImportService();
  return service.import(content, adapter);
}
terminal
$ importFile("ofx", ofxBuffer)
✓ OfxAdapter → Transaction[]

$ importFile("xlsx", excelBuffer)
✓ ExcelAdapter → Transaction[]

$ importFile("csv", csvBuffer)
✗ Error: Formato não suportado
E aqui está o ganho real: para aceitar CSV, basta criar um CsvAdapter e registrar no mapa adapters. O serviço, a validação e as regras de negócio não mudam uma linha.

O fluxo completo

  1. Upload e validação do arquivo
  2. Resolver o adaptador pela extensão
  3. O adaptador traduz o formato
  4. Modelo normalizado (Transaction[])
  5. Validação, duplicidades e gravação no serviço
Uma organização de pastas que deixa essas fronteiras claras:
src/transactions/
import/parsers/    → lê o arquivo (SheetJS, PDF.js...)
  ofx.parser.ts · excel.parser.ts · pdf.parser.ts
import/adapters/   → traduz cada formato para o modelo comum
  ofx.adapter.ts · excel.adapter.ts · pdf.adapter.ts
domain/            → validação, deduplicação e gravação
  transaction-import-service.ts

Cuidados que fazem diferença em produção

O Adapter só faz a tradução técnica. Os problemas reais de importação aparecem em outros lugares:

Adapter, Factory e Strategy

Esses três padrões costumam aparecer juntos, e vale separar o papel de cada um:

Conclusão

Uma interface, um adaptador por formato e um serviço que não sabe nada sobre arquivos. Com isso, o sistema aceita formatos novos sem que as regras centrais mudem.
O Adapter não é sofisticado, e esse é o ponto. Ele coloca a sujeira de cada formato na borda do sistema, onde ela pode ser isolada e testada, e mantém o domínio falando uma língua só.
Referências: Refactoring.Guru: Adapter | SheetJS | PDF.js | Gamma et al., Design Patterns (1994)
Se este artigo te ajudou, compartilhe com quem está lutando com importação de arquivos. Dúvidas ou comentários, é só me chamar! 🚀
BlogProjectsGithubLinkedin
Built with Next.js, Tailwind and Vercel
Coded by me (Bruno Werner :P)