Skip to content

    System Design

    Cache Layers: Client, CDN, Application and Database

    See what each cache layer stores, who can purge it, and how staleness stacks up across browser, CDN, application and database caches, with a ShopStream example.

    By Published 22 min read

    Share

    Flagship lessonintermediateBackend System Design

    Before this: Caching Basics, CDN and Edge Networking (coming soon)

    What you'll learn: By the end of this lesson you will be able to name the four cache layers between a user and the data, say for each one what it stores, who owns it, whether it is private or shared, and whether you can reach in and purge it, calculate how much staleness a stack of layers can add up to, read the HTTP headers that steer the browser and CDN layers, explain what the database's own caches do and do not do, and choose the layer where a given piece of data belongs.

    A clerk at a head office is asked for a customer's address. She checks the sticky note on her monitor first. If it is not there she asks the branch office down the road, which keeps a binder of the addresses its own customers ask for. If the binder is silent she opens the head-office filing cabinet, and only after that does she send someone to the archive in the basement. Every step is slower than the one before, and every step can hold a copy that is out of date. The sticky note is yours alone, the binder serves everyone at that branch, the cabinet serves the whole building, and the archive is the original. Nobody can walk into a customer's home and rewrite their sticky note.

    That is a stack of caches. Caching Basics explains why one cache works and Advanced Caching covers write policies, invalidation and stampedes. This lesson is about the layers themselves: what is different about a cache in a browser, at a CDN edge, next to your code or inside the database, and what happens when they are stacked. CDN and Object Storage goes deeper on the CDN layer.

    Verified against the RFC 9111 text, PostgreSQL 18, MySQL 8.0 and MySQL 8.4 documentation, the Amazon CloudFront developer guide and the Redis documentation, all read on 2026-11-08. Defaults differ between products and versions, so check the page for the version you run.

    The core idea

    A cache layer is defined less by what it stores than by who controls it. Every layer holds a copy and trades freshness for speed, so the useful questions are about control: Who decides when the copy is dropped? Is it shared between users? Can the owner of the data reach in and remove it? How far is it from the user, and how much of the work does a hit skip?

    The closer a layer sits to the user, the more latency a hit saves and the less control you have over it. The browser cache is the fastest and you cannot touch it. The application cache is slower and entirely yours. The rest of the lesson follows that tension.

    Three illustrations run through the lesson, each labelled:

    • Minimal example: one HTTP response passing through a browser, a CDN, a Redis cache and a database, with a cold path and a warm path.
    • Realistic example: ShopStream's product page when a price changes, in "In practice".
    • Failure example: a personalised response stored in a shared cache and served to a stranger.

    The four layers at a glance

    Four cache layers between a shopper and the dataShopper to Browser cache; Browser cache to CDN edge cache (miss); CDN edge cache to Application (miss); Application to Redis (lookup); Redis to Database (miss)NOT YOURS TO PURGEYOURS, WITH LIMITSYOURSShopperShopperBrowser cache: private, per userBrowser cacheprivate, per userCDN edge cache: shared, per edgeCDN edge cacheshared, per edgeApplication: in-process cacheApplicationin-process cacheRedis: shared app cacheRedisshared app cacheDatabase: buffer cache and diskDatabasebuffer cache anddiskmissmisslookupmiss

    Figure 1. The layers, grouped by how much control you have. How to read this: a request travels left to right until some layer has a fresh copy; every arrow labelled "miss" is a layer that did not have one, and the application looks in Redis before it asks the database. The zone labels are the lesson in one line: control grows as you move away from the user.

    Layer Holds Private or shared Who sets its lifetime Can you purge it?
    Client (browser, mobile app) HTTP responses, files, app state Private to one user's device Your response headers, and the client's own rules No. You can only wait for expiry or change the URL
    CDN edge Responses for many users Shared by everyone routed to that edge Your headers plus the CDN's own TTL settings Usually yes, through the provider's purge or invalidation tools
    Application Query results, fragments, sessions, computed values In-process: one server. Redis or Memcached: all servers Your code Yes, immediately
    Database Table and index pages in memory Shared by every query The database engine Not individually. You size it; the engine evicts

    Minimal example. One request for GET /api/products/123. Cold, it misses every layer, and the response fills each cache on its way back up.

    sequenceDiagram accTitle: One request through four cache layers, cold then warm accDescr: The first request misses the browser, the CDN and Redis and is answered from the database; each layer stores the response on the way back. The second request is answered by the browser cache without any network call, and a third from another user is answered by the CDN. participant U as Browser participant C as CDN participant A as App and Redis participant D as Database U->>U: look in browser cache, miss U->>C: GET /api/products/123 C->>C: look in edge cache, miss C->>A: GET /api/products/123 A->>A: look in Redis, miss A->>D: SELECT product 123 D-->>A: row A-->>C: JSON with Cache-Control, Redis now holds it C-->>U: JSON, edge now holds it Note over U: browser now holds it U->>U: second request, fresh copy, no network at all Note over C: a different user's request is answered at the edge, the app never sees it

    Figure 2. A cold request fills every layer on the way back. How to read this: time runs downward. The first request does all the work. Afterwards a repeat from the same browser costs nothing, and a request from another user stops at the CDN. The database sees one query for however many requests share the copy.

    Layer 1: the client

    The client cache is the browser's HTTP cache (and the equivalent in a mobile app's networking library, plus any state the app keeps in memory). It is private: one cache per browser profile, never shared with another person. RFC 9111, the HTTP caching specification, defines a private cache as one dedicated to a single user and a shared cache as one that serves more than one user.

    What it stores and for how long is steered by the response headers you send:

    Header What it tells the cache
    Cache-Control: max-age=N The response is stale once its age passes N seconds
    Cache-Control: s-maxage=N The same, for shared caches only; it overrides max-age and Expires there
    Cache-Control: private A shared cache must not store this response; a private cache may
    Cache-Control: no-cache May be stored, but must be validated with the origin before every reuse
    Cache-Control: no-store Do not keep any part of the request or response (the RFC notes this is not a reliable privacy mechanism)
    Vary: Accept-Language The stored copy is reusable only for requests whose named headers match

    The freshness lifetime is chosen by a fixed order, the first rule that matches wins: s-maxage (shared caches only), then max-age, then Expires, and finally a heuristic if none is present (RFC 9111, section 4.2.1). The specification suggests that a heuristic uses a fraction of the time since Last-Modified, typically about 10 percent (section 4.2.2), so a response with no explicit lifetime can still be reused for a while. Send an explicit header for everything you care about.

    Two properties define this layer.

    1. It is the cheapest hit there is. A fresh hit needs no network round trip, so no server, CDN or database does any work. For static files that never change under one URL, this is the layer to use fully: a content-hashed filename with a very long max-age.
    2. You cannot purge it. Once the browser has a fresh copy, you have no channel to the user's device. If the data changes, the user keeps the old copy until it expires. So the lifetime you hand to a browser must be one you can live with being wrong, and changing the URL (versioning the key) is the only real invalidation here. The front end's side of this layer belongs to the Frontend System Design track.

    Server-assisted client caching. A back-end service can also hold a local in-memory cache of data from a remote store, and the same "cannot purge" problem appears in miniature. Redis 6 added direct support, called tracking. In the default mode the server remembers which keys each client read and sends an invalidation message when one is modified, or evicted because it expired or because of the memory policy. In broadcasting mode the server remembers nothing; clients subscribe to key prefixes, so the server spends no memory but sends more messages. The documentation lists the hazards a client must handle: flush the local cache when the invalidation connection is lost, put a maximum TTL on every local entry, and guard against an invalidation that arrives before the reply to the read it concerns. The pattern suits small, hot, read-mostly data, and the server's invalidation table can itself evict entries.

    Layer 2: the CDN

    A CDN edge cache is a shared cache run by someone else, in many locations, in front of your origin. Every user routed to the same edge shares its contents, so one miss pays for many later hits. It is the first layer where you can both save a huge amount of work and still change your mind: unlike the browser, the edge is something you can purge through the provider's tools. (CDN and Object Storage covers cache keys, purging and cost.)

    What the CDN does with your headers is provider-specific, so read the provider's page. Amazon CloudFront's developer guide is a good example of the shape:

    • With no cache policy, the default TTL is 24 hours; minimum, maximum and default TTL settings in the cache behaviour interact with the origin's headers.
    • With Cache-Control: max-age and a minimum TTL of 0, CloudFront caches for the lesser of max-age and the maximum TTL. If s-maxage is also present, the edge uses s-maxage (again capped by the maximum TTL) and browsers use max-age, which is how you give the edge a longer life than browsers.
    • It supports stale-while-revalidate and stale-if-error, serving a stale object while it is refreshed or while the origin is failing. (RFC 9111 itself does not define them.)
    • When an object expires, CloudFront asks the origin whether it is still current; 304 Not Modified keeps the copy.
    • An edge may evict a rarely requested object before it expires, and a viewer's Cache-Control or Pragma request headers do not force it back to the origin.

    Two lessons follow for any CDN. First, expiry is not residency: a cached object can disappear before its TTL, so the CDN never guarantees a hit, only the right to serve one. Second, a CDN is a shared cache, so anything user-specific must be kept out of it. Cache-Control: private, or no-store, and a correct Vary are what keep a personalised page from being served to the next visitor. The failure example below is exactly this mistake.

    What belongs here: static assets, public images and pages, and any API response identical for everyone that tolerates short staleness. What does not: carts, sessions and anything keyed by who is asking.

    Layer 3: the application

    The application cache is the layer you build. It has two forms with different trade-offs.

    Form Where it lives Latency Shared across servers? Typical content
    In-process Memory of each app server Memory access, no network No: every server has its own copy Configuration, small hot lookups
    Distributed (Redis, Memcached) A separate service all servers share One network round trip Yes Query results, rendered fragments, sessions, rate-limit counters

    The in-process form is faster and simpler but fragments: with twelve servers there are twelve copies, they can disagree, and each warms up separately. The distributed form costs a network hop and is one more thing to run, but one fill serves every server and one delete removes the entry for all of them. Many systems use both: a tiny in-process cache with a seconds-long TTL in front of Redis, so the hottest keys do not cost a network call. The patterns for filling and invalidating this layer (cache-aside, write-through, stampede protection) are in Advanced Caching; the capacity side of it (what happens when it fills up) is the subject of Eviction Policies: LRU, LFU and TTL.

    This is the only layer where an entry's lifetime can be tied to a business event: when a price changes, application code can delete the Redis key in the code path that commits the change. It is also the layer you are responsible for keeping consistent with the source of truth. A cache that is the only holder of a fact is a database with no durability.

    Layer 4: the database

    Databases cache too, and you mostly do not control it. A relational database keeps recently used table and index pages in memory so repeated reads skip the disk. In PostgreSQL that memory is shared_buffers; the PostgreSQL 18 manual gives a default of typically 128 MB and suggests 25 percent of system memory as a reasonable starting value on a dedicated server with 1 GB or more of RAM. It adds that because PostgreSQL also relies on the operating system's cache, a setting above 40 percent of RAM is unlikely to help. So a PostgreSQL read can be served from shared_buffers, from the operating system's file cache, or from disk, and the database does not give you a key to purge.

    Two related facts keep this layer in perspective:

    • A related planner setting, effective_cache_size, does not allocate anything. The manual says it only tells the planner how big the cache available to a query is likely to be (default 4 GB), which influences whether it favours index scans. It is an estimate, not a cache.
    • MySQL's InnoDB manages its buffer pool as an LRU list with a midpoint: a newly read page enters the middle of the list, at the head of the "old" sublist (3/8 of the pool by default), and an access to an old page makes it "young", moving it to the head of the young sublist. The manual warns that by default pages read by queries are moved to the young sublist at once, so a table scan, such as a mysqldump or a SELECT with no WHERE clause, can still evict frequently used pages, and it describes tuning to limit that. This is the scan problem that the eviction lesson takes apart.

    MySQL also shows that a database-side result cache is hard to keep correct: the query cache, which stored the text of a SELECT with its result, was removed in MySQL 8.0, along with its statements and variables. PostgreSQL has no equivalent result cache in its core. If you want to cache query results, you do it in the layer above (Layer 3) where you control invalidation.

    Everything upstream exists so that the database sees only the requests nobody else could answer.

    How the layers multiply

    The layers multiply. If the CDN answers 60 percent of requests and the application cache answers 90 percent of the rest, the database sees 40 percent of 10 percent: 4 percent of the original traffic. Those percentages are illustrative. A subtlety is worth knowing: the second cache sees only what the first one let through. A CDN absorbs the hottest items, so the application cache behind it faces a flatter, colder stream and its own hit ratio is usually lower than it would be alone. Measure each layer's hit ratio separately, and judge it against the traffic it actually receives.

    Staleness across layers

    Every layer adds a way for the user to see old data. Two rules help you reason about the total.

    Rule 1: purge what you can, and give short lifetimes to what you cannot. Deleting a Redis key fixes Layer 3 at once. A CDN purge fixes Layer 2 in whatever time the provider takes. Layer 1 cannot be fixed, so the browser's lifetime sets a floor under how stale a user can be.

    Rule 2: lifetimes add up unless the layers share the age. If each cache starts its own clock when it receives a response, the worst case is the sum of the lifetimes. HTTP caches are designed to avoid that: the Age header conveys the sender's estimate of the time since the response was generated or validated at the origin (RFC 9111, section 5.1), and the receiving cache adds it to the age it calculates for itself (current_age, section 4.2.3). A browser that receives a response with Age: 58 and max-age=30 treats it as already stale. So where every HTTP cache in the path passes Age along, staleness is bounded by the longest TTL, not their sum. Caches that sit outside HTTP, such as a Redis entry sitting in front of the origin, do not propagate an age to downstream caches, so their lifetimes do add.

    Worked example (illustrative numbers). A response carries Cache-Control: public, max-age=30, s-maxage=60. The origin's Redis entry has a 600 second TTL and is deleted on every write.

    Situation Worst-case staleness after a database commit
    Redis delete succeeds 0 s from Redis. The CDN may hold a copy fetched just before the commit: up to 60 s old. A browser that got it from the CDN at Age 59 is stale at once, so if Age is passed the total stays about 60 s
    Age is not passed or not honoured Up to 60 s (CDN) plus 30 s (browser) = 90 s
    The Redis delete is lost (a network error, a bug) Redis can keep the old value for its whole TTL: 600 s, then 60 s CDN, then 30 s browser. Up to 600 + 60 + 30 = 690 s

    The last row is why every entry keeps a TTL even with explicit invalidation: it turns "unbounded" into "690 seconds". It is also why the TTL of a layer you do not control (the browser) should be the shortest of the three.

    sequenceDiagram accTitle: A price change travelling through four cache layers accDescr: The app commits a new price and deletes the Redis key. The CDN still serves the old price until its copy expires or is purged. A browser holding a fresh copy keeps showing the old price until its max-age passes. participant W as Store admin participant A as App participant R as Redis participant D as Database participant C as CDN participant B as Shopper browser W->>A: change price to 3.49 A->>D: UPDATE price, commit A->>R: DELETE product:123 Note over R: layer 3 is fresh now B->>C: GET product 123 C-->>B: old price 3.99 from the edge (within s-maxage) Note over C: layer 2 stays stale until expiry or purge Note over B: layer 1 stays stale until max-age passes A->>C: purge product 123 B->>C: GET product 123 after max-age C->>A: miss, refetch A-->>C: new price 3.49 C-->>B: new price 3.49

    Figure 3. The write reaches the layers in order of how much you control them. How to read this: the delete on Redis is instant, the CDN purge takes effect when the provider processes it, and the browser changes only when its own timer ends. Whatever the browser holds is the limit on how fast a user can see the change.

    Choosing a layer

    flowchart TD accTitle: Where does this data belong in the cache stack accDescr: A decision flow. If the response depends on who is asking, keep it out of shared caches. If it never changes under its URL, cache it in the browser for a long time. If it is the same for everyone and tolerates staleness, use the CDN with a short lifetime. Otherwise cache it in the application where you can invalidate it on write, and leave exact data in the database. S["A piece of data"] --> Q1{"Is it different for each user?"} Q1 -->|"yes"| P["Private or no-store for HTTP. Cache per user in the app, keyed by user id"] Q1 -->|"no"| Q2{"Does it ever change under the same URL?"} Q2 -->|"no, versioned"| L["Browser and CDN: very long max-age, immutable file names"] Q2 -->|"yes"| Q3{"Can users tolerate staleness of seconds to minutes?"} Q3 -->|"yes"| E["CDN with a short s-maxage, app cache behind it, delete on write"] Q3 -->|"no"| X["App cache with strict invalidation, or read the database"]

    Figure 4. A per-resource decision. How to read this: walk each response through the three questions. Per-user data goes down the first branch and never reaches a shared layer. Files with versioned names go left to long lifetimes. Changing data needs a staleness budget you can state in seconds.

    Rules of thumb that fall out of the layers: push static, shared, versioned bytes outward to the browser and CDN; cache changing, shared data behind a short CDN lifetime and a deletable app cache; keep per-user data in the application tier, keyed by user, or in the browser with private; and never cache what must be exact at the moment of use (stock at payment time, an authorisation that must reflect revocation instantly): read the source of truth.

    Failure example: a personalised response in a shared cache

    A shared cache assumes that a response with the same URL is the same response for everyone. When a personalised response carries no private or no-store, and nothing in Vary separates the users, a CDN can store the first visitor's copy and serve it to the second.

    sequenceDiagram accTitle: A personalised cart response leaks through a shared cache accDescr: Shopper A requests the cart summary, which has no private directive. The CDN stores it. Shopper B requests the same URL and receives Shopper A's cart. After the fix, the response is marked private and Shopper B is served from the origin. participant A as Shopper A participant C as CDN edge participant O as Origin participant B as Shopper B A->>C: GET /api/cart/summary with A's cookie C->>O: forward O-->>C: A's cart, Cache-Control: max-age=60 C-->>A: A's cart Note over C: stored, the URL is the cache key B->>C: GET /api/cart/summary with B's cookie C-->>B: A's cart from the edge Note over B: B sees someone else's data

    Figure 5. The cache key was the URL, and the URL does not say who is asking. How to read this: nothing is broken in the CDN. It did what the headers allowed. The bug is the missing Cache-Control: private (or no-store) on a response that depends on the caller.

    What breaks: privacy, and trust. Detection: a test that requests a personalised URL as two different users and fails if the bodies match, plus a header check on every authenticated route. Prevention: send Cache-Control: private (a shared cache must not store it, per RFC 9111) or no-store for anything that depends on the caller, and use Vary only when sharing is genuinely wanted. no-store is not a security control by itself: the RFC says it is not a reliable privacy mechanism.

    The other side of this topic

    The front end sees the same layers from the other end: how the browser stores, revalidates and evicts responses, how a service worker and an in-memory data cache add layers of their own, and how a deployment that changes file names interacts with all of them. The Frontend System Design track covers this in its lessons on HTTP caching and on caching across the stack, which complement this one. The back-end rule that makes the front end's job possible is the one in this lesson: send explicit, correct cache headers, and know which layers you can purge.

    Trade-offs

    Layer Gain Cost Wrong choice when
    Client Zero server work, fastest hit Cannot be purged, staleness is on the user's device The data changes and must show immediately
    CDN Offloads origin, short distance to users Shared (leak risk), purge delay, provider cost The response is personalised or the URL does not identify the content
    In-process No network Per-server copies disagree, cold start per server The data must be identical on every server
    Distributed (Redis) One copy, deletable Network hop, another system to run The extra hop outweighs the saving
    Database Automatic Not under your control, shared by every query You expect it to replace an application cache

    In practice: the ShopStream product page when a price changes

    This is the realistic example, an educational scenario on the course's running example, ShopStream. It describes no real company, and every number other than the shared ShopStream figures is illustrative.

    This is a realistic scenario, not a case history.

    The situation

    ShopStream peaks at about 2,300 browsing requests per second (the shared estimate from the project brief), most of them product pages and product API calls over a catalogue of 100,000 products. Stores change prices through the day. The team wants the database to see a small fraction of the traffic, but a shopper must never pay a price different from the one at checkout, and a price change should reach the page within about a minute.

    Starting architecture

    ShopStream read path with illustrative traffic at each layerShoppers to CDN (GET product); CDN to App servers (40% miss); App servers to Redis (lookup); Redis to PostgreSQL (10% miss)Shoppers: 2,300 req/s at peakShoppers2,300 req/s atpeakCDN: s-maxage=60, 60% hitCDNs-maxage=60, 60%hitApp servers: 920 req/sApp servers920 req/sRedis: 90% hit, delete on writeRedis90% hit, deleteon writePostgreSQL: about 92 req/sPostgreSQLabout 92 req/sGET product40% misslookup10% miss

    Figure 6. The design under discussion, with the hit ratios as assumptions. How to read this: the numbers on the nodes are not measurements. They show how the layers multiply: 2,300 x 0.4 = 920 requests per second reach the app, and 920 x 0.1 = 92 reach PostgreSQL.

    Options considered

    Option Staleness after a price change Load on the origin Risk
    A. Browser max-age=300, no CDN purge Up to 5 minutes, and nothing can shorten it Lowest Users see an old price for minutes, and no purge can reach them
    B. CDN s-maxage=60, browser max-age=0 with validation, Redis delete on write About 60 s worst case Moderate A stale price on the page for up to a minute
    C. No CDN caching for product data 0 s from the edge Highest: all 2,300 req/s reach the app The origin carries the whole peak

    The decision

    Option B. Product JSON is the same for every shopper, so it is safe in a shared cache. The edge gets a one-minute life, the browser revalidates each time, and the application deletes the Redis key in the code path that commits the price. Checkout never reads the cached price: it re-reads the database and uses that, so a stale page cannot change what a shopper pays. The response headers:

    Cache-Control: public, max-age=0, s-maxage=60
    Vary: Accept-Encoding

    public, max-age=0, s-maxage=60 lets the shared cache keep the response for 60 seconds while the browser treats its own copy as immediately stale and revalidates it (the app adds an ETag so that revalidation can end in a cheap 304). The cart and account endpoints get Cache-Control: private, no-store, so they never reach the shared layer.

    Rollout and verification

    A rollout would add the headers behind a flag for one route first and compare three numbers against the previous week: the CDN's hit ratio, requests reaching the app, and PostgreSQL queries per second. A price-change test would change a price and poll the page until it shows the new value (expected: about 60 seconds at most, plus any purge time). A second test would request an authenticated URL as two users and assert the bodies differ.

    The 2 a.m. failure

    The page reads: support tickets say shoppers saw a price of 3.99 on the product page and 3.49 at checkout, for several minutes after a store's price cut. The checks: the response headers on the product URL (did someone ship max-age=300?), the CDN's reported age for the object, and whether the Redis delete ran (a failed delete would leave a 600-second entry behind). The mitigation is a purge of the product URL on the CDN and a manual delete of the key. The lasting fix is to put the delete and the purge in the same retried, observable step as the price commit, to keep the Redis TTL as a safety net, and to alert when the age of a served product response exceeds the budget. Checkout used the database price, so no shopper was charged the wrong amount: the page was stale, but the invariant held.

    Architect's notes

    • Write down a staleness budget per resource in seconds, then choose lifetimes that sum to less than it for the layers you cannot purge.
    • Prefer URL versioning to invalidation for anything static: a new URL needs no purge at any layer.
    • Give the shared layers the shortest list of content. Everything personalised stays out by default; add exceptions deliberately.
    • Measure each layer separately. A CDN hit ratio, a Redis hit ratio and database load tell different stories, and the layers behind a busy one see a colder stream.
    • Make the cache safe to lose. If clearing a layer breaks correctness, it is acting as a database.
    • Check provider behaviour. TTL clamps, Age handling, purge delays and stale-serving differ between CDNs and versions.

    Common mistakes

    • Sending no Cache-Control at all, so a cache may apply a heuristic lifetime of its own.
    • Caching a personalised response in a shared layer. private or no-store, and a correct Vary, keep it out.
    • Long browser lifetimes on mutable URLs. You cannot purge the browser.
    • Relying on a CDN to hold an object until its TTL. It may evict earlier.
    • Assuming no-store is a security control. The RFC says it is not.
    • Stacking layers without adding up the TTLs. A lost invalidation leaves the longest TTL in force.
    • Expecting the database to be your result cache, or caching exact data used at the moment of decision, such as the price at payment.

    Key takeaways

    • A cache layer is defined by who controls it: the closer to the user, the faster the hit and the less control you have.
    • Client caches are private and cannot be purged; CDN caches are shared and purgeable through the provider; application caches are yours; the database's caches are the engine's.
    • HTTP headers steer the first two layers, with a fixed order for computing the freshness lifetime.
    • Layers multiply their savings, and a deeper layer sees a colder, flatter stream.
    • Staleness is the sum of lifetimes where age is not shared and roughly the longest where it is; keep a TTL on every entry.
    • A personalised response in a shared cache is a data leak; mark it private or no-store.

    Practice exercises

    Exercise 1 (30 min). For six ShopStream responses (home page, product images, product JSON, cart summary, order history, a public store list), choose the Cache-Control header and say which layers may store it.

    Exercise 2 (30 min). Compute the load reaching PostgreSQL if the CDN hit ratio is 80 percent and the Redis hit ratio is 70 percent at 2,300 requests per second. Then explain why the Redis hit ratio could fall once the CDN hit ratio rises.

    Exercise 3 (45 min). With max-age=120, s-maxage=300 and a Redis TTL of 900 seconds, state the worst-case staleness after a commit in each of the three situations in the worked example. Say which assumption changes the answer most.

    Exercise 4 (1 hour). Design the invalidation path for a price change: the commit, the Redis delete, the CDN purge and the retries. State what happens if the purge fails, and what bounds the staleness then.

    Exercise 5 (2 hours). Choose any API you know and write a test that requests a personalised URL as two users and checks the bodies differ and the response headers contain private or no-store. Decide where in a deployment pipeline it should run.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments