Knowledge

Padrões de Caching em Produção: Redis, CDN e Application-level

Estratégias multi-nível de caching podem reduzir latência, custos de infraestrutura e carga no banco. Aprenda padrões, trade-offs e práticas de implementação.

14/03/20269 min de leituraKnowledge
Padrões de Caching em Produção: Redis, CDN e Application-level

Resumo executivo

Estratégias multi-nível de caching podem reduzir latência, custos de infraestrutura e carga no banco. Aprenda padrões, trade-offs e práticas de implementação.

Ultima atualizacao: 14/03/2026

Introdução: Caching como arquitetura, não otimização

Caching é frequentemente tratado como otimização tardia — algo a adicionar quando o sistema fica lento. Em 2026, caching maduro é arquitetura de primeira classe. Sistemas em escala operam com múltiplas camadas de cache: CDN no edge, cache application-level em memória, Redis como cache distribuído e até cache de query no banco de dados.

A diferença entre sistemas que escalam e aqueles que colapsam sob carga não é apenas poder de processamento — é a capacidade de servir requisições da memória, não do disco ou da rede.

Cada camada de cache serve um propósito específico: CDN para conteúdo estático global, application cache para dados frequentes na mesma instância, Redis para compartilhamento entre instâncias e query cache para otimizar acesso ao banco.

Padrões fundamentais de caching

Cache-aside (Lazy loading)

O padrão mais comum, onde a aplicação carrega dados no cache sob demanda:

typescriptinterface CacheAsideService {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
  delete(key: string): Promise<void>;
}

async function getUser(cache: CacheAsideService, db: Database, userId: string): Promise<User> {
  // 1. Tentar obter do cache
  const cached = await cache.get<User>(`user:${userId}`);

  if (cached) {
    return cached; // Cache hit
  }

  // 2. Cache miss: buscar do banco
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

  // 3. Armazenar no cache para futuras requisições
  await cache.set(`user:${userId}`, user, 3600); // TTL de 1 hora

  return user;
}

Vantagens:

  • Apenas dados acessados são cacheados
  • Implementação simples
  • O cache é auto-populando

Desvantagens:

  • Primeira requisição após cache invalidation sempre é um cache miss
  • Cache stampede possível quando muitos clientes tentam popular o mesmo item simultaneamente

Solução para cache stampede:

typescriptasync function getUserWithLock(cache: CacheAsideService, db: Database, userId: string): Promise<User> {
  const cached = await cache.get<User>(`user:${userId}`);

  if (cached) return cached;

  // Adquirir lock para evitar cache stampede
  const lockKey = `lock:user:${userId}`;
  const lockAcquired = await cache.setIfNotExists(lockKey, 'locked', 30);

  if (lockAcquired) {
    try {
      // Apenas quem adquiriu o lock popula o cache
      const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
      await cache.set(`user:${userId}`, user, 3600);
      return user;
    } finally {
      await cache.delete(lockKey);
    }
  } else {
    // Aguardar e tentar obter do cache novamente
    await sleep(100);
    return getUserWithLock(cache, db, userId);
  }
}

Write-through

Dados são escritos tanto no cache quanto no armazenamento persistente simultaneamente:

typescriptasync function updateUser(cache: CacheAsideService, db: Database, userId: string, data: Partial<User>): Promise<User> {
  // Atualizar banco de dados
  const updated = await db.query(
    'UPDATE users SET ... WHERE id = $1 RETURNING *',
    [...data, userId]
  );

  // Atualizar cache imediatamente
  await cache.set(`user:${userId}`, updated, 3600);

  return updated;
}

Vantagens:

  • Cache sempre consistente com o banco
  • Cache hits subsequentes sempre retornam dados atualizados
  • Simples de implementar

Desvantagens:

  • Operações de escrita são mais lentas (duas escritas)
  • Writes que falham no banco mas sucedem no cache causam inconsistência

When to use: Dados frequentemente lidos e ocasionalmente escritos (profiles, configs, lookup tables).

Write-behind

Dados são escritos no cache imediatamente e no banco de forma assíncrona:

typescriptclass WriteBehindService {
  private writeQueue: Queue<WriteOperation>;

  async write(key: string, value: any): Promise<void> {
    // Escrita síncrona no cache
    await this.cache.set(key, value);

    // Enfileirar escrita no banco
    await this.writeQueue.add({ key, value, timestamp: Date.now() });
  }

  constructor() {
    // Processar fila de forma assíncrona
    this.writeQueue.process(async (op) => {
      await this.db.query('INSERT INTO data_store (key, value) VALUES ($1, $2)', [op.key, op.value]);
    });
  }
}

Vantagens:

  • Escritas são extremamente rápidas
  • Alto throughput de escrita
  • Batch de escritas pode ser consolidado

Desvantagens:

  • Risco de perda de dados se o cache falhar antes de persistir
  • Complexidade de implementação aumenta significativamente
  • Necessário mecanismo de persistência durável

When to use: Logs, analytics, contadores, dados não-críticos que podem ser tolerados com perda temporária.

Estratégias de invalidação de cache

Time-based expiration (TTL)

A forma mais simples de invalidação:

typescript// Dados com diferentes perfis de atualização
const CACHE_TTLS = {
  static: 86400,      // 24h: conteúdo raramente muda
  user: 3600,         // 1h: perfis de usuário mudam ocasionalmente
  session: 1800,       // 30m: sessions expiram naturalmente
  realtime: 60,        // 1m: dados que precisam ser quase em tempo real
  volatile: 10,        // 10s: dados que mudam frequentemente
};

Desvantagens: Dados desatualizados podem ser servidos até o TTL expirar.

Event-based invalidation

Invalidar cache quando dados mudam:

typescriptclass CacheInvalidator {
  async invalidateUser(userId: string): Promise<void> {
    // Invalidar cache direto
    await this.cache.delete(`user:${userId}`);

    // Invalidar caches derivados
    await this.cache.deletePattern(`user:${userId}:*`);

    // Emitir evento para outros serviços
    await this.eventBus.publish('user.invalidated', { userId });
  }
}

Desafio: Ordem de operações deve ser consistente:

typescript// ERRADO: Cache pode ser populado com dados antigos
async function updateUserData(userId: string, data: any) {
  await this.cache.invalidate(userId);
  await this.db.update(userId, data);
}

// CORRETO: Primeiro persiste, depois invalida
async function updateUserData(userId: string, data: any) {
  await this.db.update(userId, data);
  await this.cache.invalidate(userId);
}

Version-based caching

Usar versões para evitar race conditions:

typescriptinterface VersionedCache {
  get<T>(key: string, version: number): Promise<T | null>;
  set<T>(key: string, version: number, value: T): Promise<void>;
  incrementVersion(key: string): Promise<number>;
}

async function getUser(cache: VersionedCache, db: Database, userId: string): Promise<User> {
  const currentVersion = await cache.getCurrentVersion(`user:${userId}`);
  const cached = await cache.get(`user:${userId}`, currentVersion);

  if (cached) return cached;

  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  await cache.set(`user:${userId}`, currentVersion, user);

  return user;
}

async function invalidateUser(cache: VersionedCache, userId: string): Promise<void> {
  await cache.incrementVersion(`user:${userId}`);
}

Arquitetura multi-nível de caching

Nível 1: CDN (Content Delivery Network)

Para conteúdo estático e semi-estático:

┌─────────────────────────────────────────────────────────────────────────┐
│                           CDN LAYER                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Browser → Edge Location (CDN) → Origin                          │
│                                                                 │
│  Cached: CSS, JS, Images, Fonts, API Responses                    │
│  TTL: 1h - 24h (configurável por resource)                      │
│  Invalidation: Manual purge or versioned URLs                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────────────┘

Práticas:

  • Versionar URLs de assets (app.v2.js em vez de app.js)
  • Configurar cache headers apropriados
  • Usar cache key purging estratégico para atualizações urgentes
nginx# Nginx config para cache de API responses
location /api/public/ {
  proxy_pass http://backend;
  proxy_cache api_cache;
  proxy_cache_valid 200 60m;  # Cache respostas 200 por 60 minutos
  proxy_cache_bypass $http_cache_control;  # Respeitar no-cache do cliente
  add_header X-Cache-Status $upstream_cache_status;
}

Nível 2: Application-level cache (in-memory)

Cache local na mesma instância da aplicação:

typescriptclass ApplicationCache {
  private cache = new Map<string, { value: any; expires: number }>();

  get<T>(key: string): T | null {
    const entry = this.cache.get(key);

    if (!entry) return null;
    if (Date.now() > entry.expires) {
      this.cache.delete(key);
      return null;
    }

    return entry.value as T;
  }

  set<T>(key: string, value: T, ttlSeconds: number): void {
    this.cache.set(key, {
      value,
      expires: Date.now() + ttlSeconds * 1000,
    });
  }

  delete(key: string): void {
    this.cache.delete(key);
  }
}

Vantagens:

  • Extremamente rápido (sem latência de rede)
  • Não requer infraestrutura adicional
  • Ideal para dados acessados frequentemente na mesma instância

Desvantagens:

  • Dados não são compartilhados entre instâncias
  • Cada instância popula seu cache, desperdiçando recursos
  • Cache não persiste entre restarts

When to use: Configurações, lookup tables pequenas, resultados computacionalmente caros.

Nível 3: Distributed cache (Redis)

Cache compartilhado entre todas as instâncias:

typescriptimport { Redis } from 'ioredis';

class RedisCache {
  constructor(private redis: Redis) {}

