Israel Nunes

Last updated: November 7th, 2026Atualizado em 7 de novembro de 2026

← Back to blog← Voltar para o blog

System Design Basics: CachingFundamentos de System Design: Cache

February 10, 202610 de fevereiro de 2026·5-6 min read5-6 min de leitura
basicssystem designcacheprogramming

Hello, and thank you for joining me again! I am starting a series of posts about System Design Basics! The idea is to write about different topics that, together, can give us a broader vision on system design.

It is important to mention that this series is highly inspired by Hello Interview - Core Concepts articles! Give them a look, it is more than worth it!

For this post (#1), we are focusing on Cache! I expect that at the end of this blog you will be able to talk about what caching is, where to place it, existing architectures, eviction policies and common issues/challenges.

Defining Caching

At its core, caching is the practice of storing copies of data in a temporary storage location, so that future requests for that specific data can be served faster.

More technically, a cache is usually a high-speed data storage layer (like RAM) that sits in front of a slower, permanent storage layer (like a Hard Drive or SQL Database).

For example, a Domain Name System (DNS) caches DNS records to enhance lookup performance, Content Delivery Networks (CDN) use caching to reduce latency, web browsers cache HTML files, images, and JavaScript for faster website loading, and we could go on forever….

Where to Place a Cache

You can place a cache at almost every layer of a computing system. But for the sake of simplicity, here are the four most common:

External Caching

The external cache lives in its own separate layer, independent of the application or the database. For this approach, technologies like Redis or Memcached are well known!

One of the main benefits of external caching is scalability. Since the cache is shared across all application servers, any server can read or write to it. For example, if Server A caches a user profile, Server B can access it immediately without having to query the database again.

In-Process Caching

The cache is stored directly in the application’s memory on the server running the code.

As there is no network travel time to reach for the cached information, this approach is extremely fast! It makes sense when you need to store small pieces of data that rarely change (config values, feature flags or precomputed values).

On the other hand, each instance of your application might have different versions of the data (inconsistency).

CDN (Content Delivery Network)

A network of geographically distributed servers that caches static assets — as mentioned earlier — like images, CSS, and videos closer to the user.

This means that, if your user is in London, they can download a copy of your content from a London server, instead of from the main server at São Paulo. This approach enhances performance as it minimizes latency per request.

Client-Side Caching

Client-side caching stores data directly on the user’s device, usually a web browser (localStorage, HTTP cache) or mobile app using local memory or on-device storage.

As data is already on the user’s machine, it has zero network latency, which provides an instant load.

Cache Architectures

When designing a system, it is really important to understand and decide how the application, the cache, and the database interact. This decision will impact performance, consistency and complexity.

Cache-Aside

This is the most common pattern. The application is responsible for coordinating the data.

  1. App checks cache
  2. If Hit: Return data (”hit” means data was at cache layer)
  3. If Miss: App reads from DB
    • App updates cache
    • Return data

This architecture is resilient — if the cache fails, our system still works. One observation is that the first request might be slower, due to the absence of data at the cache level.

Write-Through

The application writes data only to the Cache, then (cache) writes to the Database synchronously before returning to the application.

  1. App writes to Cache.
  2. Cache writes to Database.
  3. Write is confirmed only when both are done.

Although we aim for bigger consistency here, as we try to keep the cache synchronized with the database, we expose our application to the dual-write problem. If the cache update succeeds but the database write fails, the system can end up inconsistent.

Also, it is worth noting that we have slower write operations (application must wait for both), and we might be caching data that will never be read (waste of memory allocation).

Write-Behind (Write-Back)

Unlike the previous approach where writes are synchronous, Write-Behind uses asynchronous logic. The application writes to the cache, then, cache batches and writes the data to the database asynchronously in the background.

  1. App writes to Cache and gets immediate confirmation.
  2. Cache updates Database in the background.

This architecture has extremely fast write performance, and can reduce load on the database (as it can batch multiple writes into one).

On the other hand, it presents a high risk of data loss. If the cache crashes before it updates the DB, that data is gone!

Read-Through

Similar to Cache-Aside, but the application treats the Cache as the main data store. The Cache itself is responsible for fetching from the DB if data is missing, not the application.

  1. App reads cache
  2. If cache miss:
    • Fetch from database
    • Cache data
    • Return info for the application

Cache Eviction Policies

A cache has limited space. When it gets full, you must decide what to delete to make room for new data. This decision is called "eviction."

Least Recently Used (LRU)

"If you haven't used it in a while, you probably won't need it soon." The system removes the item that was accessed longest ago. This is the industry standard for most web applications.

Least Frequently Used (LFU)

“If you rarely use it, get rid of it.”

In this policy, the system tracks how many times an item is accessed. Items with the lowest count are removed. It is not easy to implement; some systems use approximate LFU to avoid the high cost of precise tracking.

First-In-First-Out (FIFO)

"Oldest items leave first."

Similar to a queue data structure. The item that has been in the cache the longest is removed, regardless of how often it is accessed.

Time-To-Live (TTL)

“This data self-destructs in x minutes.”

Items are automatically deleted after a set period, regardless of space. This is crucial for data that changes frequently, like stock prices or news feeds.

Common Issues/Challenges

Caching is a really useful and important strategy, but implementing it can introduce some complexities. In order to understand trade-offs, let's check some edge cases.

Cache Stampede (Thundering Herd)

Imagine a popular cache key (like "homepage_news") expires. Suddenly, 10,000 users request that data simultaneously. All 10,000 requests see a "Cache Miss" and hit the database at the exact same millisecond, potentially crashing it.

Ways to handle it:

  1. Request Coalescing (Singleflight): The system detects that multiple users are asking for the same data. It allows only the first request to go to the database. The other 9,999 requests are made to wait. Once the first request returns, the result is shared with everyone
  2. Cache Warming: You run a script to populate the cache before it expires or before high traffic hits, ensuring users never face a "cold" cache.
  3. Probabilistic Early Expiration: If a key expires in 60s, you might randomly re-fetch it at 55s or 58s, so the refresh happens before the stampede begins.

Cache Consistency

Keeping the Cache and the database in sync is a common and well-discussed problem in software engineering. This happens because most systems read from the cache but write to the database first. For example, given a user profile update that has been written to the database, the application might still be serving other users with the previous profile version, because the Cache hasn’t been updated yet.

Ways to handle it:

  1. Short TTLs: Even if data is stale, a short Time-To-Live (e.g., 5 seconds) ensures the bad data doesn't persist for long.
  2. Explicit Invalidation: When the application writes to the database, it must immediately send a command to DELETE or UPDATE that specific key in the cache.

Hot Keys

Sometimes, a single key is accessed so frequently (Twitter profile of a celebrity during a scandal) that a single cache server node cannot handle the traffic, creating a bottleneck while other nodes sit idle.

  1. Local Caching (L1 Cache): Add a small in-memory cache directly on the application servers. Even if it stores the data for just 1 second, it saves thousands of trips to the main cache.
  2. Key Replication: Create multiple copies of the hot key (e.g., key_1, key_2, key_3) and distribute them across different nodes. Randomly route user requests to one of these copies.

Wrap Up

Caching is one of those concepts that sounds simple on the surface but quickly reveals its depth once you start making real decisions: where to place it, which architecture to use, how to handle eviction, and how to deal with edge cases like stampedes or consistency gaps.

The key takeaway is that there is no one-size-fits-all solution. Every caching decision involves trade-offs between speed, consistency, complexity and resilience. Understanding these trade-offs is what makes the difference when designing systems at scale.

If you want to go deeper, I highly recommend checking the Hello Interview - Caching article that inspired this post. Stay tuned for the next entry in the System Design Basics series!

Olá, e obrigado por estar aqui de novo! Estou começando uma série de posts sobre Fundamentos de System Design! A ideia é escrever sobre temas diferentes que, juntos, nos deem uma visão mais ampla de system design.

Vale dizer que esta série é bastante inspirada nos artigos de Core Concepts do Hello Interview! Dê uma olhada, vale muito a pena!

Neste post (#1), o foco é cache! Espero que, ao final, você consiga falar sobre o que é cache, onde colocá-lo, as arquiteturas existentes, as políticas de remoção e os problemas e desafios mais comuns.

O que é cache

Em essência, cache é a prática de guardar cópias de dados num local de armazenamento temporário, para que as próximas requisições por aquele dado sejam atendidas mais rápido.

De forma mais técnica, um cache costuma ser uma camada de armazenamento de alta velocidade (como a memória RAM) que fica na frente de uma camada de armazenamento permanente e mais lenta (como um disco rígido ou um banco de dados SQL).

Por exemplo, o Domain Name System (DNS) guarda registros DNS em cache para acelerar as consultas, as Content Delivery Networks (CDN) usam cache para reduzir a latência, os navegadores guardam em cache arquivos HTML, imagens e JavaScript para carregar sites mais rápido, e a lista não para….

Onde colocar um cache

Dá para colocar um cache em quase todas as camadas de um sistema. Mas, para simplificar, aqui estão as quatro mais comuns:

Cache externo

O cache externo vive numa camada própria, independente da aplicação e do banco de dados. Para essa abordagem, tecnologias como Redis e Memcached são bem conhecidas!

Um dos principais benefícios do cache externo é a escalabilidade. Como o cache é compartilhado entre todos os servidores da aplicação, qualquer servidor pode ler ou escrever nele. Por exemplo, se o Servidor A guarda em cache o perfil de um usuário, o Servidor B consegue acessá-lo na hora, sem consultar o banco de novo.

Cache em processo

O cache fica guardado diretamente na memória da aplicação, no servidor que está rodando o código.

Como não há tempo de rede para buscar a informação em cache, essa abordagem é extremamente rápida! Ela faz sentido quando você precisa guardar pedaços pequenos de dados que mudam pouco (valores de configuração, feature flags ou valores pré-calculados).

Por outro lado, cada instância da sua aplicação pode ter versões diferentes do dado (inconsistência).

CDN (Content Delivery Network)

Uma rede de servidores distribuídos geograficamente que guarda em cache os arquivos estáticos — como mencionei antes — como imagens, CSS e vídeos, mais perto do usuário.

Isso quer dizer que, se o seu usuário está em Londres, ele pode baixar uma cópia do seu conteúdo de um servidor em Londres, em vez do servidor principal em São Paulo. Essa abordagem melhora o desempenho, porque reduz a latência de cada requisição.

Cache no cliente

O cache no cliente guarda os dados diretamente no dispositivo do usuário, geralmente no navegador (localStorage, cache HTTP) ou num app mobile, usando a memória local ou o armazenamento do dispositivo.

Como o dado já está na máquina do usuário, a latência de rede é zero, o que dá um carregamento instantâneo.

Arquiteturas de cache

Ao desenhar um sistema, é muito importante entender e decidir como a aplicação, o cache e o banco de dados interagem. Essa decisão afeta o desempenho, a consistência e a complexidade.

Cache-Aside

Este é o padrão mais comum. A aplicação é responsável por coordenar os dados.

  1. A aplicação consulta o cache
  2. Se for Hit: retorna o dado ("hit" quer dizer que o dado estava na camada de cache)
  3. Se for Miss: a aplicação lê do banco
    • A aplicação atualiza o cache
    • Retorna o dado

Essa arquitetura é resiliente — se o cache falhar, o sistema continua funcionando. Uma observação: a primeira requisição pode ser mais lenta, porque o dado ainda não está no cache.

Write-Through

A aplicação escreve o dado apenas no cache, e então o cache escreve no banco de dados de forma síncrona antes de responder à aplicação.

  1. A aplicação escreve no cache.
  2. O cache escreve no banco de dados.
  3. A escrita só é confirmada quando as duas terminam.

Embora aqui a gente busque mais consistência, tentando manter o cache sincronizado com o banco, expomos a aplicação ao problema da escrita dupla. Se a atualização do cache der certo mas a escrita no banco falhar, o sistema pode ficar inconsistente.

Vale notar também que as escritas ficam mais lentas (a aplicação precisa esperar pelas duas) e que podemos acabar guardando em cache dados que nunca serão lidos (desperdício de memória).

Write-Behind (Write-Back)

Ao contrário da abordagem anterior, em que as escritas são síncronas, o Write-Behind usa uma lógica assíncrona. A aplicação escreve no cache e, depois, o cache agrupa e grava os dados no banco de forma assíncrona, em segundo plano.

  1. A aplicação escreve no cache e recebe a confirmação na hora.
  2. O cache atualiza o banco em segundo plano.

Essa arquitetura tem um desempenho de escrita extremamente rápido e pode reduzir a carga no banco (já que consegue juntar várias escritas numa só).

Por outro lado, ela traz um alto risco de perda de dados. Se o cache cair antes de atualizar o banco, aquele dado se perdeu!

Read-Through

Parecido com o Cache-Aside, mas a aplicação trata o cache como o armazenamento principal. É o próprio cache, e não a aplicação, que busca no banco quando o dado não está lá.

  1. A aplicação lê do cache
  2. Se der miss no cache:
    • Busca no banco de dados
    • Guarda o dado em cache
    • Retorna a informação para a aplicação

Políticas de remoção do cache

Um cache tem espaço limitado. Quando ele enche, é preciso decidir o que apagar para abrir espaço para dados novos. Essa decisão se chama "remoção" (eviction).

Least Recently Used (LRU)

"Se você não usa há um tempo, provavelmente não vai precisar tão cedo." O sistema remove o item que foi acessado há mais tempo. É o padrão da indústria para a maioria das aplicações web.

Least Frequently Used (LFU)

"Se você usa pouco, livre-se disso."

Nesta política, o sistema conta quantas vezes cada item é acessado. Os itens com a menor contagem são removidos. Não é simples de implementar; alguns sistemas usam um LFU aproximado para evitar o alto custo de uma contagem precisa.

First-In-First-Out (FIFO)

"Os itens mais antigos saem primeiro."

Parecido com a estrutura de dados fila. O item que está há mais tempo no cache é removido, não importa com que frequência ele é acessado.

Time-To-Live (TTL)

"Este dado se autodestrói em x minutos."

Os itens são apagados automaticamente depois de um período definido, independentemente do espaço. Isso é essencial para dados que mudam com frequência, como cotações de ações ou feeds de notícias.

Problemas e desafios comuns

Cache é uma estratégia muito útil e importante, mas implementá-lo pode trazer algumas complexidades. Para entender os trade-offs, vamos ver alguns casos de borda.

Cache Stampede (Thundering Herd)

Imagine que uma chave de cache popular (como "homepage_news") expira. De repente, 10.000 usuários pedem aquele dado ao mesmo tempo. Todas as 10.000 requisições recebem um "Cache Miss" e batem no banco de dados no mesmo milissegundo, podendo derrubá-lo.

Formas de lidar com isso:

  1. Request Coalescing (Singleflight): o sistema percebe que vários usuários estão pedindo o mesmo dado. Ele deixa só a primeira requisição ir ao banco. As outras 9.999 ficam esperando. Quando a primeira volta, o resultado é compartilhado com todo mundo
  2. Cache Warming: você roda um script para popular o cache antes de ele expirar ou antes de o tráfego subir, garantindo que os usuários nunca encontrem um cache "frio".
  3. Expiração antecipada probabilística: se uma chave expira em 60s, você pode buscá-la de novo aleatoriamente em 55s ou 58s, para que a atualização aconteça antes de o stampede começar.

Consistência do cache

Manter o cache e o banco de dados em sincronia é um problema comum e muito discutido em engenharia de software. Isso acontece porque a maioria dos sistemas lê do cache, mas escreve primeiro no banco. Por exemplo, depois de uma atualização de perfil gravada no banco, a aplicação ainda pode estar servindo a versão anterior do perfil para outros usuários, porque o cache ainda não foi atualizado.

Formas de lidar com isso:

  1. TTLs curtos: mesmo que o dado esteja desatualizado, um Time-To-Live curto (por exemplo, 5 segundos) garante que o dado ruim não dure muito.
  2. Invalidação explícita: quando a aplicação escreve no banco, ela precisa mandar na mesma hora um comando para APAGAR ou ATUALIZAR aquela chave específica no cache.

Hot Keys

Às vezes, uma única chave é acessada com tanta frequência (o perfil de uma celebridade no Twitter durante um escândalo) que um único nó do cache não dá conta do tráfego, criando um gargalo enquanto os outros nós ficam ociosos.

  1. Cache local (L1 Cache): adicione um cache pequeno em memória diretamente nos servidores da aplicação. Mesmo que ele guarde o dado por apenas 1 segundo, isso economiza milhares de idas ao cache principal.
  2. Replicação de chaves: crie várias cópias da chave quente (por exemplo, key_1, key_2, key_3) e distribua entre nós diferentes. Encaminhe as requisições dos usuários aleatoriamente para uma dessas cópias.

Para fechar

Cache é um daqueles conceitos que parecem simples na superfície, mas mostram rapidamente sua profundidade quando você começa a tomar decisões de verdade: onde colocá-lo, qual arquitetura usar, como lidar com a remoção e com casos de borda como stampedes ou falhas de consistência.

A principal lição é que não existe solução única. Toda decisão de cache envolve trade-offs entre velocidade, consistência, complexidade e resiliência. Entender esses trade-offs é o que faz a diferença ao desenhar sistemas em escala.

Se quiser se aprofundar, recomendo muito o artigo sobre Caching do Hello Interview que inspirou este post. Fique de olho no próximo da série Fundamentos de System Design!