Skip to content

Desafio 03: DICT — Diretório de Identificadores de Contas Transacionais

🇧🇷 Diretório de Identificadores de Contas Transacionais
🇬🇧 Directory of Transactional Account Identifiers


O DICT é o diretório central do Banco Central do Brasil que gerencia todas as chaves PIX. É a "agenda telefônica" do PIX — sem ele, ninguém saberia para qual conta enviar um PIX quando você informa apenas um CPF ou e-mail.

Switch: TypeScript vs Go

O que é o DICT?

Tipo de ChaveDescrição
CPF11 dígitos, validação por dígito verificador
CNPJ14 dígitos, validação por dígito verificador
E-mailFormato RFC 5322, até 77 caracteres
TelefoneFormato +55 XX XXXXX-XXXX
AleatóriaUUID v4, máxima privacidade
CaracterísticaDescrição
OwnershipUma chave = uma conta (única)
PortabilidadeCliente pode mover chave entre bancos
Anti-enumerationProteção contra varreduras
Rate limitingLimites por instituição

Arquitetura do DICT Simulator

Fluxos Principais

1. Registro de Chave

2. Consulta de Chave

3. Portabilidade (Claim)

Domain Layer

typescript
export enum PixKeyType {
  CPF = 'CPF',
  CNPJ = 'CNPJ',
  EMAIL = 'EMAIL',
  PHONE = 'PHONE',
  RANDOM = 'RANDOM'
}

export class PixKey extends Entity<string> {
  public static create(props: PixKeyProps): Result<PixKey, Error> {
    const validation = PixKey.validate(props);
    if (validation.isErr()) return Err(validation.error);

    return Ok(new PixKey({
      ...props,
      id: props.id || uuidv4(),
      createdAt: props.createdAt || new Date(),
      active: props.active ?? true,
    }));
  }

  public static createRandom(account: AccountInfo, owner: OwnerInfo): Result<PixKey, Error> {
    return PixKey.create({
      type: PixKeyType.RANDOM,
      value: uuidv4(),
      account, owner,
      ispb: account.ispb,
      createdAt: new Date(),
      active: true,
    });
  }

  public transferTo(newAccount: AccountInfo): Result<PixKey, Error> {
    if (!this.props.active) return Err(new Error('Chave inativa'));
    return Ok(new PixKey({
      ...this.props,
      account: newAccount,
      ispb: newAccount.ispb,
      updatedAt: new Date(),
    }));
  }
}

Validators — Validação de Cada Tipo

typescript
export class CPFValidator implements KeyValidator {
  validate(value: string): ValidationResult {
    const cpf = value.replace(/\D/g, '');
    if (cpf.length !== 11) return { isValid: false, errorMessage: 'CPF deve ter 11 dígitos' };
    if (/^(\d)\1+$/.test(cpf)) return { isValid: false, errorMessage: 'CPF inválido' };
    if (!this.validateDigits(cpf)) return { isValid: false, errorMessage: 'CPF inválido' };
    return { isValid: true, normalizedValue: cpf };
  }

  private validateDigits(cpf: string): boolean {
    let sum = 0;
    for (let i = 0; i < 9; i++) sum += parseInt(cpf[i]) * (10 - i);
    let remainder = (sum * 10) % 11;
    if (remainder === 10) remainder = 0;
    if (remainder !== parseInt(cpf[9])) return false;

    sum = 0;
    for (let i = 0; i < 10; i++) sum += parseInt(cpf[i]) * (11 - i);
    remainder = (sum * 10) % 11;
    if (remainder === 10) remainder = 0;
    return remainder === parseInt(cpf[10]);
  }
}

export class CNPJValidator implements KeyValidator {
  validate(value: string): ValidationResult {
    const cnpj = value.replace(/\D/g, '');
    if (cnpj.length !== 14) return { isValid: false, errorMessage: 'CNPJ deve ter 14 dígitos' };
    if (/^(\d)\1+$/.test(cnpj)) return { isValid: false, errorMessage: 'CNPJ inválido' };
    if (!this.validateDigits(cnpj)) return { isValid: false, errorMessage: 'CNPJ inválido' };
    return { isValid: true, normalizedValue: cnpj };
  }