  async get<T>(key: string): Promise<T | null> {
    const value = await this.redis.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value);
    if (ttl) {
      await this.redis.setex(key, ttl, serialized);
    } else {
      await this.redis.set(key, serialized);
    }
  }

  async delete(key: string): Promise<void> {
    await this.redis.del(key);
  }

  async deletePattern(pattern: string): Promise<void> {
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

Vantagens:

  • Cache compartilhado entre instâncias
  • Persistência configurável
  • Suporte a estruturas de dados avançadas (sets, sorted sets, streams)
  • Pub/sub para invalidação distribuída

Desvantagens:

  • Latência de rede adicionada
  • SPOF se não configurado com alta disponibilidade
  • Custo operacional adicional

When to use: Dados frequentemente acessados, perfis de usuário, sessões, resultados de queries caras.

Nível 4: Database query cache

Cache de queries no próprio banco de dados:

sql-- PostgreSQL query cache
SET work_mem = '256MB';
SET shared_buffers = '2GB';
SET effective_cache_size = '8GB';

-- Materialized views para queries complexas
CREATE MATERIALIZED VIEW user_stats AS
SELECT user_id, COUNT(*) as total_orders, SUM(amount) as total_spent
FROM orders
GROUP BY user_id;

-- Refresh em intervalos regulares
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;

Padrões avançados de caching

Read-through cache

Cache que popula automaticamente em miss:

typescriptclass ReadThroughCache {
  constructor(
    private cache: RedisCache,
    private loader: (key: string) => Promise<any>
  ) {}

  async get(key: string, ttl: number = 3600): Promise<any> {
    let value = await this.cache.get(key);

    if (!value) {
      value = await this.loader(key);
      await this.cache.set(key, value, ttl);
    }

    return value;
  }
}

const userCache = new ReadThroughCache(redisCache, async (userId) => {
  return await db.query('SELECT * FROM users WHERE id = $1', [userId]);
});

Refresh-ahead (proactive loading)

Refresh cache antes de expirar:

typescriptclass RefreshAheadCache {
  async get(key: string, ttl: number): Promise<any> {
    let value = await this.cache.get(key);

    if (!value) {
      // Cache miss: carregar do loader
      value = await this.loader(key);
      await this.cache.set(key, value, ttl);
    } else {
      // Cache hit: verificar se está próximo da expiração
      const ttlRemaining = await this.cache.ttl(key);
      const refreshThreshold = ttl * 0.9; // Refresh quando 90% do TTL passou

      if (ttlRemaining < refreshThreshold) {
        // Refresh assíncrono
        this.refreshInBackground(key);
      }
    }

    return value;
  }

  private async refreshInBackground(key: string): Promise<void> {
    const value = await this.loader(key);
    await this.cache.set(key, value, this.ttl);
  }
}

Cache warming

Preencher cache proativamente durante startup ou em horários de baixa demanda:

typescriptclass CacheWarmer {
  async warmUserCache(userIds: string[]): Promise<void> {
    for (const userId of userIds) {
      const user = await this.db.query('SELECT * FROM users WHERE id = $1', [userId]);
      await this.cache.set(`user:${userId}`, user, 3600);
    }
  }

  // Executar durante startup da aplicação
  async onStartup(): Promise<void> {
    const activeUsers = await this.db.query(
      'SELECT id FROM users WHERE last_active > NOW() - INTERVAL 7 days'
    );
    await this.warmUserCache(activeUsers.map(u => u.id));
  }
}

Métricas e monitoramento

Métricas essenciais

typescriptinterface CacheMetrics {
  // Taxas de hit/miss
  hitRate: number;           // cache_hits / (cache_hits + cache_misses)
  missRate: number;          // cache_misses / (cache_hits + cache_misses)

  // Latência
  avgHitLatency: number;     // Tempo médio de resposta em cache hit
  avgMissLatency: number;    // Tempo médio de resposta em cache miss

  // Evolução e tamanho
  evictions: number;         // Quantos itens foram evictados por TTL ou memória
  memoryUsage: number;      // Memória usada pelo cache
  itemCount: number;         // Número de itens em cache
}

Implementação de métricas

typescriptclass InstrumentedCache {
  private metrics = {
    hits: 0,
    misses: 0,
    evictions: 0,
  };

  async get<T>(key: string): Promise<T | null> {
    const value = await this.cache.get<T>(key);

    if (value) {
      this.metrics.hits++;
      this.metrics.histogram('cache.hit.latency', latency);
    } else {
      this.metrics.misses++;
      this.metrics.histogram('cache.miss.latency', latency);
    }

    return value;
  }

  getMetrics(): CacheMetrics {
    const total = this.metrics.hits + this.metrics.misses;
    return {
      hitRate: this.metrics.hits / total,
      missRate: this.metrics.misses / total,
      evictions: this.metrics.evictions,
      // ...
    };
  }
}

Targets de produção:

  • Hit rate > 80% para dados frequentemente acessados
  • Latência de cache hit < 5ms
  • Evolução por TTL > 90% (evitar evolução por memória insuficiente)

Plano de implementação em 30 dias

Semana 1: Identificar oportunidades de caching

  • Mapear endpoints mais acessados
  • Identificar queries mais lentas
  • Classificar dados por perfil de atualização

Semana 2: Implementar cache-aside em endpoints críticos

  • Adicionar Redis como cache distribuído
  • Implementar application cache para dados locais
  • Configurar CDN para conteúdo estático

Semana 3: Refinar estratégias de invalidação

  • Implementar invalidação baseada em eventos
  • Adicionar versioning para evitar race conditions
  • Configurar cache warming para dados críticos

Semana 4: Monitorar e otimizar

  • Implementar métricas de cache
  • Ajustar TTLs baseado em padrões de acesso
  • Ajustar tamanho de cache baseado em utilização

Sua aplicação sofre com latência ou custos elevados de infraestrutura? Fale com especialistas da Imperialis sobre estratégias de caching multi-nível, arquitetura de performance e otimização de custos em escala.

Fontes

Leituras relacionadas