Trace Cache Keys, Deletions, and Repopulation in JavaScript
Computer Science
Trace Cache Keys, Deletions, and Repopulation in JavaScript
Trace Cache Keys, Deletions, and Repopulation in JavaScript
Build and investigate a small Node.js cache service that makes cache behavior visible. You will follow a request from its input through key construction, observe reads and writes, prove whether deletion succeeded, and identify when another layer or cache instance repopulates the value.
What you learn by building this
- Trace how a request becomes a cache key and identify mismatches between read, write, and delete paths
- Use cache hit, miss, set, and delete evidence to determine what actually happened
- Separate a successful deletion from a value that was immediately recreated
- Recognize symptoms of multiple cache instances or layered cache behavior
- Run focused HTTP requests against the finished diagnostic example and explain the observed results
Learning Journey
Make Cache Behavior Visible
3 lessonsCreate the standalone cache model and a tiny HTTP surface that can be run immediately. The learner first gains direct evidence for keys, hits, misses, writes, and deletes before investigating misleading behavior.
Prove Deletion Versus Repopulation
3 lessonsUse the running cache service to establish what deletion proves and then introduce controlled repopulation. The project becomes a diagnostic experiment rather than a cache that merely appears to work.
Find Multiple Instances and Layers
3 lessonsExtend the example just enough to reproduce the confusing case where one cache does not see another cache's deletion. The finished project lets the learner distinguish a bad key from separate cache state.
Public lesson
Create an Observable In-Memory Cache
Observable In-Memory Cache
A Map already stores key–value pairs, but it does not explain what happened when your code uses it. A missing key and a key whose value is undefined can also be easy to confuse when you only inspect the returned value.
You will wrap Map so each operation returns a structured event and stores that event in an evidence history. There is no server involved: the cache and its observations live in memory.
Predict
What will happen?
First, predict the evidence
Imagine this sequence:
cache.set("theme", "dark");
cache.get("theme");
cache.get("language");
cache.delete("theme");
cache.clear();
cache.set("theme", "dark");
cache.get("theme");
cache.get("language");
cache.delete("theme");
cache.clear();
What should each operation be able to tell you?
In particular:
- How can a
getshow both the value and whether the key existed? - How can
deletedistinguish “the key was deleted” from “nothing changed”? - What should
clearreport, since it affects more than one key?
Use those questions to shape the event objects rather than relying on plain strings such as "deleted".
Tasks
Add the cache
In the existing JavaScript file for the practice project, add this class skeleton. The constructor and record helper are provided because they only hold state and centralize history recording. The four cache operations are yours to complete.
class ObservableCache {
constructor() {
this.store = new Map();
this.history = [];
}
record(event) {
this.history.push(event);
return event;
}
get(key) {
// Check whether the key exists separately from reading its value.
// Return and record an event shaped like:
// {
// operation: "get",
// key,
// result: { value: ..., hit: ... }
// }
}
set(key, value) {
// Record whether this key was already present before changing it.
// Return and record an event shaped like:
// {
// operation: "set",
// key,
// result: { value: ..., replaced: ... }
// }
}
delete(key) {
// Map.delete already gives you a useful boolean result.
// Return and record an event shaped like:
// {
// operation: "delete",
// key,
// result: { deleted: ... }
// }
}
clear() {
// Capture the keys before clearing them. This makes the evidence show
// exactly which keys were affected.
// Return and record an event shaped like:
// {
// operation: "clear",
// key: null,
// result: { keys: [...], cleared: ... }
// }
}
}
class ObservableCache {
constructor() {
this.store = new Map();
this.history = [];
}
record(event) {
this.history.push(event);
return event;
}
get(key) {
// Check whether the key exists separately from reading its value.
// Return and record an event shaped like:
// {
// operation: "get",
// key,
// result: { value: ..., hit: ... }
// }
}
set(key, value) {
// Record whether this key was already present before changing it.
// Return and record an event shaped like:
// {
// operation: "set",
// key,
// result: { value: ..., replaced: ... }
// }
}
delete(key) {
// Map.delete already gives you a useful boolean result.
// Return and record an event shaped like:
// {
// operation: "delete",
// key,
// result: { deleted: ... }
// }
}
clear() {
// Capture the keys before clearing them. This makes the evidence show
// exactly which keys were affected.
// Return and record an event shaped like:
// {
// operation: "clear",
// key: null,
// result: { keys: [...], cleared: ... }
// }
}
}
The important detail in get is to use both has and get:
const hit = this.store.has(key);
const value = this.store.get(key);
const hit = this.store.has(key);
const value = this.store.get(key);
Checking only value would not tell you whether a missing key and a stored undefined are different cases.
For set, check whether the key exists before calling set. Otherwise every set would look like a replacement.
For clear, Map.clear() does not return the removed keys. Take a snapshot first:
const keys = [...this.store.keys()];
const keys = [...this.store.keys()];
Then clear the map and record that snapshot.
Each method should finish by passing its event to this.record(event). That gives the caller immediate evidence while also preserving the complete operation history.
Tasks
Try a small scenario
Below the class, add this usage code:
const cache = new ObservableCache();
const firstSet = cache.set("theme", "dark");
const firstGet = cache.get("theme");
const missingGet = cache.get("language");
const firstDelete = cache.delete("theme");
const secondDelete = cache.delete("theme");
console.log(firstSet);
console.log(firstGet);
console.log(missingGet);
console.log(firstDelete);
console.log(secondDelete);
console.log(cache.history);
const cache = new ObservableCache();
const firstSet = cache.set("theme", "dark");
const firstGet = cache.get("theme");
const missingGet = cache.get("language");
const firstDelete = cache.delete("theme");
const secondDelete = cache.delete("theme");
console.log(firstSet);
console.log(firstGet);
console.log(missingGet);
console.log(firstDelete);
console.log(secondDelete);
console.log(cache.history);
Run the project using its documented command.
Inspect the output and compare it with these expectations:
- The first
setreportsreplaced: false. - The
get("theme")reports{ value: "dark", hit: true }. - The missing
get("language")reportshit: false. - The first delete reports
deleted: true. - The second delete reports
deleted: false. cache.historycontains the same five events in operation order.
The missing lookup is the useful surprise here: its evidence should remain informative even though its value is undefined.
Tasks
Check replacement and clearing
Extend the scenario:
cache.set("theme", "light");
cache.set("fontSize", 16);
const clearEvent = cache.clear();
console.log(clearEvent);
console.log(cache.history);
console.log(cache.store.size);
cache.set("theme", "light");
cache.set("fontSize", 16);
const clearEvent = cache.clear();
console.log(clearEvent);
console.log(cache.history);
console.log(cache.store.size);
Now check that:
- Setting
"theme"the second time reportsreplaced: true. - The clear event lists the keys that existed immediately before clearing.
clearEvent.result.clearedequals the number of listed keys.cache.store.sizeis0.- The clear event is also the final item in
cache.history.
If the clear event lists no keys, the snapshot was probably taken after clear(), which is too late. If replacement is always false, the presence check is probably happening after the write.
What the wrapper changes
Without the wrapper, callers get primitive answers:
map.get("theme"); // perhaps a value, perhaps undefined
map.delete("theme"); // true or false
map.clear(); // no useful return value
map.get("theme"); // perhaps a value, perhaps undefined
map.delete("theme"); // true or false
map.clear(); // no useful return value
That is enough for storage, but not enough for inspection. The cache now makes each action explain itself:
{
operation: "delete",
key: "theme",
result: { deleted: true }
}
{
operation: "delete",
key: "theme",
result: { deleted: true }
}
The history array also makes the cache observable after the fact. You can inspect the sequence of writes, reads, misses, deletes, and clears without adding a web server or changing the underlying Map behavior.
Course Outline
3 modules · 9 lessons
Make Cache Behavior Visible
Prove Deletion Versus Repopulation
Find Multiple Instances and Layers
Learn by building your own version.
Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.