  private validateDigits(cnpj: string): boolean {
    const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

    let sum = 0;
    for (let i = 0; i < 12; i++) sum += parseInt(cnpj[i]) * weights1[i];
    let remainder = sum % 11;
    let digit1 = remainder < 2 ? 0 : 11 - remainder;
    if (digit1 !== parseInt(cnpj[12])) return false;

    sum = 0;
    for (let i = 0; i < 13; i++) sum += parseInt(cnpj[i]) * weights2[i];
    remainder = sum % 11;
    let digit2 = remainder < 2 ? 0 : 11 - remainder;
    return digit2 === parseInt(cnpj[13]);
  }
}

export class KeyValidatorFactory {
  static create(type: PixKeyType): KeyValidator {
    switch (type) {
      case PixKeyType.CPF: return new CPFValidator();
      case PixKeyType.CNPJ: return new CNPJValidator();
      case PixKeyType.EMAIL: return new EmailValidator();
      case PixKeyType.PHONE: return new PhoneValidator();
      case PixKeyType.RANDOM: return new RandomKeyValidator();
    }
  }
}

Use Cases — Registro de Chave

typescript
export class RegisterPixKeyUseCase {
  private static readonly MAX_KEYS_PER_PERSON = 5;

  constructor(
    private readonly pixKeyRepo: PixKeyRepository,
    private readonly lock: DistributedLock,
    private readonly eventPublisher: EventPublisher
  ) {}

  public async execute(input: RegisterPixKeyInput): Promise<Either<Error, PixKey>> {
    // 1. Para chave aleatória, gera o valor
    let value = input.value;
    if (input.type === PixKeyType.RANDOM) {
      value = crypto.randomUUID();
    } else if (!value) {
      return left(new Error('Valor é obrigatório'));
    }

    // 2. Valida formato
    const validator = KeyValidatorFactory.create(input.type);
    const validation = validator.validate(value);
    if (!validation.isValid) return left(new Error(validation.errorMessage!));

    // 3. Lock distribuído
    const unlock = await this.lock.acquire(`pix_key:${value}`, { ttl: 10000 });
    try {
      // 4. Verifica se já existe
      const existing = await this.pixKeyRepo.findByValue(value);
      if (existing) return left(new KeyAlreadyRegisteredError(value));

      // 5. Valida limite por pessoa
      const existingByOwner = await this.pixKeyRepo.findByOwnerDocument(owner.document);
      if (existingByOwner.length >= RegisterPixKeyUseCase.MAX_KEYS_PER_PERSON) {
        return left(new MaxKeysReachedError(owner.document));
      }

      // 6. Cria e persiste
      const pixKey = PixKey.create({ type: input.type, value, account, owner, ispb: account.ispb });
      await this.pixKeyRepo.save(pixKey);

      await this.eventPublisher.publish('pix.key.registered', { keyId: pixKey.id, type: pixKey.type });
      return right(pixKey);
    } finally {
      await unlock();
    }
  }
}

Use Cases — Claim (Portabilidade)

typescript
export class ClaimPixKeyUseCase {
  private static readonly CLAIM_EXPIRY_HOURS = 7 * 24;

  constructor(
    private readonly pixKeyRepo: PixKeyRepository,
    private readonly claimRepo: ClaimRepository,
    private readonly lock: DistributedLock,
    private readonly notificationService: NotificationService
  ) {}

  public async execute(input: ClaimPixKeyInput): Promise<Either<Error, ClaimPixKeyOutput>> {
    const unlock = await this.lock.acquire(`pix_key:${input.key}`, { ttl: 15000 });
    try {
      const currentKey = await this.pixKeyRepo.findByValue(input.key);
      if (!currentKey) return left(new KeyNotFoundError(input.key));

      if (currentKey.ispb === input.claimerIspb) {
        return left(new SamePSPError('Não é possível claim no mesmo PSP'));
      }

      const expiresAt = new Date();
      expiresAt.setHours(expiresAt.getHours() + ClaimPixKeyUseCase.CLAIM_EXPIRY_HOURS);

      const claim = { id: crypto.randomUUID(), key: input.key, currentIspb: currentKey.ispb, claimerIspb: input.claimerIspb, status: 'PENDING', expiresAt };
      await this.claimRepo.save(claim);

      await this.notificationService.notifyPSP(currentKey.ispb, { type: 'CLAIM_RECEIVED', claimId: claim.id });
      return right({ claimId: claim.id, status: 'PENDING', expiresAt });
    } finally {
      await unlock();
    }
  }
}

