Basic Data Structures (Objects, Maps, Sets) Questions
Understand how objects work in JavaScript including prototypal inheritance and property descriptors. Know when to use Maps vs Objects and Sets vs Arrays. Understand the performance characteristics of different data structures. Be comfortable with nested data structures and how to manipulate them efficiently.
MediumSystem Design
71 practiced
Design an in-memory feature cache for a Node.js microservice that must hold ~100k keys within ~1GB of memory and serve 10k req/s. Describe architecture choices: Map vs Object, eviction policy (LRU/TTL), sharding across CPU cores, persistence/fallback to disk, serialization for warm restarts, and how to monitor and instrument cache health. Explain trade-offs and failure modes.
Sample Answer
Requirements clarification:- ~100k keys, ~1 GB memory, 10k req/s, low latency, Node.js microservice, ML features (reads heavy, writes moderate), availability and graceful restarts.High-level design:- Run N worker processes (Node cluster or separate containers) to utilize CPU cores. Each worker holds an in-memory shard of the feature cache (shard-by-key hash). This avoids locks and context switching; requests are routed (consistent hashing or sticky load balancer) to the worker owning the key.Map vs Object:- Use JavaScript's Map for the in-memory store. Map has stable iteration order, predictable semantics, O(1) key operations, and avoids prototype pollution issues. Objects are fine for tiny uses but have pitfalls (string-only keys, accidental collisions). For performance, prefer Map or a native addon (e.g., node-addon with C++ unordered_map) if GC becomes an issue.Eviction policy:- Combine TTL + capacity-based LRU: - TTL ensures features expire (staleness) — store per-key expiry. - LRU ensures memory stays bounded when hot set grows beyond target. Implement per-shard LRU (e.g., quick-lru library) or an approximate CLOCK algorithm to reduce overhead. - On read, check expiry and mark recency. On writes, evict least-recent until under target size.Sharding across CPU cores:- Use one Map+LRU per worker (shard). Route requests by consistent hashing on key. Benefits: lock-free, better cache locality, per-shard memory accounting and eviction. Trade-off: cross-shard reads for bulk requests — mitigate by batching requests to multiple workers or using a shared service layer.Persistence / fallback:- Primary fallback: backing store (Redis or RocksDB) for cold misses and durable storage. Write-through or write-back depends on consistency requirements; for ML features, write-through simplifies correctness (also async write to backing store for throughput).- Periodic snapshotting: each shard serializes its content to disk (compact binary) every T seconds or when under low load. On restart, workers load snapshots (parallel). For large datasets, incremental WAL (append-only) + snapshot reduces recovery time.Serialization for warm restarts:- Use compact binary formats (MessagePack or Protocol Buffers) to reduce size and speed parsing. Persist only keys, values, and expiry timestamps. Load in parallel into shards, rebuild LRU metadata (approximate by access timestamps if saved). Optionally store bloom filter or top-k hot set to warm critical entries first.Monitoring & Instrumentation:- Expose metrics per-shard and aggregated: - Hit rate, miss rate, request rate (req/s), average and p99 latency for cache ops - Evictions/sec, expirations/sec, insert rate - Memory usage (RSS and heapUsed), entries count - Load time on restart, serialization size- Emit metrics to Prometheus + Grafana, alert on low hit-rate, high evictions, memory > threshold, restart times.- Tracing for slow operations, logs for fallback reads to backing store.Trade-offs:- Map + in-process gives lowest latency but limited by node memory and per-process GC pauses. Large heaps may trigger long GC.- Sharding avoids locks but increases complexity in request routing and makes global operations (e.g., full scan) expensive.- LRU exactness vs CPU overhead: exact LRU is accurate but costlier; approximate policies reduce CPU but may evict hot items.- Durability vs performance: synchronous persistence increases latency; async write-through/backing store reduces latency but risks temporary divergence on crash.- Serialization format: JSON is simple but larger and slower; protobuf/MessagePack faster and smaller but requires schema management.Failure modes and mitigations:- OOM / GC pauses: cap per-shard memory, monitor heap and restart gracefully or shed load. Use node options (--max-old-space-size) and consider moving to a C++ store or external cache if needed.- Thundering herd on miss: use request coalescing (singleflight) so only one backend fetch per key in-flight.- Stale reads: TTL plus versioning or timestamps; for ML, include feature timestamp and accept eventual consistency or use synchronous refresh when strict freshness required.- Partial restart/warmup leading to high miss spike: stagger restarts, pre-warm using snapshot and warm-first load of top-k hot keys.- Network partition with backing store: operate in degraded mode with current in-memory snapshot; expose increasing miss metric and fail fast if necessary.Example libraries/practices:- quick-lru or lru-cache for JS LRU; node-cluster or PM2 for workers; Redis or RocksDB for backing store; protobuf/MessagePack for serialization; Prometheus for metrics.Sizing check:- 100k keys in ~1GB → ~10 KB per key average; ensure value encoding is compact (binary, not verbose JSON). If features are large, consider offloading large embeddings to external storage (Redis with memory-optimized structures or object store) and keep indexes locally.Overall: prefer per-core Map+LRU+TTL shards, backing store as cold storage, compact binary snapshots for warm restarts, strong monitoring, and operational controls (memory caps, request coalescing) to manage failure modes.
HardTechnical
85 practiced
Implement serialize(obj) and deserialize(str) in JavaScript that preserve Map and Set types within nested structures. Your serializer should convert Maps and Sets into JSON in a way that can be reversed by deserialize into original types. Do not handle functions; throw on circular references. Provide example round-trip behavior for nested containers.
Sample Answer
Approach: convert the input into a JSON-safe intermediate structure that marks Maps and Sets with a __type tag and recursively transforms keys/values. Detect circular references with a WeakSet and throw if encountered. Then JSON.stringify that intermediate. For deserialize, parse and recursively rebuild Map/Set/Array/Object from the tagged structure.Key points:- Uses a tagging scheme: {__type:'Map'|'Set'|'Object'|'Array', ...}.- Preserves Map keys by serializing keys recursively (so object keys work).- Detects circular references via WeakSet and throws.- Does not support functions, symbols, undefined, Date, RegExp, prototypes, or non-enumerable properties (these can be added if needed).Complexity:- Time: O(n) where n is number of nodes (values, keys, entries) visited.- Space: O(n) for the intermediate representation and resulting string.Edge cases and trade-offs:- Keys that are objects are preserved by serializing them — after deserialization object identity is not preserved (distinct object instances reconstructed).- Date/RegExp/typed arrays are not handled; could be added with additional __type tags.- This approach favors clarity and reversibility over compactness; a binary or custom format could be more compact for large datasets.
javascript
// Serialize and deserialize preserving Map and Set and nested structures.
// Throws on functions and circular references.
function serialize(obj) {
const seen = new WeakSet();
function transform(value) {
// primitives
if (value === null) return null;
const t = typeof value;
if (t === 'number' || t === 'string' || t === 'boolean') return value;
if (t === 'function' || t === 'symbol' || t === 'undefined') {
throw new TypeError('Cannot serialize functions, symbols, or undefined');
}
// objects (including Array, Map, Set)
if (t === 'object') {
if (seen.has(value)) throw new TypeError('Circular reference detected');
seen.add(value);
if (Array.isArray(value)) {
const arr = value.map(transform);
return { __type: 'Array', items: arr };
}
if (value instanceof Map) {
// store entries as [keyTransformed, valueTransformed]
const entries = [];
for (const [k, v] of value.entries()) {
entries.push([transform(k), transform(v)]);
}
return { __type: 'Map', entries };
}
if (value instanceof Set) {
const values = [];
for (const v of value.values()) values.push(transform(v));
return { __type: 'Set', values };
}
// Plain object
const out = { __type: 'Object', props: {} };
for (const key of Object.keys(value)) {
out.props[key] = transform(value[key]);
}
return out;
}
throw new TypeError('Unsupported type: ' + t);
}
return JSON.stringify(transform(obj));
}
function deserialize(str) {
const parsed = JSON.parse(str);
function revive(node) {
if (node === null) return null;
if (typeof node !== 'object') return node;
// Recognize tagged structures
if (!node.__type) {
// raw object without type tag (should not occur because serialize tags objects)
const plain = {};
for (const k in node) plain[k] = revive(node[k]);
return plain;
}
switch (node.__type) {
case 'Array':
return node.items.map(revive);
case 'Map':
return new Map(node.entries.map(([k, v]) => [revive(k), revive(v)]));
case 'Set':
return new Set(node.values.map(revive));
case 'Object': {
const obj = {};
for (const k of Object.keys(node.props)) obj[k] = revive(node.props[k]);
return obj;
}
default:
throw new TypeError('Unknown __type: ' + node.__type);
}
}
return revive(parsed);
}
// Example round-trip
const nested = {
a: new Map([
['x', 1],
[ { complexKey: true }, new Set([1, 2, { y: 3 }]) ],
]),
b: [ new Set(['s']), { inner: new Map([[42, 'answer']]) } ]
};
const s = serialize(nested);
const restored = deserialize(s);
console.log('original:', nested);
console.log('stringified:', s);
console.log('restored instanceof Map?', restored.a instanceof Map); // true
console.log('restored.b[1].inner instanceof Map?', restored.b[1].inner instanceof Map); // trueHardTechnical
83 practiced
Many samples share the same feature schema (same keys) but different values, causing duplicated key strings and objects across millions of instances. Propose and demonstrate a memory-optimization pattern in JavaScript (flyweight/prototype sharing) that reduces per-instance memory by sharing a common prototype for default keys and only storing differing values as own properties. Provide illustrative code and discuss trade-offs (GC, mutation safety, debugging complexity).
Sample Answer
To reduce per-instance memory when millions of objects share the same keys, use a shared prototype object that holds default values (or sentinel) and only assign differing values as own properties. This is a flyweight + prototypal inheritance pattern: small own-property maps per instance, large strings and default objects live once.Approach:1. Create a shared prototype with default keys.2. For each sample, create a lightweight object whose prototype is that shared defaults object.3. Only set own properties for values that differ from the default.Example code:Key points:- Memory: shared key strings and default values (including frozen objects) are stored once on defaultProto; instances only have own properties for deltas.- Performance: property lookups follow prototype chain (fast in modern engines). Creating instances via Object.create is cheap.- GC: fewer duplicated objects/strings means less GC pressure. However, the shared proto must stay reachable, so defaults persist for app lifetime.- Mutation safety: avoid mutating shared values. Freeze default objects or use primitives. If instance needs to mutate a default object, clone first to avoid global side effects.- Debugging complexity: prototype-based defaults can hide values in stack traces/tools. Use Object.hasOwnProperty or inspect prototype to see where a property comes from.- Trade-offs: Slightly more cognitive overhead and care required for immutability, and own-property checks may be needed when serializing (e.g., JSON.stringify ignores prototype properties). For serialization, produce a plain object by merging: Object.assign({}, defaultProto, instance) or custom serializer that copies prototype keys plus own overrides.This pattern is well-suited for ML pipelines producing millions of similar feature vectors where most features are default; it reduces memory and GC overhead while preserving fast property access when used carefully.
javascript
// shared defaults live once
const defaultProto = {
featureA: 0,
featureB: 'unknown',
metadata: Object.freeze({ source: 'sensor' }) // immutable shared object
};
// factory to create lightweight sample instances
function createSample(values = {}) {
// create object with prototype = defaultProto
const obj = Object.create(defaultProto);
// assign only differing keys as own properties
for (const [k, v] of Object.entries(values)) {
if (v !== defaultProto[k]) obj[k] = v;
}
return obj;
}
// usage
const s1 = createSample({ featureA: 3.14 });
const s2 = createSample({ featureB: 'calibrated', metadata: { source: 'custom' } });
// metadata on s2 becomes own property because it differsEasyTechnical
70 practiced
Write a JavaScript function freezeFeature(obj, paths) that takes an object and an array of key paths (e.g., ['name', 'metadata.createdAt']) and sets those properties to be non-writable and non-configurable using Object.defineProperty. The function should support nested paths and leave other properties mutable. Show an example of usage and verify that JSON.stringify still includes frozen keys.
Sample Answer
Approach: traverse each dot-separated path to the target object and call Object.defineProperty on the final property to make it non-writable and non-configurable. Leave other properties unchanged. If a path doesn't exist, skip or optionally create intermediate objects — here I skip and warn.Example and verification:Key points:- Uses Object.defineProperty to set writable: false and configurable: false.- Preserves enumerability so JSON.stringify includes frozen properties.- Time complexity: O(P) where P is total number of path segments across all paths; space O(1).- Edge cases: non-existent paths (skipped), non-object input (throws), accessor descriptors (you may want to handle getters/setters separately).
javascript
// freezeFeature.js
function freezeFeature(obj, paths = []) {
if (typeof obj !== 'object' || obj === null) {
throw new TypeError('First arg must be a non-null object');
}
for (const rawPath of paths) {
const parts = String(rawPath).split('.');
let cur = obj;
// traverse to the parent of the final key
for (let i = 0; i < parts.length - 1; i++) {
if (cur && typeof cur === 'object' && parts[i] in cur) {
cur = cur[parts[i]];
} else {
// path doesn't exist; skip
console.warn(`Path "${rawPath}" not found — skipping.`);
cur = null;
break;
}
}
if (!cur) continue;
const key = parts[parts.length - 1];
if (!(key in cur)) {
console.warn(`Property "${rawPath}" not found on target — skipping.`);
continue;
}
// define non-writable, non-configurable but keep enumerable and current value
const current = Object.getOwnPropertyDescriptor(cur, key) || {
value: cur[key],
writable: true,
configurable: true,
enumerable: true,
};
Object.defineProperty(cur, key, {
value: current.value,
writable: false,
configurable: false,
enumerable: current.enumerable !== false, // preserve enumerable unless explicitly false
});
}
}javascript
const modelMeta = {
name: 'spam-detector-v1',
accuracy: 0.92,
metadata: { createdAt: '2025-01-10', owner: 'team-ml' }
};
freezeFeature(modelMeta, ['name', 'metadata.createdAt']);
// attempts to change frozen keys
try { modelMeta.name = 'hacked'; } catch(e) {}
modelMeta.metadata.createdAt = '2099-01-01'; // silent fail in non-strict mode
console.log(modelMeta.name); // 'spam-detector-v1'
console.log(modelMeta.metadata.createdAt); // '2025-01-10'
// JSON.stringify still includes frozen keys
console.log(JSON.stringify(modelMeta));
// -> {"name":"spam-detector-v1","accuracy":0.92,"metadata":{"createdAt":"2025-01-10","owner":"team-ml"}}MediumTechnical
95 practiced
Design a benchmarking harness to compare Map vs Object lookups with 1 million entries in Node.js. Describe how you'd implement warm-up, measurement (ops/sec, median latency, max latency), memory consumption, GC events, and how you'd control for noise (process isolation, repeated runs). Provide pseudocode for the harness and describe how to interpret the results for production decisions.
Sample Answer
Approach summary- Create a repeatable harness that isolates runs (separate Node process per run), warms up the JIT, measures ops/sec and per-op latencies, records memory before/after, and captures GC events. Run many iterations and summarize median/percentiles and max. Control noise via process isolation, CPU pinning (if available), disabling other services, and repeated runs.Pseudocode (Node.js-style)What to measure & how- Warm-up: run both access patterns for several seconds to let V8 optimize.- Ops/sec: run a large fixed-count tight loop; compute iterations / elapsed seconds.- Latency: record per-op durations (performance.now) and compute median, p50/p95/p99, and max.- Memory: sample process.memoryUsage() before and after structure creation and after measurements (heapUsed, rss).- GC events: run Node with --expose-gc and use PerformanceObserver to subscribe to 'gc' entries, or collect GC logs via --trace_gc; count full vs minor collections and record pause durations.- Noise control: run each test in a fresh child process, run multiple processes sequentially, pin CPU or run on isolated machine, disable background load, set NODE_ENV=production, use consistent node version and flags.Analysis & interpretation- Compare ops/sec and median/p95 for Map vs Object. If Map shows higher ops/sec and lower tail latency, prefer Map for heavy dynamic-key workloads. If Object is slightly faster but memory usage is dramatically lower, and keys are static strings, Object might be preferable.- Consider GC: frequent/long GC pauses (especially full GCs) for one structure indicate allocation patterns that will harm production latency — favor the structure with fewer/shorter pauses.- Use percentiles (p95/p99) for SLO evaluation rather than mean. If p99 exceeds acceptable latency, redesign access pattern or use pooling/sharding to reduce working set.- Validate in production-like environment (same input distribution, concurrency) and include stress tests.Best practices- Automate: run harness in CI over multiple node versions and CPU types.- Report: ops/sec, p50/p95/p99, max latency, heapUsed delta, GC count and max pause.- Make decision based on latency SLOs, memory budget, and GC behavior rather than raw average throughput alone.
javascript
// run in child process; run Node with --expose-gc
const {performance, PerformanceObserver} = require('perf_hooks');
function buildData(n){
const keys = Array.from({length:n}, (_,i)=>`k${i}`);
const obj = Object.create(null);
const map = new Map();
for(const k of keys){ obj[k]=k; map.set(k,k); }
return {keys, obj, map};
}
function warmup(getFn, keys, durationMs=5000){
const end = performance.now()+durationMs;
let i=0;
while(performance.now()<end){
getFn(keys[i%keys.length]);
i++;
}
}
function measure(getFn, keys, iterations){
const latencies = new Float64Array(iterations);
const t0 = performance.now();
for(let i=0;i<iterations;i++){
const s = performance.now();
getFn(keys[i%keys.length]);
latencies[i] = performance.now()-s;
}
const t1 = performance.now();
return {opsPerSec: iterations/((t1-t0)/1000), latencies: Array.from(latencies)};
}
function snapshotMemory(){ return process.memoryUsage(); }
// main
const {keys,obj,map} = buildData(1_000_000);
global.gc(); warmup(k=>obj[k], keys); warmup(k=>map.get(k), keys);
global.gc(); const before = snapshotMemory();
const r1 = measure(k=>obj[k], keys, 200_000);
global.gc(); const after1 = snapshotMemory();
global.gc(); const r2 = measure(k=>map.get(k), keys, 200_000);
global.gc(); const after2 = snapshotMemory();
// emit results, GC counts can be collected by PerformanceObserver if enabledUnlock Full Question Bank
Get access to hundreds of Basic Data Structures (Objects, Maps, Sets) interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.