Anti-Enumeration — Segurança

typescript
export class AntiEnumerationService {
  public async analyzeQuery(ispb: string, ip: string, key: string, result: 'found' | 'not_found'): Promise<SecurityAction> {
    const ispbCount = await this.redis.incr(`query:${ispb}:${hourKey}`);
    const ispbNotFound = result === 'not_found' ? await this.redis.incr(`notfound:${ispb}:${hourKey}`) : 0;

    const notFoundRate = ispbCount > 0 ? ispbNotFound / ispbCount : 0;

    // Alta taxa de "não encontrado" = possível enumeração
    if (notFoundRate > 0.9 && ispbCount > 100) {
      return SecurityAction.THROTTLE;
    }

    // Volume muito alto
    if (ispbCount > 5000) {
      return SecurityAction.BLOCK;
    }

    // Detecta CPFs sequenciais
    if (this.isSequentialCPF(key)) {
      return SecurityAction.BLOCK;
    }

    return SecurityAction.ALLOW;
  }
}

Proteções obrigatórias:

  • Rate limiting por ISPB e IP
  • Respostas homogêneas (404 genérico, sem detalhes)
  • Masking de dados (nunca retorna CPF completo)
  • Detecção de padrões anômalos
  • Auditoria de todas as consultas

Comparação: TypeScript vs Go

AspectoTypeScriptGo
DesenvolvimentoRápido (Zod, Express)Mais verboso
CPU-boundValidações travam event loopNão bloqueia
MemoryMais RAM por conexão2-3x menos
Throughput~10K req/s~50K req/s
RegexV8 otimizadoNativo, pré-compilado
JSONNativoStruct tags

Quando usar TypeScript?

  • PSP pequeno/médio (até 1M de chaves)
  • Time-to-market rápido
  • Volume moderado (até 5K consultas/s)
  • Equipe sem experiência em Go

Caso Real: Nubank e Itaú

  • Nubank — Clojure (core DICT) + TypeScript (BFFs), 80M+ clientes, Redis Cluster
  • Itaú — Go (DICT alta performance) + Java (core banking legado), 60M+ clientes

Boas Práticas

Faça:

  • Locks distribuídos em operações de escrita
  • Rate limiting por ISPB, IP e usuário
  • Masking de dados (nunca retorne CPF completo)
  • Auditoria de todas as operações
  • TTL em cache balanceando freshness vs performance

Evite:

  • Respostas detalhadas em 404 (anti-enumeration)
  • Cache sem invalidação (claims mudam ownership)
  • Validação só no backend (sempre backend)
  • Logs com dados sensíveis
  • Concorrência sem locks

Como testar

bash
# TypeScript
make infra-up
pnpm --filter @banking/dict dev

# Registrar chave
curl -X POST http://localhost:3003/api/v1/dict/keys \
  -H "Content-Type: application/json" \
  -d '{"type":"CPF","value":"12345678901","account":{"ispb":"12345678","branch":"0001","number":"12345","type":"CACC"},"owner":{"name":"João","document":"12345678901"}}'

# Consultar chave
curl http://localhost:3003/api/v1/dict/keys/12345678901 \
  -H "X-ISPB: 12345678"

# Go
cd packages/backend/dict-simulator-go
go run .

Lições aprendidas

  1. Uma chave = uma conta — Invariante sagrado do DICT
  2. Anti-enumeration não é opcional — Proteção contra varreduras de CPFs
  3. Locks distribuídos são obrigatórios — Race conditions em claims corrompem ownership
  4. Masking de dados — Nunca retorne CPF/CNPJ completo
  5. Cache com TTL inteligente — Claims mudam ownership, cache precisa invalidar
  6. Auditoria de tudo — Cada consulta deve ser logada
  7. Limite de 5 chaves por pessoa — Regra do BCB
  8. Claim expira em 7 dias — Implemente limpeza automática
  9. Go escala linearmente — CPUs extras viram throughput direto
  10. TypeScript é suficiente — Para volumes moderados (< 5K req/s)